Vue.js×Go×EvansでgRPC-Webの動作検証
仕事上gRPC-Webの学習が必要になったため、
Evansで各段階のテストを行いながら動作検証してみることにしました。
クライアント側はほとんどこちらの記事を
参考にさせていただきました。
※本記事はブログからの移行記事となります。
今回の検証に利用したサンプルコードは以下で公開中です。
https://github.com/gosarami/grpc_sandbox
対象者
- 基本的なgRPCの概念は理解している
- protocなどのgRPC用の環境構築を既に終えている
今回のゴール
gRPC-Webの検証として、以下の繋ぎ込みを行っていきます。
クライアント(JavaScript) -> リバースプロキシ(Envoy) -> サーバー(Go)
なお、中間にリバースプロキシを挟んでいる理由は、2020年11月現在gRPC-Webクライアントから、直接バックエンドサービスに対してgRPCコールを呼び出すことができないためです。
将来的には、ブラウザの対応が進むことによって中間のプロキシーは不要になっていくようです。
環境
- ディレクトリ構成
$ tree -d -L 2
.
├── client
│ ├── vanilla //生js
│ └── vue //Vue.js
├── protobuf
├── proxy
└── server
- MacOS Mojave
- Go: 1.12.3
- NPM: 6.7.0
- Vue Cli: 3.4.1
- Docker: 19.03.1
- docker-compose: 1.24.1
- protoc: 3.7.1
- protoc-gen-gRPC-Web: 1.0.6
- evans: 0.8.2
大まかな手順
- protobufの定義ファイルを作成
- protobufの定義ファイルからサーバー・クライアント両方のスタブを生成
- サーバー側の実装
- gRPC-Web対応のリバースプロキシを構築
- クライアント側の実装
protobufの定義ファイルを作成
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
protobufの定義ファイルかサーバー・クライアント両方のスタブを生成
# サーバー
$ protoc -I=. --go_out=plugins=grpc:. helloworld.proto
# クライアント
$ protoc -I=. helloworld.proto \
--js_out=import_style=commonjs:. \
--gRPC-Web_out=import_style=commonjs,mode=grpcwebtext:.
protoファイルを更新するたびにサーバー・クライアント双方の変更が必要になるので、実運用では何らかの自動更新フローを確立する必要があります。
具体的な対応としては、protoファイルの更新を起点にした専用のCIを組むか、protobufをvendoringする以下のようなツールを利用するのがポイントになります。
サーバー側の実装
- スタブ生成時に以下のファイルが生成されたことを確認
helloworld.pb.go
- インターフェースを参考に以下のように実装
package main
import (
"context"
"log"
"net"
pb "grpc_sandbox/protobuf"
"google.golang.org/grpc"
)
func main() {
listener,err := net.Listen("tcp", ":5300")
if err != nil {
log.Fatalf("failed to listen %v¥n",err)
return
}
grpcSrv := grpc.NewServer()
pb.RegisterGreeterServer(grpcSrv,&server{})
log.Printf("hello service is running!")
grpcSrv.Serve(listener)
}
type server struct{}
func (s *server) SayHello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloReply, error){
return &pb.HelloReply{
Message: "hello! " + request.GetName(),
},nil
}
- サーバー起動
$ go run server/main.go
- サーバーの動作確認
$ evans --host localhost -p 5300 helloworld.proto
helloworld.Greeter@localhost:5300> call SayHello
name (TYPE_STRING) => server test
{
"message": "hello! server test"
}
gRPC-Web対応のproxyを構築
gRPC対応のリバースプロキシとしてはNginx or Envoyを選択可能ですが、
今回は公式チュートリアルでも利用されているEnvoyを利用します。
- envoy.yamlを用意
※テスト用にCORSの設定を緩めていますが、本番の利用の際は適切な設定を行ってください
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: greeter_service
max_grpc_timeout: 0s
cors:
allow_origin:
- "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-gRPC-Web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
enabled: true
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: greeter_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
hosts: [{ socket_address: { address: host.docker.internal, port_value: 5300 }}]
- Envoyのイメージ作成用のDockerfileを書く
FROM envoyproxy/envoy:latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
- docker-compose.ymlを書く
version: "3"
services:
proxy:
build: .
ports:
- "8081:8080"
- docker-compose up
$ docker-compose up -d
- 動作確認
# --webオプションを足すとgRPC-Webのテストができる
$ evans --web --host localhost -p 8081 helloworld.proto
helloworld.Greeter@localhost:8081> call SayHello
name (TYPE_STRING) => proxy test
{
"message": "hello! proxy test"
}
クライアント側の実装
vanilla.js
- 先述のprotocコマンドで以下のファイルが生成されたことを確認
helloworld_pb.js
helloworld_grpc_web_pb.js
- client.jsを作成
const {HelloRequest, HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');
# 以下の記述はChromeのgRPC-Web拡張用のものなので、なくても動く
# https://chrome.google.com/webstore/detail/gRPC-Web-developer-tools/ddamlpimmiapbcopeoifjfmoabdbfbjj
const enableDevTools = window.__GRPCWEB_DEVTOOLS__ || (() => {});
const client = new GreeterClient('http://localhost:8081');
enableDevTools([
client,
]);
const request = new HelloRequest();
request.setName('World!');
client.sayHello(request, {}, (err, response) => {
console.log(response.getMessage());
});
- gRPC用ライブラリ、Webpackをインストールするためpackage.jsonを用意
{
"name": "gRPC-Web-simple-example",
"version": "0.1.0",
"description": "gRPC-Web simple example",
"devDependencies": {
"@grpc/proto-loader": "^0.3.0",
"google-protobuf": "^3.6.1",
"grpc": "^1.15.0",
"gRPC-Web": "^1.0.0",
"webpack": "^4.16.5",
"webpack-cli": "^3.1.0"
}
}
- npm install
$ npm i
- webpackでバンドル
以下のコマンドを実行した後、dist/main.jsが出力されていることを確認します。
$ npx webpack client.js --mode development
- バンドルされたJavaScriptを読むindex.html作成
<meta charset="UTF-8">
<title>gRPC-Web Example</title>
<script src="dist/main.js"></script>
- 簡易webサーバを立ち上げる
今回はPython経由でビルトインサーバーを起動していますが、
なんでもOKです。
$ python3 -m http.server 8080
- localhost:8080にChromeでアクセス
- consoleに<hello!World>が出力されていればOK
Vue.js
- Vue CLIを利用して新規プロジェクトを作成
$ cd client/
$ vue create vue
- 一旦動作確認
$ npm run serve
- 必要なライブラリをインストール
$ npm i --save google-protobuf gRPC-Web babel-plugin-add-module-export
- 吐いたcommonjsの構文とesの構文が混在するのを解決するために、babelで追加設定を行う
module.exports = {
presets: [
'@vue/app',
],
plugins: ["add-module-exports"] //←この一行を追加
}
- console.log()したい場合は、package.jsonに以下追記しておく
Vue CLI3系から.eslintrcからpackage.jsonに記入する形に変わりました。
"eslintConfig": {
"root": true,
"env": {
~~~
~~~
"rules": {
"no-console": 0
},
- スタブをVueプロジェクト内に配置
$ mkdir -p client/vue/src/components/assets/protobuf
$ cp protobuf/*.js src/components/assets/protobuf
- 生成されたスタブがESLintに怒られないようにIgnoreする
src/assets/protobuf/*.js
- 以下のようなVueコンポーネントを追加
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button @click="helloWorld">HelloButton</button>
</div>
</template>
<script>
// eslint-disable-next-line
const {HelloRequest, HelloReply} = require('../assets/protobuf/helloworld_pb.js');
const {GreeterClient} = require('../assets/protobuf/helloworld_grpc_web_pb.js');
// for gRPC-Web debug
const enableDevTools = window.__GRPCWEB_DEVTOOLS__ || (() => {});
const client = new GreeterClient('http://localhost:8081');
enableDevTools([
client,
]);
export default {
name: 'HelloWorld',
data: function() {
return {
msg: ""
}
},
methods: {
helloWorld: function(){
// eslint-disable-next-line
const request = new HelloRequest();
request.setName('gRPC-Web with vue');
client.sayHello(request, {}, (err, response) => {
//console.log(response.getMessage());
this.msg = response.getMessage();
});
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
- App.vueでインポート先を変更
//import HelloWorld from './components/HelloWorld.vue'
import HelloWorld from './components/TestGrpc.vue'
...
- もう一度ビルトインサーバを起動
$ npm run serve
- ブラウザからlocalhost:8080にアクセスし、<hello! grpc-web with vue>というメッセージが表示されたらOK
参考
Discussion