はじめに
最近話題?の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
でインストールできると思います。
事前準備
このあと利用するディレクトリを事前に作成しておきます。
[~]$ mkdir grpc-sample && cd grpc-sample
[~/grpc-sample]$ mkdir -p proto static ts/grpcpb go/grpcpb
gRPCサービス定義
gRPCサービスのインタフェースをProtocol Buffersで定義します。
引数にHelloRequestを取り、戻り値でHelloReplyを返す、SayHelloというAPIを定義しています。
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コードを生成します。
[~/grpc-sample]$ protoc \
--go_out=plugins=grpc:go/grpcpb proto/hello.proto
[~/grpc-sample]$ tree go/
go/
└── grpcpb
└── hello.pb.go
フロントエンド用(TypeScript)のgRPCコードを生成します。
[~/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サービス戻り値を表示するための結果欄を配置しています。
<!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()
)を呼び出し、戻り値を結果欄に表示します。
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を使用します)。
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"noImplicitAny": true
}
}
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"],
},
};
{
"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モジュールをインストールします。
[~/grpc-sample]$ cd ts
[~/grpc-sample/ts]$ npm install
TypeScriptソースをコンパイルします。
[~/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を使用しています。
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モジュールを初期化します。
[~/grpc-sample]$ cd go
[~/grpc-sample/go]$ go mod init grpc-web-sample
go: creating new go.mod: module grpc-web-sample
動作確認
バックエンド処理を起動します。
[~/grpc-sample/go]$ go run main.go
ブラウザで以下のURLにアクセスします。
送信ボタンをクリックすると、gRPCでバックエンド処理が呼び出され、戻り値が結果欄に表示されます。
おわりに
今回は、gRPC-WebをGoとTypeSciptで試してみました。フロントエンド/バックエンドのインタフェース部分のコードをgRPCサービス定義から生成できるのは、本当に便利ですね!(しかもProtocol Bufferesの定義がソースコードっぽくて明瞭なのも良いですね)
ぜひ、今後のWebシステム開発の主流になっていって欲しいと感じました。