LoginSignup
2
1

More than 3 years have passed since last update.

gRPC-WebをKotlinバックエンドで試した時のメモ - 4. Webフロントエンド編

Posted at

About

このテーマの連載、Webフロントエンド編です。

はじめに

おさらいになりますが、WebフロントエンドはgRPC-Webを利用して、ReverseProxy経由でバックエンドのgRPCサービスとやり取りします。

architecture.png

今回のサンプルでは、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を掲載しておきます。

web/package.json
  "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をインストールします。

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を実装しました。

web/service/GreeterService.ts
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インスタンスに注入するためにプラグインを定義します。

web/plugins/greeterService.ts
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で読み込みます。

web/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から呼び出してみます。

web/pages/index.vue
<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>
2
1
0

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
2
1