Vue+Golang+gRPC-webを試してみた
January 21, 2019
個人開発で gRPC-web 使ってみたいなぁと思ったので動作確認してみた
gRPC や vue, go については特に解説ないです
作るもの
gRPC の service で数の加算と、これまで加算された値の合計を返すものを用意 Go でこれらの処理を実行, vue から呼び出して、結果を画面に反映させる サーバ側で現在の値を保持しているので、画面を再読み込みしても、合計値が初期化されない
各種バージョン
// protobufferのcompiler
// macなら brew install protobuf でインストールできる
$ protoc --version
libprotoc 3.6.1
$ npm -v
6.5.0
$ node -v
v11.6.0
$ go version
go version go1.11.1 darwin/amd64
$ vue --version
3.3.0
フォルダ構成
$ tree -L 3
.
├── docker-compose.yaml
├── sandbox-client
│ ├── Dockerfile
│ ├── README.md
│ ├── build
│ ├── config
│ │ ├── dev.env.js
│ │ ├── index.js
│ │ └── prod.env.js
│ ├── index.html
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── App.vue
│ │ ├── assets
│ │ ├── components
│ │ ├── main.js
│ │ ├── sandbox_grpc_web_pb.js
│ │ └── sandbox_pb.js
│ └── static
├── sandbox-proxy
│ ├── Dockerfile
│ └── envoy.yaml
└── sandbox-server
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
├── sandbox
│ ├── handler.go
│ ├── sandbox.pb.go
│ └── sandbox.proto
└── tmp
└── runner-build
フォルダ作成
$ mkdir sandbox-gRPC-web && cd sandbox-gRPC-web
$ mkdir -p sandbox-server/sandbox
$ vue init webpack sandbox-client
? Project name sandbox-client
? Project description A Vue.js project
? Author esakat <esakat@gmail.com>
? Vue build standalone
? Install vue-router? No
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm
vue-cli · Generated "sandbox-client".
...
server 側の実装
proto ファイルの作成
syntax = "proto3";
package sandbox;
message getTotalNumParams{}
message addNumParams {
int32 number = 1;
}
message totalNum {
int32 total = 1;
}
service addNumService {
rpc addNum(addNumParams) returns (totalNum) {}
rpc getTotalNum(getTotalNumParams) returns (totalNum) {}
}
server 側の stub を生成
// 環境設定
$ export GO111MODULE=on
$ go mod init sandbox-server
// goの生成に必要なライブラリ追加
$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
// path通す
$ export PATH=$PATH:$GOPATH/bin
// stub作成
$ protoc -I sandbox-server/sandbox/ sandbox-server/sandbox/sandbox.proto --go_out=plugins=grpc:sandbox-server/sandbox
$ ls sandbox
sandbox.pb.go sandbox.proto
handler の作成
package sandbox
import (
"log"
"golang.org/x/net/context"
)
// gRPC server
type Server struct {
// 加算される合計値を保持する, goだと0で初期化しなくても,加算時nilエラーとかならないのでこれで
totalNum int32
}
func (s *Server) AddNum(ctx context.Context, addingNum *AddNumParams) (*TotalNum, error) {
// パラメータから数値を取り出して、Serverの合計値に加算
log.Printf("add number")
s.totalNum += addingNum.Number
total := &TotalNum{Total: s.totalNum}
return total, nil
}
func (s *Server) GetTotalNum(ctx context.Context, _ *GetTotalNumParams) (*TotalNum, error) {
// 現在のtotalを返すだけ
log.Printf("return total number")
total := &TotalNum{Total: s.totalNum}
return total, nil
}
main.go の作成
package main
import (
"fmt"
"log"
"net"
"google.golang.org/grpc"
"sandbox-server/sandbox"
)
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 9999))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := sandbox.Server{}
grpcServer := grpc.NewServer()
// serverにserviceを追加
sandbox.RegisterAddNumServiceServer(grpcServer, &s)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %s", err)
} else {
log.Printf("Server started successfully")
}
}
client 側の実装
client 側の stub を作成
gRPC-web のプラグインをインストール
$ git clone https://github.com/grpc/grpc-web
$ cd grpc-web
$ make install-plugin
stub を作成
$ protoc --proto_path=sandbox-server/sandbox --js_out=import_style=commonjs,binary:sandbox-client/src/ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:sandbox-client/src/ sandbox-server/sandbox/sandbox.proto
// sandbox_grpc_web_pb.js, sandbox_pb.jsというファイルが作られる
$ ls sandbox-client/src
App.vue components sandbox_grpc_web_pb.js
assets main.js sandbox_pb.js
中身をみてもらえばわかるんですが、commonjs 形式のファイルになります 現状、gRPC-web として es6 サポートはやっていないようです JavaScript: es6 module generation
このままだと es6 構文と commonjs 構文が混在して、babel がうまく解釈できないので以下の対応を行います
babel-plugin-add-module-exports の追加
ライブラリをインストール
$ npm install babel-plugin-add-module-exports --save-dev
.babelrc を以下のように修正
{
"presets": [
["env", {
- "modules": false,
+ "modules": "commonjs",
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
- "plugins": ["transform-vue-jsx", "transform-runtime"]
+ "plugins": ["transform-vue-jsx", "transform-runtime", "add-module-exports"]
}
これで es6 構文と commonjs 構文を混在できるようになります
eslint の設定変更
自動生成されたファイルは eslint に引っかかるので、.eslintignore
に無視設定を追加
/build/
/config/
/dist/
/*.js
+ src/sandbox_pb.js
+ src/sandbox_grpc_web_pb.js
App.vue の編集
<template>
<div id='app'>
<section>
<span class='title-text'>gRPC Client</span>
<div class='row justify-content-center mt-4'>
<input v-model='inputField' v-on:keyup.enter='addNum' class='mr-1' placeholder='Please input Number'>
<button @click='addNum' class='btn btn-primary'>Add Num</button>
</div>
</section>
<section>
<h2>now total: {{num.total}}</h2>
</section>
</div>
</template>
<script>
import { addNumParams, getTotalNumParams } from './sandbox_pb'
import { addNumServiceClient } from './sandbox_grpc_web_pb'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
export default {
name: 'app',
components: {},
data: function () {
return {
inputField: '',
num: 0
}
},
created: function () {
// eslint-disable-next-line
this.client = new addNumServiceClient('http://localhost:8001', null, null)
this.getTotalNum()
},
methods: {
getTotalNum: function () {
// eslint-disable-next-line
let getRequest = new getTotalNumParams()
// eslint-disable-next-line
this.client.getTotalNum(getRequest, {}, (err, response) => {
this.num = response.toObject()
console.log(this.num)
})
},
addNum: function () {
// eslint-disable-next-line
let request = new addNumParams()
request.setNumber(Number(this.inputField))
// eslint-disable-next-line
this.client.addNum(request, {}, (err, response) => {
this.inputField = ''
this.num = response.toObject()
})
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.title-text {
font-size: 22px;
}
</style>
gRPC-web の使い方の肝はここですね
let request = new addNumParams()
request.setNumber(Number(this.inputField))
// eslint-disable-next-line
this.client.addNum(request, {}, (err, response) => {
this.inputField = ''
this.num = response.toObject()
})
呼び出す service の Params を宣言して、それを client に渡して service 実行という流れになります
引数が必要な service を呼び出すときは宣言した Params に値を設定します
このメソッドはsetXXXXX
という風になっており, proto ファイルで定義した引数のキャメルケースになります
message addNumParams {
int32 number = 1;
}
今回はnumber
という名前で定義しているのでsetNumber
というメソッド名になっています.
この辺りは自動生成された js ファイルを見るとわかりやすいと思います.
Docker で動かす
client と server の間に proxy をかます必要があるようです envoy というやつ proxy だけ Docker でやろうと思ったら go サーバとの接続がうまくいかなかったので,全部 Docker で動かす
各種 Dockerfile
Server
FROM golang:1.11.1
ENV GO111MODULE=on
WORKDIR /go/src/sandbox-server
COPY . .
RUN go get -u github.com/pilu/fresh
CMD ["fresh"]
EXPOSE 9999
Client
FROM node:11.6.0-slim
WORKDIR /sandbox-client
COPY . .
RUN npm install
CMD ["npm", "run", "dev"]
EXPOSE 8080
Proxy
FROM envoyproxy/envoy:latest
RUN apt-get update
COPY envoy.yaml /etc/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy.yaml
COPY する envoy.yaml は以下 8001 ポートで受け付けて、go の 9999 ポートへ繋げてる
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: 8001 }
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: echo_service }
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
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: echo_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: server, port_value: 9999 }}]
docker-compose.yaml
version: "3"
services:
proxy:
build: ./sandbox-proxy
ports:
- "8001:8001"
links:
- "server"
server:
build: ./sandbox-server
ports:
- "9999:9999"
volumes:
- ./sandbox-server:/go/src/sandbox-server
container_name: "server"
client:
build: ./sandbox-client
ports:
- "8080:8080"
links:
- "server"
docker で動かすための準備
client が localhost からしかアクセスできない設定なので、config/index.js
変えておく
// Various Dev Server settings
- host: 'localhost', // can be overwritten by process.env.HOST
+ host: '0.0.0.0', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
build して起動
$ docker-compose build
$ docker-compose up
動作確認
localhost:8080 にアクセス 記事のあたまに貼ったアニメーションみたいな感じ 数字を足して加算されてるのがわかる 一度画面を更新しても、サーバー側が状態を持っているので、初期化されない
参考
https://medium.com/@aravindhanjay/a-todo-app-using-grpc-web-and-vue-js-4e0c18461a3e https://qiita.com/otanu/items/98d553d4b685a8419952#docker