Vue+Golang+gRPC-webを試してみた

個人開発でgRPC-web使ってみたいなぁと思ったので動作確認してみた

gRPCやvue, goについては特に解説ないです


作るもの

リポジトリ

gRPCのserviceで数の加算と、これまで加算された値の合計を返すものを用意

Goでこれらの処理を実行, vueから呼び出して、結果を画面に反映させる

サーバ側で現在の値を保持しているので、画面を再読み込みしても、合計値が初期化されない

gRPC-web.gif


各種バージョン

// 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ファイルの作成


sandbox.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の作成


handler.go

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の作成


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の編集


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ポートへ繋げてる


envoy.yaml

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