在上周的 GMTC 全球大前端大会上,淘宝前端技术专家陈仲寅在“Node 实战”主题专场中发表了《TypeScript 多场景开发和实践》的演讲,演讲内容整理如下。
前两天参加了 GMTC 大会,作为讲师分享了淘宝的 midway 的部分想法和实践,也顺带听了 Node.js 全场的分享,同时也听了一些行业内的其他实践,还是有不少收获。也有些场因为时间冲突比较遗憾,只能后期再看视频。
今年很明显的是,围绕着 Serverless 去开展,Node.js 场的我们讲师,其实都是在 Sereverless 方向有不少实践,腾讯的 Starkwang 和网易的金炳,都是有落地的,小程序自然不用说,目前国内基本上都是围绕着它的生态,我没想到的是,网易的起步也挺快。
跨平台方案
下面还是聊聊主题,我分享的内容,是基于 TypeScript 的多场景开发方案。
整个分享的内容基调是基于当前的 Node.js 开发背景来的,阿里的应用分为几种。
对于阿里集团来说,大部分的应用都是 BFF 应用,这些应用表现为长尾应用,维护的人不断的迭代流失,可能最后也找不到人维护,而这 70% 的应用逐步逐步的就被放弃,但是又有一些同学还在不断的使用,对于一个 BU 来说,是很不利的。而 Serverless 的出现,可能是给这些应用一个机会,一个能摆脱维护,摆脱人员投入的机会,但是具体如何,还是得看今年的发展,毕竟 Serverless 和传统的应用开发有很大的不同。
剩下的就是全栈应用了,去除那些不重要的 BFF 应用,我们还对其做了核心和非核心的划分,在这些应用中,不乏有承载千万流量的大应用。而这些应用都由前端同学来维护,整个研发,测试,发布的流程都必须非常谨慎。
在集团应用中,TS 的使用没有想象的那么多,据我们采集的数据,也就只占 5% 左右(演讲是变成 0.05 了,这里解释一下),基本都是 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 定义,将原有的代码进行了增强,这都是一次性的工作量,可以造福后人。
第二代设计
之前我们解决了 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 我们还在演进中,除了保持小体积,基本完整的功能外也想提供更多的能力。
上下半场共 90 分钟的分享到此就结束了,我们通过不断的改进,从解决实际问题出发,和各个模块解耦,实现不同场景相同的代码编写方式,这些都是不断的思考,不断的沉淀,未来可能还会有很多挑战和变化,我们希望也能够一如既往的迭代下去,感谢大家倾听(阅读)我的分享。
最后
整理一次还蛮累的,内容多,算是对整个演讲逻辑的又一次回顾。有些用于过场的部分就酌情省略了。
整个分享其实正是淘宝 Midway5 到 Midway6 开发的实践积累,过程中的点点滴滴都在字里行间流出,我们希望听众或者读者能够感受到其中的每次变化的原因,从中能够理解为什么要做这些事情,做了之后能够带来什么影响,从而更好的帮助各位思考和改进。
嘉宾介绍:
陈仲寅,淘宝前端技术专家,长期耕耘于 Node.js 技术栈,为淘宝和阿里其他 BU 提供框架和中间件解决方案,负责淘宝整体的 Node.js 体系基础建设,解决全栈开发的各种维护和稳定性问题,也同时负责 MidwayJs 系列内部和社区开源产品,包括 Midway、Sandbox、Pandora、Injection 等开源产品的开发、维护等工作。
评论 2 条评论