gRPC-WebをGoとTypeScriptで実装してみた

## はじめに
最近話題？のgRPC（gRPC-Web）を、フロントエンドはTypeScript、バックエンドはGoで実装してみました。

今回試した内容は、画面でボタンをクリックすると、gRPCでバックエンド処理を呼び出し、戻り値を画面表示するというシンプルなものです。

なお、説明で使用しているサンプルコードは、簡単に試せるよう以下にも上げてあります。
https://github.com/kahia1114/grpc-web-sample

## 前提条件
- 以下で説明するサンプルコードは、Macで動作確認しています。

- 端末に以下のパッケージがインストール済みであることを前提としています。

  - Node.js v12.4.0
  - protobuf 3.12.4
  - protoc-gen-grpc-web
  - protoc-gen-go
  - Go 1.15

  ※MacにHomeBrew導入済みであれば、`brew install node protobuf protoc-gen-grpc-web protoc-gen-go go`でインストールできると思います。

## 事前準備
このあと利用するディレクトリを事前に作成しておきます。

```console
[~]$ mkdir grpc-sample && cd grpc-sample
[~/grpc-sample]$ mkdir -p proto static ts/grpcpb go/grpcpb
```

## gRPCサービス定義
gRPCサービスのインタフェースをProtocol Buffersで定義します。

引数にHelloRequestを取り、戻り値でHelloReplyを返す、SayHelloというAPIを定義しています。

```proto:proto/hello.proto
syntax = "proto3";

option go_package = ".;grpcpb";

service HelloService {
  rpc SayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest { string name = 1; }

message HelloReply { string message = 1; }
```

バックエンド用（Go）のgRPCコードを生成します。

```console
[~/grpc-sample]$ protoc \
    --go_out=plugins=grpc:go/grpcpb proto/hello.proto
[~/grpc-sample]$ tree go/
go/
└── grpcpb
    └── hello.pb.go
```

フロントエンド用（TypeScript）のgRPCコードを生成します。

```console
[~/grpc-sample]$ protoc \
    --grpc-web_out=import_style=typescript,mode=grpcwebtext:ts/grpcpb \
    --js_out=import_style=commonjs:ts/grpcpb -I=proto proto/hello.proto
[~/grpc-sample]$ tree ts/
ts/
└── grpcpb
    ├── HelloServiceClientPb.ts
    ├── hello_pb.d.ts
    └── hello_pb.js
```

## 画面HTML
画面用のHTMLを作成します。

gRPCサービス呼び出し用の送信ボタン、gRPCサービス引数用の入力欄（名前）、gRPCサービス戻り値を表示するための結果欄を配置しています。

```html:static/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>gRPC-Web sample</title>
    <script src="build/bundle.js" defer></script>
  </head>
  <body>
    <h1>gRPC-Web sample</h1>
    <hr />
    <div>
      名前：<input type="text" id="name" value="Test" />
      <input type="button" id="sendBtn" value="送信" />
    </div>
    <div>結果：<input type="text" id="result" readonly /></div>
  </body>
</html>
```

## フロントエンド実装
フロントエンド処理をTypeScriptで実装します。

送信ボタンクリック時に、gRPCサービス定義から生成されたAPI（`HelloServiceClient#sayHello()`）を呼び出し、戻り値を結果欄に表示します。

```ts:ts/main.ts
import { HelloServiceClient } from "./grpcpb/HelloServiceClientPb";
import { HelloRequest } from "./grpcpb/hello_pb";

const htmlInputElement = (id: string) => {
  return <HTMLInputElement>document.getElementById(id);
};

const requestHello = async () => {
  const request = new HelloRequest();
  request.setName(htmlInputElement("name").value);

  const client = new HelloServiceClient("http://localhost:8080");

  const reply = await client.sayHello(request, {});

  htmlInputElement("result").value = reply.getMessage();
};

htmlInputElement("sendBtn").addEventListener("click", requestHello);
```

TypeScriptをコンパイルするための設定ファイルを作成します（JSファイルをまとめるためにWebpackを使用します）。

```json:ts/tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "noImplicitAny": true
  }
}
```

```js:ts/webpack.config.js
module.exports = {
  mode: "development",
  entry: "./main.ts",
  output: {
    path: __dirname + "/../static/build",
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: "ts-loader",
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};
```

```json:ts/package.json
{
  "name": "grpc-web-sample",
  "version": "1.0.0",
  "scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "google-protobuf": "^3.13.0",
    "grpc-web": "^1.2.0",
    "ts-loader": "^8.0.2",
    "typescript": "^3.9.7",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  }
}
```

TypeScriptをコンパイルするために依存するNode.jsモジュールをインストールします。

```console
[~/grpc-sample]$ cd ts
[~/grpc-sample/ts]$ npm install
```

TypeScriptソースをコンパイルします。

```console
[~/grpc-sample/ts]$ npm run build
[~/grpc-sample/ts]$ cd ..
[~/grpc-sample]$ tree static/
static/
├── build
│   └── bundle.js
└── index.html
```

## バックエンド実装
バックエンド処理をGoで実装します。

gRPCサービス定義から生成されたAPI（`SayHello()`）の処理を実装します。

gRPC-Web用のProxyとして、[grpcweb](https://github.com/improbable-eng/grpc-web/tree/master/go/grpcweb)を使用しています。

```go:go/main.go
package main

import (
	"context"
	"log"
	"net/http"

	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"google.golang.org/grpc"

	"grpc-web-sample/grpcpb"
)

type helloService struct{}

func (s *helloService) SayHello(ctx context.Context, req *grpcpb.HelloRequest) (*grpcpb.HelloReply, error) {
	log.Printf("Recieved name: %s", req.GetName())
	return &grpcpb.HelloReply{Message: "Hello, " + req.GetName() + "!!"}, nil
}

func main() {
	http.Handle("/static/", http.FileServer(http.Dir("../")))

	grpcServer := grpc.NewServer()
	grpcpb.RegisterHelloServiceServer(grpcServer, &helloService{})

	wrapServer := grpcweb.WrapServer(grpcServer)
	http.Handle("/", wrapServer)

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatalf("Failed to serve: %v", err)
	}
}
```

Goモジュールを初期化します。

```console
[~/grpc-sample]$ cd go
[~/grpc-sample/go]$ go mod init grpc-web-sample
go: creating new go.mod: module grpc-web-sample
```

## 動作確認

バックエンド処理を起動します。

```console
[~/grpc-sample/go]$ go run main.go
```

ブラウザで以下のURLにアクセスします。

http://localhost:8080/static/index.html

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427113/68f43f43-2f59-f78b-6634-e18fe7e29b42.png)

送信ボタンをクリックすると、gRPCでバックエンド処理が呼び出され、戻り値が結果欄に表示されます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427113/12284497-6f27-8f07-0dc6-34a787723749.png)

## おわりに
今回は、gRPC-WebをGoとTypeSciptで試してみました。フロントエンド／バックエンドのインタフェース部分のコードをgRPCサービス定義から生成できるのは、本当に便利ですね！（しかもProtocol Bufferesの定義がソースコードっぽくて明瞭なのも良いですね）

ぜひ、今後のWebシステム開発の主流になっていって欲しいと感じました。