一、为什么?
在 NestJS
中使用 gRPC
实现微服务通信, proto
文件编译成 ts
文件避免手动 TS
类型文件。
二、安装
sh
# linux
apt install -y protobuf-compiler
protoc --version # 查看安装是否成功,和版本
# mac
brew install protobuf
protoc --version # 查看安装是否成功,和版本
# pnpm
pnpm global add protoc # 查看安装是否成功,和版本
三、protobuf 类型系统
回顾 proto3 基础知识:
四、ts-proto 插件
关于 protoc typescript 插件有很多,这使用 ts-proto
五、基于 pnpm 实战 gRPC + protoc + ts-proto
考虑到微服务,这里使用 pnpm workspace 作为基础工具, 在 pnpm workspace 子项目中完成对 proto
文件进行编译。
1) 初始化
sh
cd <your_dir>
pnpm init
touch pnpm-workspace.yaml
追加 workspace 内容:
yaml
- packages:
- "packages/*"
初始化子项目
sh
cd workspace
mkdir ts-proto-test
pnpm init
pnpm add protoc ts-protoc
注意: npm 包的 protoc 中
"postinstall": "node scripts/postinstall.js"
可能需要等待。
2) 编写 proto 文件
以上是一个 user 的 crud/count/list 操作的proto, 其中有非标量
google.protobuf.Timestamp
用于生成 TS 的 Date 类型。
proto
syntax = "proto3";
package user;
import "google/protobuf/timestamp.proto";
service UserService {
rpc CreateUser(CreateUserRequest) returns (UserResponse);
rpc DeleteUser(DeleteUserRequest) returns (UserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc GetUsersCount(GetUsersCountRequest) returns (GetUsersCountResponse);
}
message User {
string id = 1;
string username = 2;
string password = 3;
string email = 4;
optional string role = 5;
google.protobuf.Timestamp createdAt = 6;
google.protobuf.Timestamp updatedAt = 7;
}
message CreateUserRequest {
string uesrname = 1;
string password = 2;
string email = 3;
optional string role = 4;
google.protobuf.Timestamp createdAt = 5;
google.protobuf.Timestamp updatedAt = 6;
}
message DeleteUserRequest {
string id = 1;
}
message UpdateUserRequest {
string id = 1;
string username = 2;
string password = 3;
string email = 4;
string role = 5;
google.protobuf.Timestamp updatedAt = 6;
}
message GetUserRequest {
string id = 1;
}
message UserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 pageSize = 2;
}
message ListUsersResponse {
repeated User users = 1;
}
message GetUsersCountRequest {}
message GetUsersCountResponse {
int32 count = 1;
}
3) 编写 protoc 编译命令
sh
{
"scripts":
"proto-ts": "protoc --plugin=./node_modules/ts-proto/protoc-gen-ts_proto.CMD --ts_proto_out=./ ./src/proto/*.proto --ts_proto_opt=outputEncodeMethods=false,outputJsonMethods=false,outputClientImpl=false"
}
--plugin
指定插件:./node_modules/ts-proto/protoc-gen-ts_proto.CMD
, windows 是 CMD 或者ps1 文件,其他的平台不需要此后缀。--ts_proto_out
: 指定编译后 TS 的输出路径
和 proto文件路径
, 根据自己的项目自定义路径。outputEncodeMethods/outputJsonMethods/outputClientImpl
一些与 proto encode/json/client 相关。
4) 编译后的结果
ts
export const protobufPackage = "user";
export interface User {
id: string;
username: string;
password: string;
email: string;
role?: string | undefined;
createdAt: Date | undefined;
updatedAt: Date | undefined;
}
export interface CreateUserRequest {
uesrname: string;
password: string;
email: string;
role?: string | undefined;
createdAt: Date | undefined;
updatedAt: Date | undefined;
}
export interface DeleteUserRequest {
id: string;
}
export interface UpdateUserRequest {
id: string;
username: string;
password: string;
email: string;
role: string;
updatedAt: Date | undefined;
}
export interface GetUserRequest {
id: string;
}
export interface UserResponse {
user: User | undefined;
}
export interface ListUsersRequest {
page: number;
pageSize: number;
}
export interface ListUsersResponse {
users: User[];
}
export interface GetUsersCountRequest {
}
export interface GetUsersCountResponse {
count: number;
}
export interface UserService {
CreateUser(request: CreateUserRequest): Promise<UserResponse>;
DeleteUser(request: DeleteUserRequest): Promise<UserResponse>;
UpdateUser(request: UpdateUserRequest): Promise<UserResponse>;
GetUser(request: GetUserRequest): Promise<UserResponse>;
ListUsers(request: ListUsersRequest): Promise<ListUsersResponse>;
GetUsersCount(request: GetUsersCountRequest): Promise<GetUsersCountResponse>;
}
可选被编译成联合类型(undefined), 通过引入 google/protobuf/timestamp.proto
解决了 proto 原生没有 Date
的问题,配合其他的类型的时候就更加方便了。google/protobuf/timestamp.proto
文件,会在项目中生成对应的 ts 类型文件加和文件,不需要的话需要删除。
有了 google/protobuf/timestamp.proto
示例,扩展其他其他的数据类型方法也是一样的。基于 protoc npm 包,我们可以找到其他的扩展:
- any
- api
- descriptor
- duration
- empty
- field_mask
- source_context
- struct
- timestamp
- type
- wrappers
等等类型
六、小结
在 NestJS 中使用 gRPC 配合 Prisma ORM 数据库操作的时候,类型不一致。proto 的原生类型不足以支持 TS 中的类型,需要在编译的时候插件支持。本文主要解决了 protoc 在 pnpm workspace 中认知、使用与实践,希望能够帮助大家。