4
3

More than 3 years have passed since last update.

Vue 3.0とgRPCを使ってTodoListを作ってみた

Last updated at Posted at 2020-08-17

gRPCとは?

gRPCはオープンソース、RPCフレームワークをベースとして、最初はGoogleが開発されました。
インターフェース記述言語としてProtocol Buffersを使用し、protobufは構造化データをシリアル化するためのメカニズムです。
protoファイルでサービスとそのデータ構造を定義するだけで、gRPCがさまざまな言語でプラットフォームのクライアントとサーバーのStubsを自動的に生成します。
profobufを使用すると、JSONではなくバイナリを使用して資料を転送しています。
これにより、gRPCがはるかに高速で信頼性の高いものになります。
gRPCの他の主要な機能のいくつかは、双方向ストリーミングとフロー制御、BlockingまたはNonBlockingバインディング、および認証機能です。
gRPCはHTTP/2を使用して、シングルTCPコネクションの中で複数のストリームを開始することができます。
gRPCの詳細については、こちらをご覧ください:https://grpc.io/

gRPC V.S. REST

Feature gRPC REST
Portocol  HTTP/2 (早い) HTTP/1.1 (遅い)
Payload  Protobuf (バイナリ、小さい) JSON (テキスト、大きい)
API構造  厳格、必要 (.proto) ゆるい、選択
Code生成  内蔵 (protoc) 他のツール (Swagger)
安全性  TLS/SSL TLS/SSL
ストリーミング  双方向ストリーミング クライアント -> サーバーリクエストだけ
ブラウザのサポート 制限あり (grpc-webは必要) ほぼ全部

Protobuf と gRPC を使えば、REST API の GET、PUT やヘッダーなどを気にする必要はありません、そしてgRPCフレームワークによって生成されたStubsにはデータモデル用の記述が全部書いてるので、直接引用するだけで使えます。

開発環境とツール

  • Protoc v3.12.4 -- Protobuf コンパイラー Stubs を生成する為に使ます。
  • Node.js v14.2.0 -- バックエンドとVueのビルドに使います。
  • Docker v19.03.12 -- envoyを動かす為に使ます。
  • envoy v1.14 -- 普通WebからのHTTP/1.1をHTTP/2に変換する為のプロキシ。
  • Vue.js 3.0.0-rc.5 -- 今回は Vue 3 を使ってフロントエンドを作成します。
  • Docker Compose v1.26.2 -- 全部をDocker化する為に使います、なくでも動けます。

フォルダ構成

dya2g02.png

全体の流れ

  1. Protoファイルの作成
  2. Node.jsでバックエンド作成
  3. Envoy proxyの設定
  4. Client stubsの生成
  5. Clientの作成
  6. 動かしてみましょう
  7. Docker化

コードを書いてみましょう

1. Protoファイルの作成

ProtoファイルはgRPCの心臓と呼ばれる部分、ここでRequestとResponseとサービスを定義することによって、後でStubsファイルを自動的に生成することができます。
Protoファイルの構成は大体四つの部分に分けている。
1. Protoのバージョンを定義する
2. Packageの名前
3. サービス定義
4. メッセージ定義

todo.proto
syntax = "proto3";

package todo;

service todoService {
  rpc addTodo(addTodoParams) returns (todoObject) {}
  rpc deleteTodo(deleteTodoParams) returns (deleteResponse) {}
  rpc getTodos(getTodoParams) returns (todoResponse) {}
}
// Request
message getTodoParams{}

message addTodoParams {
  string task = 1;
}

message deleteTodoParams {
  string id = 1;
}
// Response
message todoObject {
  string id = 1;
  string task = 2;
}

message todoResponse {
  repeated todoObject todos = 1; //ここはArrayの中身にtodoObjectが複数ありますのこと。
}

message deleteResponse {
  string message = 1;
}

2. Node.jsでバックエンド作成

環境設定:

npm:

bash
npm init -y
npm i uuid grpc @grpc/proto-loader
npm i -D nodemon

yarn:

bash
yarn init -y
yarn add uuid grpc @grpc/proto-loader
yarn add -D nodemon

startのnodeをnodemonに書き換える:

package.json
{
  "name": "server",
  "version": "1.0.0",
  "description": "A Node.js gRPC API Server",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon server.js"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "nodemon": "^2.0.4"
  },
  "dependencies": {
    "@grpc/proto-loader": "^0.5.5",
    "grpc": "^1.24.3",
    "uuid": "^8.3.0"
  }
}

サーバーのコード内容:

server.js
// proto ファイルのパス
const todoProtoPath = './todo.proto';
// npm packageを導入
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const { v4: uuidv4 } = require('uuid');
// grpcの初期化
const packageDefinition = protoLoader.loadSync(
  todoProtoPath,
  {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  },
);
// packageを指定
const todoProto = grpc.loadPackageDefinition(packageDefinition).todo;

// Todosの保存、リースダートしたら資料が消えます
let Todos = [];

const addTodo = (call, callback) => {
  const todoObject = {
    id: uuidv4(),
    task: call.request.task,
  };
  console.log(call.request);
  Todos.push(todoObject);
  console.log(`Todo: ${todoObject.id} added!`);
  callback(null, todoObject);
};

const getTodos = (call, callback) => {
  console.log('Get tasks');
  console.log(Todos);
  callback(null, { todos: Todos });
};

const deleteTodo = (call, callback) => {
  Todos = Todos.filter((todo) => todo.id !== call.request.id);
  console.log(`Todo: ${call.request.id} deleted`);
  callback(null, { message: 'Success' });
};

const getServer = () => {
  const server = new grpc.Server();
  // サービスを登録、名前はprotoファイルと同じなので省略できます
  server.addService(todoProto.todoService.service,
    { addTodo, getTodos, deleteTodo });
  return server;
};

if (require.main === module) {
  const server = getServer();
  server.bind('0.0.0.0:9090', grpc.ServerCredentials.createInsecure());
  server.start();
  console.log('Server running at port: 9090');
}

3. Envoy proxyの設定

Envoy proxyはサーバーとクライアントの中央にいるサービスです、主にはHTTP/1.1のコネクションをHTTP/2に変換するの役割です。

Dockerのイメージ設定ファイル

Dockerfile
FROM envoyproxy/envoy:v1.14-latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

Envoyの設定ファイル

envoy.yaml
admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: todo_service
                  max_grpc_timeout: 0s
              cors:
                allow_origin_string_match:
                  - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.filters.http.grpc_web
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router
  clusters:
  - name: todo_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    load_assignment:
      cluster_name: cluster_0
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    # docker-composeを使うときserverに書き換えます
                    address: host.docker.internal
                    port_value: 9090

gRPCサービスは9090 portで動かして、Envoyは8080 portでWebからのHTTP/1.1をHTTP/2に変換して9090に送るそいう仕組みです。

4. Client stubsの生成

protocをインストール : Protocol Buffer Compiler Installation

bash
# Linux
$ apt install -y protobuf-compiler
$ protoc --version
# MacOS using Homebrew
$ brew install protobuf
$ protoc --version

先にVueのProjectを作成します。
vue-cliを使います。

bash
vue create client

そしてStubsを作ります
このコメンドで ./client/src に二つのJSファイルを生成する

  • todo_pb.js // メッセージのType定義
  • todo_grpc_web_pb.js // gRPCクライアント
bash
protoc -I server todo.proto \
    --js_out=import_style=commonjs,binary:client/src \
    --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/src

5. Clientの作成

クライアントのTodoコンポーネントの中にtodo_pb.jsとtodo_grpc_web_pb.jsを導入して、todoServiceClient()を使ってlocalhost:8080のEnvoy proxyに接続します。

Todo.vue
import { ref } from 'vue'
// クライアントが使う部分だけを導入する
import { getTodoParams, addTodoParams, deleteTodoParams } from "../todo_pb.js";
import { todoServiceClient } from "../todo_grpc_web_pb.js";
import CloseIcon from './CloseIcon'
export default {
  components:{ CloseIcon },
  setup() {
    const todos = ref([])
    const inputField = ref('')
    // 新しクライアントのインスタンスを作成
    const client = new todoServiceClient("http://localhost:8080", null, null);

    const getTodos = () => {
      let getRequest = new getTodoParams();
      client.getTodos(getRequest, {}, (err, response) => {
        if (err) console.log(err);
        console.log(response.toObject());
        todos.value = response.toObject().todosList;
      });
    }

    getTodos()

    const addTodo = () => {
      let addRequest = new addTodoParams();
      addRequest.setTask(inputField.value);
      client.addTodo(addRequest, {}, (err) => {
        if (err) console.log(err);
        inputField.value = "";
        getTodos();
      });
    }
    const deleteTodo = (todo) => {
      let deleteRequest = new deleteTodoParams();
      deleteRequest.setId(todo.id);
      client.deleteTodo(deleteRequest, {}, (err, response) => {
        if (err) console.log(err);
        if (response.getMessage() === "Success") {
          getTodos();
        }
      });
    }

    return {
      todos,
      inputField,
      addTodo,
      deleteTodo
    }
  }
}

完成の参考 : Github

6. 動かしてみましょう

Back-Endを立ち上げて:

bash
$ cd ./server
$ npm start

enovy proxy:

bash
$ docker build -t envoy:v1 ./enovy
$ docker run --rm -it -p 8080:8080 envoy:v1

Front-End:

bash
$ cd ./client
$ yarn dev

成功すればこんな感じです:
giphy.gif

7. Docker化

Docker Compose一発で動かす為にDocker化します。
各フォルダにDockerfileと.dockerignore入れます。

./serverフォルダ

Dockerfile
FROM node:lts-alpine

# make the 'app' folder the current working directory
WORKDIR /app

# copy both 'package.json' and 'package-lock.json' (if available)
COPY package*.json ./

# install project dependencies
RUN npm install

# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY . .

EXPOSE 9090
CMD [ "node", "server.js" ]
.dockerignore
.git
.gitignore

node_modules

README.md

./clientフォルダ

Dockerfile
FROM node:lts-alpine

# install simple http server for serving static content
RUN npm install -g http-server

# make the 'app' folder the current working directory
WORKDIR /app

# copy both 'package.json' and 'package-lock.json' (if available)
COPY package*.json ./
COPY yarn.lock ./

# install project dependencies
RUN yarn install

# copy project files and folders to the current working directory (i.e. 'app' folder)
COPY . .

# build app for production with minification
RUN yarn run build

EXPOSE 3000
CMD [ "http-server", "-p", "3000", "dist" ]
.dockerignore
.git
.gitignore

node_modules

README.md

./docker-composer.ymlを作成します。

docker-compose.yml
version: '3'

services: 
  web:
    build: ./client
    image: todo-grpc-vue-client:v1
    ports: 
      - 3000:3000
    restart: unless-stopped
    networks:
      - grpc-todolist
  proxy:
    build: ./envoy
    image: todo-grpc-envoy:v1
    ports: 
      - 8080:8080
    restart: unless-stopped
    networks: 
      - grpc-todolist
  server:
    build: ./server
    image: todo-grpc-server:v1
    restart: unless-stopped
    networks:
      - grpc-todolist
networks:
  grpc-todolist:
    driver: bridge

Envoyの設定ファイルのサーバーアドレス修正します

envoy.yaml
endpoints:
  - lb_endpoints:
      - endpoint:
          address:
          socket_address:
          # host.docker.internalをserverに書き換える
          address: server
          port_value: 9090

そしてビルドして立ち上げます。

bash
# イメージをビルド
$ docker-compose build
# 特定のイメージをリビルドします
$ docker-compose build --no-cache [service]
# 立ち上げる
$ docker-compose up
# 背景で立ち上げる
$ docker-compose up -d
# ログを確認
$ docker-compose logs

Front-Endに入ります:http://localhost:3000

最後

Qiitaで初めての投稿です、よろしくお願いします。
Githubでソースコードを公開しています、
なんが詰まったところがあれば参考してください:Github

4
3
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
4
3