开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:API 管理
开门见山,Kratos内置的 RPC 是gRPC,而 gRPC 是基于Protobuf作为 接口规范的描述语言(IDL,Interface Description Language)。
与此同时我们还可以通过grpc-gateway对 RESTfull 进行支持。这样,Kratos 就同时支持 gRPC 和 REST。
也就是说,我们只需要编写一套 Protobuf 代码,就能够同时支持 gRPC 协议 和 RESTfull 协议。
Protobuf 支持很多编程语言,比如:C++、Java、JavaScript、Python、Go、Ruby、Objective-C、C#……这也就意味着,它很适合多语言异构化架构,这样的场景在现实应用当中是很稀松平常的,这使得 Protobuf 具有很强的实用性。
Protobuf 具有序列化后数据量更小、序列化/反序列化速度更快、更简单的特性;而 JSON 则相反,序列化后数据量较大,序列化和反序列化速度不优的特性,但是前端对 JSON 是原生支持,对前端极其友好。那么,我们可以在服务之间使用 gRPC 进行通讯,服务与前端之间可以通过 RESTfull 进行通讯。
Protobuf 和 gRPC 已经发展了许多年,极其稳定,生态链丰富。它具有强大的工具链可供使用,只要你想得到的,都能够找得到相对应的工具。没有合适的工具也没有关系,它的工具是使用插件方式来实现可扩展性的,因此我们可以容易的开发出自己的工具插件,Kratos 就为此开发了自己的一系列的工具插件方便开发使用。
综上,我们可知使用 gRPC/protobuf 的好处:
一套 proto,同时支持 gRPC 协议和 RESTfull 协议;
支持多编程语言,适合多语言异构化架构;
gRPC 协议,数据量小、序列化/反序列化速度更快、更简单,适合服务之间通讯;
RESTfull 协议,数据量较大、序列化/反序列化速度较慢、前端原生支持 JSON,适合同前端的通讯。
强大的工具链,使用插件的方式实现强大的可扩展性,可方便的扩展。
了解了基础的知识之后,我们简单的了解一下本文的核心知识点:
使用Protobuf编写 API;
使用Buf管理 proto;
使用Make执行 Buf 命令。
1. 使用 Protobuf 编写 API
syntax = "proto3";
package user.service.v1;
import "gnostic/openapi/v3/annotations.proto";
import "google/protobuf/empty.proto";import "google/protobuf/field_mask.proto";import "google/protobuf/timestamp.proto";
import "google/api/field_behavior.proto";
import "pagination/v1/pagination.proto";
// 用户服务service UserService { // 查询用户列表 rpc ListUser (pagination.PagingRequest) returns (ListUserResponse) {}
// 查询用户详情 rpc GetUser (GetUserRequest) returns (User) {}
// 创建用户 rpc CreateUser (CreateUserRequest) returns (google.protobuf.Empty) {}
// 更新用户 rpc UpdateUser (UpdateUserRequest) returns (google.protobuf.Empty) {}
// 删除用户 rpc DeleteUser (DeleteUserRequest) returns (google.protobuf.Empty) {}}
// 用户权限enum UserAuthority { SYS_ADMIN = 0; // 系统超级用户 SYS_MANAGER = 1; // 系统管理员 CUSTOMER_USER = 2; // 普通用户 GUEST_USER = 3; // 游客
REFRESH_TOKEN = 4; // 刷新令牌}
// 用户性别enum UserGender { SECRET = 0; // 未知 MALE = 1; // 男性 FEMALE = 2; // 女性}
// 用户状态enum UserStatus { OFF = 0; ON = 1;}
// 用户message User { optional uint32 id = 1 [ json_name = "id", (gnostic.openapi.v3.property) = {description: "用户ID"} ]; // 用户ID
optional uint32 role_id = 2 [json_name = "roleId", (gnostic.openapi.v3.property) = {description: "角色ID"}]; // 角色ID optional uint32 work_id = 3 [json_name = "workId", (gnostic.openapi.v3.property) = {description: "工号"}]; // 工号 optional uint32 org_id = 4 [json_name = "orgId", (gnostic.openapi.v3.property) = {description: "部门ID"}]; // 部门ID optional uint32 position_id = 5 [json_name = "positionId", (gnostic.openapi.v3.property) = {description: "岗位ID"}]; // 岗位ID optional uint32 creator_id = 6 [json_name = "creatorId", (gnostic.openapi.v3.property) = {description: "创建者ID"}]; // 创建者ID
optional string user_name = 10 [ json_name = "userName", (gnostic.openapi.v3.property) = {description: "登录名"} ]; // 登录名
optional string nick_name = 11 [ json_name = "nickName", (gnostic.openapi.v3.property) = {description: "昵称"} ]; // 昵称
optional string real_name = 12 [ json_name = "realName", (gnostic.openapi.v3.property) = {description: "真实姓名"} ]; // 真实姓名
optional string avatar = 13 [ json_name = "avatar", (gnostic.openapi.v3.property) = {description: "头像"} ]; // 头像
optional string email = 14 [ json_name = "email", (gnostic.openapi.v3.property) = {description: "邮箱"} ]; // 邮箱
optional string mobile = 15 [ json_name = "mobile", (gnostic.openapi.v3.property) = {description: "手机号"} ]; // 手机号
optional string telephone = 16 [ json_name = "telephone", (gnostic.openapi.v3.property) = {description: "座机号"} ]; // 手机号
optional UserGender gender = 17 [ json_name = "gender", (gnostic.openapi.v3.property) = {description: "性别"} ]; // 性别
optional string address = 18 [ json_name = "address", (gnostic.openapi.v3.property) = {description: "住址"} ]; // 住址
optional string region = 19 [ json_name = "region", (gnostic.openapi.v3.property) = {description: "国家地区"} ]; // 国家地区
optional string description = 20 [ json_name = "description", (gnostic.openapi.v3.property) = {description: "个人描述"} ]; // 个人描述
optional string remark = 21 [ json_name = "remark", (gnostic.openapi.v3.property) = {description: "备注名"} ]; // 备注名
optional int64 last_login_time = 30 [ json_name = "lastLoginTime", (gnostic.openapi.v3.property) = {description: "最后登录时间"} ]; // 最后登录时间
optional string last_login_ip = 31 [ json_name = "lastLoginIp", (gnostic.openapi.v3.property) = {description: "最后登录IP"} ]; // 最后登录IP
optional UserStatus status = 32 [(gnostic.openapi.v3.property) = { description: "用户状态" default: {string: "ON"} }]; // 用户状态
optional UserAuthority authority = 33 [(gnostic.openapi.v3.property) = { description: "权限" default: {string: "CUSTOMER_USER"} }]; // 权限
repeated string roles = 34 [(gnostic.openapi.v3.property) = { description: "角色码" }]; // 角色码
optional google.protobuf.Timestamp create_time = 200 [json_name = "createTime", (gnostic.openapi.v3.property) = {description: "创建时间"}];// 创建时间 optional google.protobuf.Timestamp update_time = 201 [json_name = "updateTime", (gnostic.openapi.v3.property) = {description: "更新时间"}];// 更新时间 optional google.protobuf.Timestamp delete_time = 202 [json_name = "deleteTime", (gnostic.openapi.v3.property) = {description: "删除时间"}];// 删除时间}
// 获取用户列表 - 答复message ListUserResponse { repeated User items = 1; int32 total = 2;}
// 获取用户数据 - 请求message GetUserRequest { uint32 id = 1;}
// 创建用户 - 请求message CreateUserRequest { optional uint32 operator_id = 1 [ (gnostic.openapi.v3.property) = {description: "操作用户ID", read_only: true}, json_name = "operatorId" ]; // 操作用户ID
User data = 2;
optional string password = 3 [ (gnostic.openapi.v3.property) = {description: "用户登录密码", read_only: true}, json_name = "password" ]; // 用户登录密码}
// 更新用户 - 请求message UpdateUserRequest { optional uint32 operator_id = 1 [ json_name = "operatorId", (gnostic.openapi.v3.property) = {description: "操作用户ID", read_only: true} ]; // 操作用户ID
User data = 2 [ json_name = "data", (google.api.field_behavior) = REQUIRED, (gnostic.openapi.v3.property) = {description: "用户的数据"} ]; // 用户的数据
optional string password = 3 [ (gnostic.openapi.v3.property) = {description: "用户登录密码", read_only: true}, json_name = "password" ]; // 用户登录密码
google.protobuf.FieldMask update_mask = 4 [ json_name = "updateMask", (gnostic.openapi.v3.property) = { description: "要更新的字段列表", example: {yaml : "id,realName,userName"} } ]; // 要更新的字段列表
optional bool allow_missing = 5 [ json_name = "allowMissing", (gnostic.openapi.v3.property) = {description: "如果设置为true的时候,资源不存在则会新增(插入),并且在这种情况下`updateMask`字段将会被忽略。"} ]; // 如果设置为true的时候,资源不存在则会新增(插入),并且在这种情况下`updateMask`字段将会被忽略。}
// 删除用户 - 请求message DeleteUserRequest { optional uint32 operator_id = 1 [ (gnostic.openapi.v3.property) = {description: "操作用户ID", read_only: true}, json_name = "operatorId" ]; // 操作用户ID
uint32 id = 2;}
复制代码
以上是用户User的一个完整的 gRPC 的 API,包含了最基本的:用户数据结构,CURD 的 API。
需要特别讲解的是:gnostic.openapi.v3.property,这是用于生成 OpenAPI 的。
下面再给出 RESTFull 的服务定义:
syntax = "proto3";
package admin.service.v1;
import "gnostic/openapi/v3/annotations.proto";import "google/api/annotations.proto";import "google/protobuf/empty.proto";
import "user/service/v1/user.proto";import "pagination/v1/pagination.proto";
// 用户管理服务service UserService { // 获取用户列表 rpc ListUser (pagination.PagingRequest) returns (user.service.v1.ListUserResponse) { option (google.api.http) = { get: "/admin/v1/users" }; }
// 获取用户数据 rpc GetUser (user.service.v1.GetUserRequest) returns (user.service.v1.User) { option (google.api.http) = { get: "/admin/v1/users/{id}" }; }
// 创建用户 rpc CreateUser (user.service.v1.CreateUserRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/admin/v1/users" body: "*" }; }
// 更新用户 rpc UpdateUser (user.service.v1.UpdateUserRequest) returns (google.protobuf.Empty) { option (google.api.http) = { put: "/admin/v1/users/{data.id}" body: "*" }; }
// 删除用户 rpc DeleteUser (user.service.v1.DeleteUserRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/admin/v1/users/{id}" }; }}
复制代码
这里需要说明的是:两个UserService可以是合二为一,也可以有多个。初学者一定会疑惑,我为什么要将之分离开来,为啥不能一个?首先,它可以只有一个,即 RESTfull 和 gRPC 的接口定义都在一个UserService当中,而我分离开来了,那么,我为什么分离开来了呢?分离开来的好处是,职责分明。前者用于内部通讯的 RPC 接口,后者用于对外的 RESTfull 接口。
在实际应用中,我们可能有 admin 和面向于 app 的两套 API,我们就可以分别对之进行定义:
syntax = "proto3";
package front.service.v1;
import "gnostic/openapi/v3/annotations.proto";
import "google/api/field_behavior.proto";import "google/api/annotations.proto";
import "pagination/v1/pagination.proto";
import "user/service/v1/user.proto";
// 用户服务service UserService { // 查询用户列表 rpc ListUser(pagination.PagingRequest) returns (user.service.v1.ListUserResponse) { option (google.api.http) = { get: "/app/v1/users" }; }
// 搜索用户 rpc SearchUser(user.service.v1.SearchUserRequest) returns (user.service.v1.ListUserResponse) { option (google.api.http) = { get: "/app/v1/users:search" }; }
// 查询用户详情 rpc GetUser(user.service.v1.GetUserRequest) returns (user.service.v1.User) { option (google.api.http) = { get: "/app/v1/users/{id}" }; }}
复制代码
2. 使用 Buf 管理 proto
当我们用熟悉了 Protobuf 之后,会遇到一个很头疼的问题:
我们该如何去管理和构建它呢?
在最早的时候,我只能够手动的调用 protoc 命令进行代码生成:
# generate go struct codeprotoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto # generate grpc service codeprotoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto # generate rest service codeprotoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto # generate kratos errors codeprotoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto # generate message validator codeprotoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto
# generate openapi v3 yaml docprotoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto
# generate typescript codeprotoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. ./simple.proto
# generate dart codeprotoc --dart_out=. test.proto
复制代码
怎么样,头大不?要调用这么多命令,生成这么多代码。我后来又想了很多办法,比如:
写进 Shell 脚本;
写进 Makefile;
利用 go 语言的 go:generate 注解。
结果发现,全部都不实用,而且无法进行工程化,在团队内难以实施。直到最后,我发现了buf.build这个专门用于构建 protobuf API 的工具。
Buf 主要提供了两个工具:
Buf Schema Registry(BSR): 其官方网站地址:buf.build,是一个用于管理和共享 Protocol Buffers(protobuf)代码的平台。
Buf CLI: 是一个强大的命令行工具,提供了一系列用于处理、验证和管理 protobuf 代码的功能。
简单的说就是:BSR 是一个 proto 的代码库,类似于 github,上面我用到的 pagination.proto,我就是提交到了 BSR。
更多时候,我们使用最多的是 Buf CLI 这个工具,用它来生成代码,用的最多的就是两个命令:
buf dep update 用于更新三方依赖 proto;
buf generate 用于生成代码。
buf 有两套配置文件:
buf.yaml 主要也就是定义 proto 文件的路径,以及依赖的第三方 proto。
buf.gen.yaml 定义生成规则(这里是 go 代码生成)。
如果我们还需要生成 OpenAPI,则可以再多定义个比如文件名为buf.openapi.gen.yaml的 proto 文件:
# 配置protoc生成规则version: v2
clean: false
managed: enabled: true
disable: - module: buf.build/googleapis/googleapis - module: 'buf.build/envoyproxy/protoc-gen-validate' - module: 'buf.build/kratos/apis' - module: 'buf.build/gnostic/gnostic' - module: 'buf.build/gogo/protobuf' - module: 'buf.build/tx7do/pagination'
override: - file_option: go_package_prefix value: kratos-cms/api/gen/go
inputs: - directory: protos paths: - protos/front/service/v1
plugins: # generate openapi v2 json doc # - local: protoc-gen-openapiv2 # out: ../app/front/service/cmd/server/assets # opt: # - json_names_for_fields=true # - logtostderr=true
# generate openapi v3 yaml doc - local: protoc-gen-openapi out: ../app/front/service/cmd/server/assets opt: - naming=json # 命名约定。使用"proto"则直接从proto文件传递名称。默认为:json - depth=2 # 循环消息的递归深度,默认为:2 - default_response=false # 添加默认响应消息。如果为“true”,则自动为使用google.rpc.Status消息的操作添加默认响应。如果您使用envoy或grpc-gateway进行转码,则非常有用,因为它们使用此类型作为默认错误响应。默认为:true。 - enum_type=string # 枚举类型的序列化的类型。使用"string"则进行基于字符串的序列化。默认为:integer。 - output_mode=merged # 输出文件生成模式。默认情况下,只有一个openapi.yaml文件会生成在输出文件夹。使用“source_relative”则会为每一个'[inputfile].proto'文件单独生成一个“[inputfile].openapi.yaml”文件。默认为:merged。 - fq_schema_naming=false # Schema的命名是否加上包名,为true,则会加上包名,例如:trade.service.v1.ListDictDetailResponse,否则为:ListDictDetailResponse。默认为:false。
复制代码
执行命令:buf generate --template buf.openapi.gen.yaml生成 OpenAPI 文档。
要生成 Typescript 代码就创建一个buf.typescript.gen.yaml的配置文件:
# 配置protoc生成规则version: v2
clean: true
managed: enabled: true
disable: - module: buf.build/googleapis/googleapis - module: 'buf.build/envoyproxy/protoc-gen-validate' - module: 'buf.build/kratos/apis' - module: 'buf.build/gnostic/gnostic' - module: 'buf.build/gogo/protobuf' - module: 'buf.build/tx7do/pagination'
inputs: - directory: protos paths: - protos/admin/service/v1
plugins: # generate typescript code # - remote: buf.build/community/stephenh-ts-proto - local: protoc-gen-ts_proto out: ../../frontend/admin/apps/admin/src/rpc/api opt: - outputSchema=false # 生成模式 (const, no-file-descriptor, true, false) - outputTypeRegistry=false # 生成类型注册表 - outputTypeAnnotations=false # 生成类型注解 (static-only, true, optional, false) - outputServices=default # 生成服务代码 (default, definitions, grpc-js, nice-grpc, false, none) - outputJsonMethods=false # 生成json方法:toJSON、fromJSON - outputEncodeMethods=false # 生成编码方法:encode、decode - outputPartialMethods=false # Message.fromPartial和Message.create方法生成 - outputClientImpl=false # 生成客户端实现 (grpc-web, false) - useExactTypes=true # 使用精确类型 - usePrototypeForDefaults=true # 使用原型作为默认值 - useJsonName=true # 使用json_name定义的字段名 - useNullAsOptional=true # optional字段生成的类型,如果为true生成null,否则生成undefined。 - useDate=false # google.protobuf.Timestamp类型转换为Date类型,如果为true,则生成Date类型,否则保持Timestamp类型。 - useOptionals=none # 将字段声明为可选项,即是否加?号(all, messages, none) - useMapType=true # 使用Map类型 - useReadonlyTypes=false # 使用只读类型readonly - nestJs=false # 使用nestjs - onlyTypes=false # 只生成类型,如果为true,等价于:outputJsonMethods=false,outputEncodeMethods=false,outputClientImpl=false,nestJs=false - fileSuffix=.pb # 文件后缀 - enumsAsLiterals=false # 枚举作为文字 - comments=true # 注释输出 - exportCommonSymbols=false # 导出公共符号,如果为true,则生成protobufPackage,否则不导出。 - esModuleInterop=true - forceLong=string # 强制long类型为string - oneof=unions # oneof生成的类型,如果为unions,则生成联合类型,否则生成交叉类型。 - stringEnums=true # 枚举项生成的类型,如果为true,则生成字符串,否则生成为整型。 - unrecognizedEnum=false # 未识别的枚举项,如果为true,默认会给enum增加一个UNRECOGNIZED枚举项。 - outputIndex=false # 生成index.ts文件 - paths=source_relative # 使用相对路径
复制代码
执行命令:buf generate --template buf.typescript.gen.yaml生成 TypeScript 代码。
需要注意的是,protoc 的插件可以用本地的插件,也可以用远端 BSR 中的 protoc 插件。在上面的实例里面,我都用的是本地的 protoc 插件,需要先行在本地进行安装。在团队协作当中,建议使用远端的 protoc 插件,用远端的 protoc 插件有个好处,那就是可以保证每一个人所使用的插件一致性(本地的有可能版本不一致)。
3. 使用 Make 执行 Buf 命令
本以为用了 Buf 之后,可以高枕无忧了,实际运用中,我们有可能会有多个 buf 的生成配置文件,那么我们就需要多个生成命令。命令写进 shell 脚本也不是不可以,但是,会有平台差异(Windows 没有 shell 脚本,只有 bat)。那么,我们可以使用 Makefile,make 是可以在任意平台上运行的,包括 Windows。
# generate protobuf api go codegoapi: @cd api && \ buf generate
# generate OpenAPI v3 docs.openapi: @cd api && \ buf generate --template buf.admin.openapi.gen.yaml && \ buf generate --template buf.front.openapi.gen.yaml
# generate typescript.ts: @cd api && \ buf generate --template buf.admin.typescript.gen.yaml && \ buf generate --template buf.front.typescript.gen.yaml
# generate protobuf api dart code.dart: @cd api && \ buf generate --template buf.front.dart.gen.yaml
复制代码
buf generate命令默认读取的是当前目录下文件名为buf.gen.yaml的配置文件。
现在我们就可以在项目的根目录下面运行 make 命令:
生成 go 代码:
生成 OpenAPI 文档:
生成 TypeScript 代码:
生成 Dart 代码:
项目代码
参考资料
评论