About
このテーマの連載、Webフロントエンド編です。
はじめに
おさらいになりますが、WebフロントエンドはgRPC-Webを利用して、ReverseProxy経由でバックエンドのgRPCサービスとやり取りします。
今回のサンプルでは、Webフロントエンドは下記の構成としました。
- TypeScript
- Nuxt.js 2.9.2 : SPAモード
Nuxt.jsのTypeScriptサポートですが、2.9からそれなりの規模の変更が入っている模様なので、2.8以前を使っている場合はサンプルの書き換えが必要になるケースがあるのでご注意下さい。
また、gRPC-Web自体、まだSSRには対応していないようです。今回はSPAモードで検証しましたが、SSRで使いたい場合は別の選択肢を検討する必要がありそうです。
構成
サンプルのWebディレクトリに、Webフロントエンドのプロジェクトが入っています。
プロジェクト作成
公式チュートリアルを参考に、Nuxtプロジェクトを作成しました。
package.json
主要なdependenciesを掲載しておきます。
"dependencies": {
...
"google-protobuf": "^3.10.0-rc.1",
"grpc-web": "^1.0.6",
"vue-property-decorator": "^8.2.2"
},
"devDependencies": {
"@nuxt/typescript-build": "^0.2.6",
...
"@types/google-protobuf": "^3.7.1",
...
}
自分で追加したのは、
- google-protobuf
- grpc-web
- vue-property-decorator
の3つです。
gRPCクライアントの実装
serviceディレクトリを作成し、その配下にgRPCサービス(GreeterService
)を呼び出すためのクライアント実装を配置しました。
service
├── GreeterService.ts
└── grpc ← protocで生成したファイル
├── Greeter_grpc_web_pb.d.ts
├── Greeter_grpc_web_pb.js
├── Greeter_pb.d.ts
└── Greeter_pb.js
個別に見てみます。
protocによるファイル生成
ようやく本題のgRPC-Webが登場します。
GitHubのREADMEを参考に、protocのプラグインとして、protoc-gen-grpc-web
をインストールします。
$ curl -o /tmp/protoc-gen-grpc-web-1.0.6-darwin-x86_64 \
https://github.com/grpc/grpc-web/releases/download/1.0.6/protoc-gen-grpc-web-1.0.6-darwin-x86_64
$ sudo mv /tmp/protoc-gen-grpc-web-1.0.6-darwin-x86_64 \
/usr/local/bin/protoc-gen-grpc-web
$ chmod +x /usr/local/bin/protoc-gen-grpc-web
これでprotoc
コマンドで--grpc-web_out
オプションが使えるようになるので、Greeter.proto
からコードを生成します。
protoc -I=service/src/main/proto Greeter.proto \
--js_out=import_style=commonjs:web/service/grpc \
--grpc-web_out=import_style=commonjs+dts,mode=grpcwebtext:web/service/grpc
import_style
ですが、今回はcommonjs+dts
を指定しました。
Experimentalですがtypescript
も指定できるので試してみたものの、下記の点で実用にはまだ早そうな印象を受けたので、今回は採用しませんでした。
- PromiseベースのClient関数が提供されない (コールバックベースのClient関数のみ)
-
d.ts
をimportしようとしてコンパイルエラーが発生する- TypeScript不勉強につき解決に至らず
GreeterService.tsの実装
protocで生成したファイルを元に、GreeterService
に対するClientを実装しました。
import { GreeterPromiseClient } from './grpc/Greeter_grpc_web_pb'
import { HelloRequest } from './grpc/Greeter_pb'
export default class GreeterService {
constructor(private readonly hostname: string) {
this.client = new GreeterPromiseClient(hostname, null, null)
}
private readonly client: GreeterPromiseClient
public async sayHello(name: string): Promise<{message: string; nameLength: number}> {
const request = new HelloRequest()
request.setName(name)
const response = await this.client.sayHello(request)
return Promise.resolve({
message: response.getMessage(),
nameLength: response.getNamelength()
})
}
}
ClientをVueインスタンスに注入
実装したClientをVueインスタンスに注入するためにプラグインを定義します。
import GreeterService from '~/service/GreeterService'
import { Context } from '@nuxt/types'
declare module 'vue/types/vue' {
interface Vue {
readonly $greeterService: GreeterService
}
}
export default (ctx: Context, inject: (key: string, value: any) => void) => {
const greeterService = new GreeterService(ctx.env['GRPC_HOST'])
inject('greeterService', greeterService)
}
定義したプラグインを、nuxt.config.js
で読み込みます。
export default {
mode: 'spa',
...
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~plugins/greeterService.ts', ssr: false }
],
env: {
GRPC_HOST: process.env.GRPC_HOST || 'http://localhost:8080'
}
}
Component
プラグインでVueインスタンスに$greeterService
が注入されるので、画面のComponentから呼び出してみます。
<template>
<section class="section">
<div class="container">
<h1 class="title">gRPC(gRPC-Web) Sample</h1>
<div class="field">
<label class="label">Name</label>
<div class="control">
<input v-model="name" class="input" type="text" placeholder="Name" />
</div>
</div>
<div class="control">
<button class="button is-primary" @click="send">Send</button>
</div>
<div
v-if="message && nameLength"
class="content"
style="margin-top: 0.75rem;"
>
<blockquote>{{ message }} (nameLength={{ nameLength }})</blockquote>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'
@Component({ name: 'IndexPage' })
class IndexPage extends Vue {
name: string = ''
message: string | null = null
nameLength: number | null = null
async send() {
// Vueインスタンスに注入された$greeterService
const { message, nameLength } = await this.$greeterService.sayHello(
this.name
)
this.message = message
this.nameLength = nameLength
}
}
export default IndexPage
</script>