LoginSignup
9
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-21

はじめに

最近話題?の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を定義しています。

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コードを生成します。

[~/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サービス戻り値を表示するための結果欄を配置しています。

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/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を使用します)。

ts/tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "noImplicitAny": true
  }
}
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"],
  },
};
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モジュールをインストールします。

[~/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を使用しています。

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モジュールを初期化します。

[~/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システム開発の主流になっていって欲しいと感じました。

9
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6