Vue 3.0とgRPCを使ってTodoListを作ってみた
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化する為に使います、なくでも動けます。
フォルダ構成
全体の流れ
- Protoファイルの作成
- Node.jsでバックエンド作成
- Envoy proxyの設定
- Client stubsの生成
- Clientの作成
- 動かしてみましょう
- Docker化
コードを書いてみましょう
1. Protoファイルの作成
ProtoファイルはgRPCの心臓と呼ばれる部分、ここでRequestとResponseとサービスを定義することによって、後でStubsファイルを自動的に生成することができます。
Protoファイルの構成は大体四つの部分に分けている。
- Protoのバージョンを定義する
- Packageの名前
- サービス定義
- メッセージ定義
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:
npm init -y
npm i uuid grpc @grpc/proto-loader
npm i -D nodemon
yarn:
yarn init -y
yarn add uuid grpc @grpc/proto-loader
yarn add -D nodemon
startのnodeをnodemonに書き換える:
{
"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"
}
}
サーバーのコード内容:
// 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のイメージ設定ファイル
FROM envoyproxy/envoy:v1.14-latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
Envoyの設定ファイル
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
# Linux
$ apt install -y protobuf-compiler
$ protoc --version
# MacOS using Homebrew
$ brew install protobuf
$ protoc --version
先にVueのProjectを作成します。
vue-cliを使います。
vue create client
そしてStubsを作ります
このコメンドで ./client/src に二つのJSファイルを生成する
- todo_pb.js // メッセージのType定義
- todo_grpc_web_pb.js // gRPCクライアント
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に接続します。
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を立ち上げて:
$ cd ./server
$ npm start
enovy proxy:
$ docker build -t envoy:v1 ./enovy
$ docker run --rm -it -p 8080:8080 envoy:v1
Front-End:
$ cd ./client
$ yarn dev
成功すればこんな感じです:
7. Docker化
Docker Compose一発で動かす為にDocker化します。
各フォルダにDockerfileと.dockerignore入れます。
./serverフォルダ
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" ]
.git
.gitignore
node_modules
README.md
./clientフォルダ
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" ]
.git
.gitignore
node_modules
README.md
./docker-composer.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の設定ファイルのサーバーアドレス修正します
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
# host.docker.internalをserverに書き換える
address: server
port_value: 9090
そしてビルドして立ち上げます。
# イメージをビルド
$ docker-compose build
# 特定のイメージをリビルドします
$ docker-compose build --no-cache [service]
# 立ち上げる
$ docker-compose up
# 背景で立ち上げる
$ docker-compose up -d
# ログを確認
$ docker-compose logs
Front-Endに入ります:http://localhost:3000
最後
Zennで初めての投稿です、よろしくお願いします。
Githubでソースコードを公開しています、
なんが詰まったところがあれば参考してください:Github
Discussion