protoapi实现的方式
接上文,我们选择了protobuf
作为IDL,而在代码生成的工具上,我是选择了使用go
实现,因为:
- go开发的程序可以方便的编译为windows / linux / osx上的可执行程序,并且几乎没有任何依赖,非常方便分发
- go内置有模板引擎,便于代码生成
- 相关资源丰富
protoapi
的核心仅是protobuf
的编译器protoc的插件,而protoc
本身的运行命令类似:
protoc --api_out=lang=php:[output_path] [api.proto]
那么,这里是需要开发者分别下载:protoc
以及protoapi
的可执行文件,设置好path路径之后才可以运行。
这对于熟悉protoc
的开发者来说并不是问题,但对于不熟悉的童鞋来说,有可能产生困扰。
我们会认为,作为工具的提供者,应该尽可能的让使用者感到方便,故此,我们在实现protoapi
时:
- 选择了将
protoc
也嵌入其中 - 使用者只需要下载
protoapi
,然后运行protoapi init
命令 - 便会自动下载所需的
protoc
,以及设置好path路径。
让使用者可以仅通过protoapi
作为命令入口,做代码生成:
protoapi gen --lang=php --out=[output_path] [api.proto]
嵌套调用
运行protoapi gen
命令的时候,protoapi
实际上会:
- 获得
protoapi
自身所在的路径 - 生成
protoc
的命令参数,内部另开子进程调用protoc
-
protoc
则又会再开子进程调用新的protoapi
- 第二个
protoapi
进程,会检测自身的环境,区分自己是被protoc
作为插件调用,又或者是被独立运行
- 第二个
嵌套调用类似:
protoapi
进程,会检测自身的环境的代码类似:
// main.go
// when no any parameter and not reading from char device
// treat it as being called by protoc
if len(args) == 1 && err == nil && (stat.Mode()&os.ModeCharDevice) == 0 {
// 被 protoc 作为插件调用
// 执行代码生成的逻辑
} else {
// 从命令行被调用,根据命令参数执行逻辑
cmd.Execute()
}
语言模板
go本身内置有模板支持,text/template
是在go 1.9的时候,甚至对生成的代码模板空行控制做了支持,处女座表示很爽。
这样我们可以让通过模板控制,输出空格、空行的排版都严谨的代码。
在开发调试的阶段,我们会希望使用独立的文件来编辑模板,然后实时生效;但在分发、使用的阶段,我们又会希望将这些模板文件跟主进程打包
在一起,而不是分发多个文件。
在go里面,我们可以使用esc,自动的将静态资源打包至进程内,成为代码的一部分;但同时又可以通过环境变量,切换从硬盘读取源文件的模式。
debug_tpl
属于protoapi
一个undocumented的API。
命令行
cobra提供了一个命令行框架
,可以方便的添加命令、设置参数、输出帮助等。
测试
开发过程中,经常可能会需要对模板、代码生成逻辑做修改、重构,需要有足够的测试用例支持,确保代码变动后,生成的代码不变。
在这里,自动测试代码实现起来也容易,只需要在程序开发至一定阶段后,拟定一个或者多个完成的proto文件
,然后跑一遍代码生成,将生成的文件保存下来作为范本,以后有代码改动,就比较一下重新生成的代码跟范本是否有差异即可。
可以做成go test,也可以简单的搭配jenkins + 脚本做比较。
最后
protoc
本身也有各种语言的代码生成,但它是针对直接使用protobuf
做对象序列化的场景,而我们这里,首先是要兼容旧的web项目,使用json
作为序列化,直接生成新的代码会便利一些。
但对于合适的场景,protoapi
也会尽量采用protoc
默认生成的代码,然后对其进行扩展。