protoc-gen-ts_proto
protoc-gen-ts_proto 是从 Protocol Buffers 定义文件生成 TypeScript 类型定义的工具,适用于后端使用 gRPC 提供服务的场景。
工具说明
核心特性
- 文件映射:每个
.proto文件对应生成一个 TypeScript 文件 - 类型精确:支持完整的 Protocol Buffers 特性
- 性能优化:生成的代码经过性能优化
- 依赖处理:通过
import自动处理文件间的依赖关系
适用场景
- 后端使用 gRPC 提供服务
- 需要精确的类型定义
- 希望按模块清晰组织代码(每个 proto 文件一个 TypeScript 文件)
安装
安装 protoc
# macOS
brew install protobuf
# Linux
apt-get install protobuf-compiler
# 或从源码编译
# https://github.com/protocolbuffers/protobuf/releases
安装 protoc-gen-ts_proto
ts-proto 是一个 npm 包,有多种安装方式:
方式1:在后端项目中安装(如果后端项目没有 package.json,需要先初始化)
# 如果后端项目是纯 Go 项目,没有 package.json,需要先初始化
npm init -y
# 然后安装 ts-proto
npm install -D ts-proto
安装后,protoc-gen-ts_proto 插件位于 ./node_modules/.bin/protoc-gen-ts_proto
方式2:全局安装(推荐,避免在后端项目中添加 npm 依赖)
npm install -g ts-proto
# 安装后,插件路径通常是:
# macOS/Linux: /usr/local/bin/protoc-gen-ts_proto 或 ~/.npm-global/bin/protoc-gen-ts_proto
# 可以通过 which protoc-gen-ts_proto 查看实际路径
方式3:使用 Docker(推荐,完全避免本地环境依赖)
无需安装,直接使用 Docker 运行,见下面的 Docker 方式示例。
提示
对于纯后端项目,推荐使用方式2(全局安装)或方式3(Docker),避免在后端项目中添加 npm 依赖和 package.json 文件。
使用方式
后端 Makefile 配置
方式1:使用本地 node_modules(需要先 npm install)
FRONT_TYPES_DIR ?= ./frontend-types
PROTO_DIR := handler/grpc/proto
PROTO_FILES := $(shell find $(PROTO_DIR) -name "*.proto")
generate-front-types: ## 生成前端TypeScript类型定义
@echo "generating frontend types to $(FRONT_TYPES_DIR)..."
@mkdir -p $(FRONT_TYPES_DIR)
protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
--proto_path=$(PROTO_DIR) \
--ts_proto_out=$(FRONT_TYPES_DIR) \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
$(PROTO_FILES)
@echo "Frontend types generated to $(FRONT_TYPES_DIR)"
方式2:使用全局安装的插件(推荐)
FRONT_TYPES_DIR ?= ./frontend-types
PROTO_DIR := handler/grpc/proto
PROTO_FILES := $(shell find $(PROTO_DIR) -name "*.proto")
PROTOC_GEN_TS_PROTO := $(shell which protoc-gen-ts_proto)
generate-front-types: ## 生成前端TypeScript类型定义
@mkdir -p $(FRONT_TYPES_DIR)
protoc --plugin=protoc-gen-ts_proto=$(PROTOC_GEN_TS_PROTO) \
--proto_path=$(PROTO_DIR) \
--ts_proto_out=$(FRONT_TYPES_DIR) \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
$(PROTO_FILES)
说明:
PROTOC_GEN_TS_PROTO := $(shell which protoc-gen-ts_proto):动态查找全局安装的插件路径--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true:只生成类型定义 ,不生成序列化方法和公共符号
命令行参数说明
--plugin=protoc-gen-ts_proto=路径:指定 TypeScript 生成器插件路径- 本地安装:
./node_modules/.bin/protoc-gen-ts_proto - 全局安装:使用
$(shell which protoc-gen-ts_proto)动态查找
- 本地安装:
--proto_path:指定 proto 文件的搜索路径,可以多次指定--ts_proto_out:指定 TypeScript 文件输出目录--ts_proto_opt:生成选项,多个选项用逗号分隔
方式3:使用 Docker(推荐,完全避免本地环境依赖)
FRONT_TYPES_DIR ?= ./frontend-types
generate-front-types: ## 生成前端TypeScript类型定义
@echo "generating frontend types to $(FRONT_TYPES_DIR)..."
@mkdir -p $(FRONT_TYPES_DIR)
docker run --rm \
-v $(PWD)/api:/api:ro \
-v $(PWD)/third_party:/third_party:ro \
-v $(PWD)/$(FRONT_TYPES_DIR):/output \
node:18-alpine sh -c "\
apk add --no-cache protobuf && \
npm install -g ts-proto && \
PROTOC_GEN_TS_PROTO=\$$(which protoc-gen-ts_proto) && \
protoc --plugin=protoc-gen-ts_proto=\$$PROTOC_GEN_TS_PROTO \
--proto_path=/api \
--proto_path=/third_party \
--ts_proto_out=/output \
--ts_proto_opt=exportCommonSymbols=false,outputClientImpl=false,esModuleInterop=true,onlyTypes=true \
/api/**/*.proto"
@echo "Frontend types generated to $(FRONT_TYPES_DIR)"
生成结果
文件结构
每个 .proto 文件对应生成一个 TypeScript 文件:
frontend-types/
├── user/
│ └── v1/
│ ├── common.ts # 从 common.proto 生成(公共类型)
│ ├── user.ts # 从 user.proto 生成(用户服务)
│ └── role.ts # 从 role.proto 生成(角色服务)
生成文件示例
common.ts(公共类型):
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.10.1
// protoc v6.33.0
// source: user/v1/common.proto
export interface ResponseHeader {
code: number;
reason: string;
msg: string;
}
export interface PaginationRequest {
pageNum: number;
pageSize: number;
}
export interface PaginationReply {
pageNum: number;
pageSize: number;
total: number;
}
user.ts(用户服务,使用 onlyTypes=true 生成):
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.10.1
// protoc v6.33.0
// source: user/v1/user.proto
/* eslint-disable */
import type { PaginationRequest, PaginationReply } from "./common";
export interface GetUserRequest {
uid: number;
}
export interface GetUserReply {
code: number;
msg: string;
data: UserInfo | undefined;
}
export interface ListUserRequest {
pageNum: number;
pageSize: number;
keyword?: string | undefined;
}
export interface ListUserReply {
code: number;
msg: string;
data: UserInfo[];
pagination: PaginationReply | undefined;
}
export interface UserInfo {
uid: number;
username: string;
email: string;
phone: string;
createdAt: string;
}
export interface User {
GetUser(request: GetUserRequest): Promise<GetUserReply>;
ListUser(request: ListUserRequest): Promise<ListUserReply>;
CreateUser(request: CreateUserRequest): Promise<CreateUserReply>;
UpdateUser(request: UpdateUserRequest): Promise<UpdateUserReply>;
DeleteUser(request: DeleteUserRequest): Promise<DeleteUserReply>;
}
前端集成
复制生成的代码
将后端项目的 frontend-types/ 目录复制到前端项目的 src/api/user/v1/ 目录。
使用生成的类型
import type { GetUserRequest, UserInfo } from '@/api/user/v1/user';
import type { PaginationRequest } from '@/api/user/v1/common';
// 使用生成的类型
const request: GetUserRequest = {
uid: 123,
};
// 使用类型定义
const userInfo: UserInfo = {
uid: 123,
username: 'john',
email: 'john@example.com',
phone: '13800138000',
createdAt: '2024-01-01T00:00:00Z',
};
自行封装 API 调用
由于 protoc-gen-ts_proto 只生成类型定义,需要自行封装 API 调用:
import axios from 'axios';
import type { GetUserRequest, GetUserReply, ListUserRequest, ListUserReply } from '@/api/user/v1/user';
// 获取用户信息
export async function getUser(request: GetUserRequest): Promise<GetUserReply> {
const response = await axios.get<GetUserReply>(
`/api/v1/user/${request.uid}`
);
return response.data;
}
// 获取用户列表
export async function listUser(request: ListUserRequest): Promise<ListUserReply> {
const response = await axios.get<ListUserReply>('/api/v1/user', {
params: {
pageNum: request.pageNum,
pageSize: request.pageSize,
keyword: request.keyword,
},
});
return response.data;
}