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

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

January 21, 2019
Programming
golang, gRPC, gRPC-web, vue

個人開発で 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 ファイルの作成


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