偷偷告诉
文末有完整版PPT分享~
淘系技术部前端技术专家 张挺
淘宝在 2017 年之前就开始探索 TypeScript 的落地方式,随着时间的推移已经将新的模块和框架全部迁移到 TypeScript 体系,在 2019 年, TypeScript 应用已经遍地开花,提前完成了非常不错的布局。
GMTC 大会上,淘系技术部前端技术专家 张挺,分享了淘宝的 Midway 部分想法和实践,本次分享主要介绍淘宝最近开源的 Midway 框架在新的场景、新的体系下如何和现有的 Egg 体系保持良好的兼容性,同时又能在 TypeScript 的使用中有着独特的体验,通过针对不同场景的情况,我们引入相同的解决方案,为未来打下了夯实的基础。
▐ 跨平台方案
整个分享的内容基调是基于当前的 Node.js 开发背景来的。
BFF 应用
对于阿里集团来说,大部分的应用都是 BFF 应用,这些应用表现为长尾应用,维护人不断的迭代流失,可能最后也找不到人维护,而这 70% 的应用被逐步放弃,但是依旧有一些同学还在不断的使用,对于一个 BU 来说,是很不利的。而 Serverless 的出现,可能是给这些应用一个机会,一个能摆脱维护,摆脱人员投入的机会,但是具体如何,还要看今年的发展,毕竟 Serverless 和传统的应用开发有很大的不同。
全栈应用
剩下的就是全栈应用了,去除那些不重要的 BFF 应用,我们还对其做了核心和非核心的划分,在这些应用中,不乏有承载千万流量的大应用。而这些应用都由前端同学来维护,整个研发,测试,发布的流程都必须非常谨慎。
TypeScript应用
在集团应用中,TS 的使用没有想象的那么多,据我们采集的数据,也就只占 5% 左右,基本都是 midway(TS 版本,内部还有 JS版本),而今年,我们希望新应用全量使用 TS。
在这种场景下,对于业务同学来说,也有很多苦恼,比如业务复杂,接口没有定义,以前使用 schema,但是没有很大的推广开来,这些都需要自己去拿时间来填,反而并不友好。在集团内发布 RPC 服务,也需要写 jsdoc,用于匹配 java 的类型,在 js 场景场景下,这些都是不得已的选择。
这个时候引入 TypeScript,来帮助我们解决这些质量,习惯,方法上的问题,就拿 midway 团队来说,自从使用了 TypeScript,质量提升的非常明显,平常需要测试很久的代码,几乎不会出现低级的问题,反而暴露出的大多都是逻辑问题。
面向接口编程,也成为了大家的习惯,每次多人协作,也只需要先定义 interface,再根据 interface 的约定去各自实现,效率也非常高。同时,我们将 RPC 生成的工具替换成了 TypeScript 解析,将 Java 类型和 TS 类型做了一些映射,也避免了再使用 JsDoc 描述的问题。
讲了这么多 TS 的使用,下面来解决具体的问题。
Midway 是淘宝去年开源,面向未来的全栈框架,所谓面向未来,我们希望在未来能够不断的迭代,而主代码不需要做过多的变更,同时在技术迭代的浪潮中,我们的框架也能不断的适用于新的场景。
Egg.js 解决了 Web 开发的场景,在不断的演进中,淘宝产生了全栈场景,Egg.js 已经无法满足目前的需求,一方面集团内需要编写上层框架,另一方面我们希望有原生的 TS 体验。
在现有的 Controller - Service 架构中,除了 Controller 是明确意义的,Service 承载了非常多的职能,把 API,服务,逻辑其实都放在了一起,如果想单独拆分目录,也不是特别方便。
在 Egg.js 的更新之后,加入 ts-helper 填补了 TS 方面的空缺,不过目前由于目录约定,编译前后的文件是在一起的,略微有一些不舒服。
在体验方面,不同之处,例如:Egg.js 是支持过程式写法的,在类的写法中,由于请求链路的关系,比如手动继承一个基类,这在业务中,如果想要自行再继承就无法满足。
同时,核心的 Loader 机制把属性方法都挂载到了 app 上,显得不是特别优雅。这促使我们做了第一代的设计。
▐ 第一代的设计
淘宝使用 IoC 非常早,我们有许多熟悉 Java 的同学非常喜欢 spring,一开始沿用了 XML 的写法来配置,但是转到前端来写,XML 就变成了桎梏,负累重重。
在参考了轻量的 inversify 之后,我们觉得提供两个简单的装饰器是一个最好的办法。
@injectable()
提供了暴露类可以被 IoC 注入的能力。而 @inject()
提供了相应的注入属性的能力。同时 inversify 有个 bindding 的包,提供了自动绑定的能力,我们也沿用了里面的装饰器,这才有了自研的 injection 包,里面包含了 @provide
和 @inject
两个装饰器方法。
经过了 IoC 之后,我们把所有的对象统一放在了 IoC 容器中管理,不再需要关心实例的来源,也不在需要自行去创建实例(new)。
在使用了 IoC 之后,我们发现所有的写法都可以变成传统的 class 形式,封装继承多态三大特性都可以完美的使用,不再受到其他限制。
抛开装饰器,代码就是原生的 class,不管是测试也好,开发也好,都方便的使用 TS 的类型描述,最直观,也最简单。
在集团内,大约有 10 来个中间件,为了让使用者有 TS 定义,将原有的代码进行了增强,这都是一次性的工作量,可以造福后人。
▐ 第二代设计
和 Egg.js 解耦
之前我们解决了 Service 的问题, 通过 IoC,我们可以随便创建目录,调用 API,以及测试。但是在 Web 层,和 egg 耦合的地方还是沿用了 egg 的写法,虽然有变通的办法,但是需要在体验上更进一步。
Midway 基于 Egg.js 进行迭代开发,要实现 egg 的插件化能力,是直接在 package.json
中依赖了 egg 包,同时由于 IoC 的产出,又希望能够让各种开发体验保持一致,全部使用 class 的写法,这也促使我们和 egg 进行了解耦,使用装饰器完成各种 web 层的能力。
通过 @config 能力,和 app.config 解耦
通过 @plugin,和 app.xxx 插件解耦
通过 @inject() ctx 和请求链路解耦
此外还有 @logger 等装饰器,提供额外的能力。
和目录结构解耦
在做完 IoC 自扫描能力之后,已经完全不需要考虑目录结构了,如果还需要 egg 的插件能力,目录还需要保留,如果不需要插件,就可以自由定义目录,扫描能力会完成一切。
通过自扫描能力,在极端情况下,可以将原有应用按功能划分,也可以随意拆分成子模块,甚至是 npm 包,而每一个模块都可以随时独立开发部署,也可以随时聚合成一个大应用。
和自己解耦
在做完这些之后,我们觉得未来可能要面向不同的场景去了,这个时候如果一味的只考虑一个框架入口,可能会被受限制,虽然我们将 Midway 的代码分开抽象,但是核心还是在一起的,各个装饰器的实现和定义都是在同一个包,这样扩展插件或者新增装饰器都需要改动到 Midway 本身。所以需要一次重构把和 Midway 依赖的东西都解耦掉。
首先将装饰器的定义都单独分离出来,形成一个新包,这个包中有所有的装饰器,以及他们最基本的函数(装饰器定义)。
抽离完定义之后,我们就可以将实现部分单独成为新的包,这个时候才有 midway-web 等包的产生。
▐ 面向未来的设计
所谓面向未来,就要为未来考虑和设计,而几年 Serverless 的大热,也为 Node.js 开发者提供了新的机会,而作为集团唯一的 Node.js 架构团队,自然当仁不让的投入到了研究的浪潮中。
在考虑跨场景之时,正逢将装饰器定义与实现分离的时候,我们顺便也将通用的能力沉淀了下来,这样未来不同的场景都可以共享这些能力。
我们沉淀出了 midway-core 这个包,包含以下几种能力。
第一种是自扫描注入 IoC 的能力,injection 提供通用绑定能力。
第二种是适配 midway 的请求作用域能力,不同的场景必然有请求,这个能力也属于通用的能力之一。
第三是统一的装饰器扩展能力,比如 @config 的扩展。
在 Midway-core 之外,我们也实现了一个 Decorator Manager 用于装饰器的编码和管理。
以新创建一个装饰器为例,比如 @autoload,某些类加了这个装饰器,希望能在应用启动时自动被实例化,执行 init 方法。在新的分离体系下,只需要定义一个装饰器(标准函数),将这个装饰器的 key 通过 saveModule
方法进行保存。
在模块、插件等任意你希望实现这个装饰器能力的地方,通过 listModule
就可以把用到这个装饰器的类通通拿出来,接下去你只要循环,然后实例化这个类,执行方法就行了。通过这样的机制,我们把所有的装饰器都进行了改造,实现了整个模式。
在这次改造之后,我们觉得多场景的方案基本可行,在 koa/express 上做了试点,通过编码之后,基本上在 200 行左右就完成整个功能,同时达到整个代码使用相同的装饰器,并且逻辑基本不变。
在这之后,又逐步实现了其他的一些场景,同时对这些场景完成了一些工具链,配套等等。这些工具链有些是复用的,例如:Midway-bin,有些又是特定场景使用。
Serverless 场景,也是我们的整个场景之一。Serverless 整体分为很多部分,这里我们只将将函数代码部分。
FaaS 是 Serverless 的实现之一,我们本来觉得在 FaaS 体系中代码比较简单,无需框架的帮助,但是在实际调研中,我们发现用户的代码还是有不少,同时文件和复杂度还是有一些,所以也同样需要框架的帮助。
但是这个框架必须是非常精简,非常小,只需要完成基本的功能即可。由于我们多场景的设计,代码的整体结构也和原来的基本保持一致,最终我们实现的 midway-faas,大概在 120 行代码,保留最基本的 IoC 能力。
可以看到代码写法基本一致,只有装饰器的区别。
可以看到除了包名不同,入口的装饰器略有差异外,整个写法上依旧保持基本的 class 形态。
除了写法一致之外,对于 FaaS 本身,我们还有一些诉求。
1、代码一致,能力一致,这个通过 IoC,基本能够做到了
2、我们希望一套代码,能够部署到多个云环境
对于不同的平台来说,调用方式(回调,async),函数参数(event),以及描述文件(spec)都是不同的,要把他们统一其实比较困难,但是经过内部验证,我们依旧可以在一些地方进行统一。
我们针对不同平台的入口文件进行包裹,一般来说,入口文件是通过描述文件 (spec) 的 handler
字段指定的,例如: index.handler
,指的就是 index.js
文件的 handler
方法。但是由于 TypeScript 目录结构的关系,所有的文件都在 src/dist
目录下,正好在根目录空缺出了这个文件,使得我们可以进行一些黑科技操作。
举个例子,针对阿里云 FC,我们可以做一些 callback 转 async
的包裹操作,使得用户端调用的代码格式保持统一,这部分代码目前还未开源,这部分方案我们希望尽快,比如在下半年能够提供给社区。通过这样的黑科技操作,我们能够在多个平台之间使得用户代码保持一致性。
当然 Midway-faas 我们还在演进中,除了保持小体积,基本完整的功能外也想提供更多的能力。我们通过不断的改进,从解决实际问题出发,和各个模块解耦,实现不同场景相同的代码编写方式,这些都是不断的思考,不断的沉淀,未来可能还会有很多挑战和变化,我们希望也能够一如既往的迭代下去。
▐ 总结
我们通过 IOC,解决了困扰我们多年的全栈开发问题。
我们通过装饰器,解决了和某个框架依赖过深的问题。
我们通过多场景,拓宽了 Node.js 的开发职能,也创造了前端的新场景。
本文是淘宝从 Midway5 到 Midway6 开发的实践积累,过程中的点点滴滴都在字里行间流出,不知道大家有没有Get到其中的每次变化的原因,从中能够理解为什么要做这些事情,做了之后能够带来什么影响,最后希望本文能够帮助各位思考和改进。有更多疑问,欢迎在文章留言区回复交流。
One More Thing
关注「 淘宝技术」微信公众号,回复 PPT 可领取本文完整版 PPT 资料。
推荐阅读
在看点这里