为什么需要代码生成
在使用 NestJS 开发 gRPC 服务时,默认的 @grpc/proto-loader 方式虽然方便,但存在两个核心问题:
- 缺少类型安全 -- 服务端和客户端都使用
any类型,无法知道服务有哪些方法和属性 - Observable 包装 -- gRPC 返回的都是 Observable 类型,与 TypeScript 的 Promise 风格不一致
使用 grpc-tools + ts-proto 可以从 .proto 文件自动生成类型安全的 TypeScript 代码,解决上述问题。
ts-proto 简介
ts-proto 是一个 Protocol Buffers 到 TypeScript 的代码生成器,专为 Node.js/TypeScript 生态设计。它可以将 .proto 文件生成为:
- 类型安全的接口定义
- 客户端和服务端的 stub 代码
- 支持与 NestJS 集成的代码
GitHub 仓库:stephenh/ts-proto
安装依赖
# 安装 gRPC Node.js 客户端
pnpm add @grpc/grpc-js
# 安装代码生成工具(开发依赖)
pnpm add -D grpc-tools ts-proto
bash
代码生成流程
1. 编写 proto 文件
// protos/hero.proto
syntax = "proto3";
package hero;
service HeroesService {
rpc FindOne (HeroById) returns (Hero) {}
rpc FindMany (HeroByIdList) returns (stream Hero) {}
}
message HeroById {
int32 id = 1;
}
message HeroByIdList {
repeated int32 ids = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
protobuf
2. 使用 grpc-tools 生成 JavaScript 代码
# 生成基础 JS/PB 文件
./node_modules/.bin/grpc_tools_node_protoc \
--js_out=import_style=commonjs,binary:./src/generated \
--grpc_out=grpc_js:./src/generated \
-I ./protos \
./protos/hero.proto
bash
3. 使用 ts-proto 生成 TypeScript 类型
# 生成 TypeScript 接口和客户端代码
./node_modules/.bin/protoc \
--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
--ts_proto_out=./src/generated \
-I ./protos \
./protos/hero.proto
bash
4. 封装为 npm script
{
"scripts": {
"proto:generate": "protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/generated -I ./protos ./protos/*.proto",
"proto:all": "npm run proto:js && npm run proto:ts",
"proto:js": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./src/generated --grpc_out=grpc_js:./src/generated -I ./protos ./protos/*.proto",
"proto:ts": "protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=./src/generated -I ./protos ./protos/*.proto"
}
}
json
生成代码的使用
生成的文件结构
src/generated/
├── hero.ts # ts-proto 生成的 TypeScript 类型和服务定义
├── hero_pb.js # Protocol Buffers 序列化/反序列化代码
└── hero_grpc_pb.js # gRPC 客户端和服务端 stub
text
在 NestJS 中使用生成的类型
// hero.controller.ts
import { Controller } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
import { Hero, HeroById, HeroesService } from '../generated/hero';
@Controller()
export class HeroController {
@GrpcMethod('HeroesService', 'FindOne')
async findOne(data: HeroById): Promise<Hero> {
// data 和返回值都有完整的类型提示
return { id: data.id, name: '钢铁侠' };
}
}
typescript
客户端使用生成的代码
// app.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { HeroesService, Hero } from '../generated/hero';
@Injectable()
export class AppService implements OnModuleInit {
private heroesService: HeroesService;
onModuleInit() {
// 使用生成的类型,方法名和参数都有完整的类型提示
this.heroesService = this.client.getService<HeroesService>('HeroesService');
}
async getHero(id: number): Promise<Hero> {
// 返回值类型明确为 Hero,而非 any
return this.heroesService.findOne({ id }).toPromise();
}
}
typescript
ts-proto 的优势
| 特性 | proto-loader(默认) | ts-proto |
|---|---|---|
| 类型安全 | 无(any 类型) | 完整类型提示 |
| IDE 支持 | 无 | 完整的自动补全 |
| 编译时检查 | 无 | 编译时发现类型错误 |
| 额外步骤 | 无 | 需要运行代码生成 |
| 维护成本 | 低 | proto 文件变更需重新生成 |
注意事项
.proto文件修改后需要重新运行生成命令- 生成的文件应该加入
.gitignore(通过 CI 构建生成)或提交到仓库中(团队协作方便) ts-proto生成的代码风格更接近 TypeScript 习惯,比grpc-tools生成的 JS 风格代码更易读
↑