前言
| | 我是天猪,热爱开源,Egg 核心开发者,出没于:知乎、GitHub
目前负责:蚂蚁体验技术部 - 基础技术团队 (广州、杭州)
相关开源:EggJS、CNPM、Easy-Monitor | | —- | —- |
『Node.js 在前端领域是一个不可或缺的基础设施,或许未来前端的变革使得一切工程问题从根本上得到解决,但不管怎样,我只是希望当下能认真记录自己以及同行者们在这个领域的所见所想,与正在经历前端工业化演进并被此过程困扰的同学交流心得。』 — by 天猪
阿里 Node.js 编年史
- 2009 年 05 月 01 日,Ryan Dah 在与 Tim Becker 的邮件讨论中正式命名为 node,同月 27 日发布第一个版本,同年 11 月在 JSConf EU 上正式对外发布。
- 2010 年,具体时间已经无法考证,在招聘应用
淘job
中写下了阿里 Node.js 的第一行代码。 - 2012 年 11 月,苏千为了解决 npm 国内速度缓慢问题,创建了 cnpm 国内镜像服务。
- 2013 年 11 月,蚂蚁的 Node.js 基础框架 Chair 诞生,寓意是:『椅子比沙发轻』,因为蚂蚁的 Java 框架叫 SOFA。此时 Node.js 还处于 0.10 版本。
- 2014 年 06 月,淘宝的 Node.js 基础框架 Midway 诞生,同年 JSConf CN 大会(杭 JS)上 赫门 分享了 《淘宝前后端分离实践》。
- 2015 年 10 月,在 苏千 的召集下,各 BU 的接口人齐聚一堂,成立了阿里 Node.js 虚拟工作组。同月 朴灵 发布了 AliNode 1.0 版本,性能分析再无后顾之忧。
- 2015 年 11 月,工作组在黄龙国际闭关一周,产出了 Web 框架规约,发布了 @ali/egg 1.0 版本,蚂蚁、天猫、UC、ICBU 等 BU 的上层框架陆续产出。
- 2016 年 01 月,天时地利人和,Egg 成为了阿里的 Node.js 核心基础框架,自此阿里 Node.js 生态有了三驾马车(框架、包管理、性能分析)的护航。
- 2016 年 09 月,天猪 在 JSConf CN 大会(宁 JS)上宣布开源,Egg 的寓意是:希望各团队架构师孕育出适合自己业务场景的上层框架。
- 2017 年 01 月,Egg 团队再次闭关,并在 CNode 在线直播写文档,完成了第一版的官网文档,语雀的 子溯 设计了 Logo 和官网。
- 2017 年 03 月 21 日发布 Egg 1.0,同年 12 月 03 日在 Node.js 8.x 进入 LTS 后同步发布了 Egg 2.0 。
- 2021 年 Q4,我们再次重启了 Egg 3.0 的开发,目前处于 RFC 讨论阶段,欢迎加入。预计 2022 年 08 月正式发布。
PS:一些社区生态组件的诞生时间:
年份 | 社区 | 阿里 |
---|---|---|
2009 年 | node 诞生(2009.05.01) | |
2010 年 | Express (2010.05.22) | 淘 job,第一行 Node 代码,具体时间不可考 |
2011 年 | Grunt (2011.09.21) | |
2012 年 | Webpack(2012.03.10) Babel(2012.09.24) |
tnpm(2012.11.08) |
2013 年 | Docker(2013.03.13) Koa(2013.08.17) |
Chair(2013.11.27) cnpm(2013.12.06) |
2014 年 | Kubernetes(2014.6.7) | Midway(2014.06.03) |
2015 年 | AliNode (2015.10.14) EggJS (2015.11.13) |
EggJS 的定位
从上面的历史进程可以发现,Egg 出现的时机点正好在前端完成工程化,开始探索前后端分层一年多的时候,我们聚到了一起,思考 Web 框架的定位和如何发展。
我们应该是做一个大而全的框架(类似 Sails/RoR),还是小而美的框架(Express/Koa)就够了?
大家可以回想下,在团队里面做 Node.js 的基建时的心路历程:
- 阶段1:使用社区框架
- 一般不会直接用 Node.js 的 Http Server,因为它太底层了,API 太裸了。
- 会以 Express 或 Koa 这些小而美的框架为起点,它们在 Node.js 之上提供了 API 语法糖 + 中间件模型。
- 同时也会去挑选一些对应的第三方中间件 npm 包。
- 阶段2:封装脚手架
- 随着业务的发展,我们需要维护的应用数量持续增加, 应用数 10+ 个。
- 为了降低初始化成本,往往我们会把成型的目录结构变为约定,封装为一个脚手架包,作为团队新应用的起点。
- 阶段3:封装上层框架
- 随着业务持续迭代,50+ 个应用,我们会发现,脚手架生成的约定,不具备可持续维护性。
- 譬如我们想统一加一个中间件,需要手动逐个对存量应用进行处理。
- 此时会演进为把 框架 + 常用的中间件 + 公共逻辑 封装为一个上层框架。
- 阶段4:???
- 随着公司的壮大,我们和其他前端团队会师了,发现有数百个应用,但规范风格却五花八门,各不相同。
- 成立一个虚拟小组,开始思考这个问题,是封装为一个大而全的框架?每个团队的场景都一样么?初始化配置和中间件都一样么?
- 未来量变达到质变,1000+ 个应用的时候,我们还可以支撑业务的可持续发展么?如何从工程化走向工业化?
- 未来成为类似阿里这种体量的巨型公司,各个 BU 都有自己不同的业务场景(电商、金融、资讯、商家等等),怎么办?
- 作为技术人,未来是否想让我们的框架,回馈给社区呢?不同公司之间又如何能避免重复造轮子呢?
🆘 课题:企业级大规模应用场景下,如何既能在尽可能共建和不重复造轮子的情况下,保证灵活的差异化定制,并可持续治理及维护?
✅ 我们的选择是:做框架的框架,EggJS 提供了一套 Loader 规范以及延伸出的插件和框架能力,期望能帮助前端架构师孵化出适合团队的业务场景的上层框架。
QuickStart
先通过一个简单的演示,快速了解下 EggJS 的应用开发侧体感是怎么样的。
如下文档,如有不熟悉 EggJS 的同学,可以阅读下。
EggJS 渐进式开发
Look Inside
通过上面的演示,我们大概了解了 Egg 的几个概念:应用、插件、框架。
接下来,我们来看看 Egg 的内部,大致如下:
如上图,Egg 核心其实基本上这么多年来没啥变化,非常稳定了, 2.0 主要是异步模型从 co 升级为 async。
大家也许会疑惑:原来 EggJS 面向的是团队架构师,那为什么开源的版本,基本上大家都是开箱即用直接在业务中使用?
主要是因为开源设计上的不成熟,对各个模块的拆分不够,egg-core 更像是我们心目中的那个框架的框架,而 egg 是一个开箱即用的集成了常见 Web 开发能力的上层框架。
后面曾和蚂蚁开源的 SOFA 框架(Java)配套的 sofa-node,可惜后面 Node.js 的战略调整,不再投入人力到商业化上,不过当时对应的一些 dubbo 相关的插件还在。
在企业内部,我们都是会针对企业的基础设施以及特定的业务场景,来封装对应的上层框架。
EggJS 是在实践中边摸索边发展的,随着时代的变化,很多当时的最佳实践需要重新审视,在后面几节我们会来逐一探讨下。
Loader
egg-core 是整个 Egg 核心中的核心,习惯称之为 Loader。
所谓 Loader,就是一套约定了如何从文件系统『加载』代码,以及如何实例化并『挂载』到指定对象的规范,并提供了扩展机制、生命周期管理等能力。
Loader 最核心的约定是 加载 + 挂载 规范,我们基于它衍生出了 插件机制 和 上层框架机制,并内置了一套目录规范。在 渐进式开发 一文中,也介绍了如何新增一个自己的规范。
是不是觉得 Egg 只能用于 directory-base 的应用?而对 feature-base 的目录结构不友好?如下,我们在基于上层框架 tegg 来重构 cnpm 的实践(今年会开源出来):
后续规划
- 更容易定制自己的目录规范。
- 精细化的生命周期,如插件的 mount、unmount、异步加载配置等。
- 便于跟踪和分析的数据输出,包括错误日志、合并结果、耗时等等。
- 支持基于 manifest 的加载提速,支持 pkg 等私有化打包部署方式。
- 挂载方式优化,从之前的合并方式,优化为挂载到各自命名空间再 Proxy 方式。
- 更友好的 TS 支持。
- 等等。。。
Context 模型
Egg 设计之初是从 Web 应用服务的角度出发,基于 Koa 实现的洋葱中间件模型之上。
然而,Node.js 技术栈发展时至今日,在服务端的应用领域里其实已经不仅仅是承载 Web 开发这样的职能了。
那么在之前的设计思路下,Egg 想要应用到其余的 C/S 交互协议里就会比较麻烦,譬如:
- egg-socket.io 就是一个强行的模型适配,导致实现起来很是别扭,也很难维护。
- 内部常见的 RPC 模型,一般也需要有中间件机制。
我们会重新设计一个底层协议无关的实现,提供 BaseContext、BaseTrigger 以及对应的洋葱模型,上层业务再通过插件方式扩展提供对应的 HttpContext、SocketContext。
插件
在之前 QuickStart 的介绍中,我们知道,插件是 Egg 的核心能力,它不仅是能力的复用,也是差异化定制的基础。
在我们设计之初,插件的大部分场景是用来对后端基础设施服务的 SDK 封装,提供对应的 API 能力,如服务发现、远程配置、RPC 调用等等。对后端整套中间件能力的接入与支持,是 Node.js 能在阿里快速成为第二大语言的重要原因之一。
但在实际应用中,由于基础设施能力封装这块基本上由框架开发者集成了,应用开发者更多的是会把自己团队内部的一些偏业务逻辑的能力封装出来。
在这么多年的实践中,我们发现一些问题:
- 我们用的某个 API 是哪个插件提供的经常会需要找半天。
- 插件提供的一些 API 是不希望开放给应用使用,仅仅面向其他插件。
- 插件里面的生命周期不够精细。
- 应用开发者会把插件作为业务逻辑的能力复用,但很难治理,也不支持动态下发和挂载。
在 Egg 3.0 中,我们会探索:
- 业务逻辑复用的相关支持,如元数据、上下游依赖声明等。
- 插件 API 可见性,区分面向开发者、插件内部、其他插件的 API。
- 更精细化的生命周期钩子,如插件的 mount 和 umount 等。
TypeScript
这么多年来,对 Egg 的另一大吐槽,就是对 TS 的支持,以及经常被问 Egg 啥时要重构为 TS?
首先,就像很多年前在知乎上的分享的观点:『应用开发者用 TS 开发 Egg 应用』 和 『Egg 本身用 TS 开发』是两个问题。
如上图,我们看到,除了可以用 TS 开发 Egg 应用外,即使你用 JS 开发的 Egg 应用也能享受到丝滑的智能提示。
这背后的工作:
- 框架和插件本身以及很多基础模块其实都是 JS 的,只要有完善的 d.ts 即可。
- 对于应用本身,由于是动态加载的,天生和 TS 的静态分析有冲突,不过我们仔细分析后会发现,每个目录下的文件本身,它的类型信息其实是完整的,我们之前唯一的卡点是挂载后的对象和文件本身是割裂的,所以我们通过 TypeScript 的 Declaration Merging 特性,我们自动生成对应的映射关系,即可完美解决了:
// typings/app/service/index.d.ts
import News from '../../../app/service/News';
declare module 'egg' {
interface IService {
news: News;
}
}
后续规划
我们依旧倾向于拥抱标准,然而装饰器方案目前几次变动都没法落地,现在最新版才 Stage2,JS 的类型也没有相关规划,TS 已经成为了事实上的标准。
同时由于在企业里面的路径依赖,我们过往更看重单元测试等能力,并且看到了 TS 带来的额外成本,所以在内部虽然有基于 Egg 的上层 TS 框架,但一直没有开源出来。
在去年的一次内部共建时,苏千给我们一大警醒,以及动力。
因此,我们的后续规划是:
- Egg 3.0 本来就要大重构,此时用 TS 和 JS 对我们来说,都没啥成本区别。
- 基于 Egg 之上封装的上层框架 TEgg 将一并开源。
- 框架和插件对 d.ts 的支持会作为 first-citizens。
进程模型
多进程模式,对于 EggJS 来说,曾经是一个亮点,但随着时代的发展,需要重新去审视。
为什么需要进程模型?
我们知道 JavaScript 代码是运行在单线程上,那么如果用 Node.js 来做 Web Server,就无法享受到多核运算的好处。
Node.js 官方提供了 child_process 和 cluster 模块,然而太底层了,很多错误处理需要开发者自己考虑,很多新手就经常会忽视,从而踩坑造成孤儿进程泄露。
社区有 PM2 这个工具,但它的 License 是 LGPL,无法在企业中使用,同时它的很多能力对我们说是不必要的。
所以 EggJS 自己实现了 egg-cluster,提供了完善的多进程管理能力,并多出了一个 Agent 的概念(因为有一些公共事务,是不需要每个进程都去重复执行的,譬如拉取远程配置,定时清除日志等)。
进程模型的未来
随着 Docker(2013 年)和 Kubernetes(2014 年)的出现,开始吹响云原生的号角,部署单元的粒度进一步细化为 pod,最佳实践模式下已经变成 1c2g 的 pod + 应用单进程部署。
在这块我们并没有很快的跟进,因为:
- 云原生的进展比想象中的慢,即使是在大厂,更别提社区广泛的中小企业。
- 如果 servicemesh 的具体实现,没有支持多租户的话,对微服务基础设施的请求压力其实是倍增的。(原来 n 个集群,每个 pod 只需一个 agent 和后端建连,单进程之后,放大为 n * m)
- 如下图可以看到,实际上相当于 Agent 的一些事,被 k8s 本身 + 放在 sidecar 里面的 servicemesh 给承接了,这更多是一种架构上的收益。
PS:基于 WSAM 的轻容器也是目前正在探索的一个方向。
但不得不承认,内置的多进程带来了一些问题,譬如本地启动速度慢, egg-mock 实现复杂,不能用到 nodemon 等社区组件,socket 场景用起来比较别扭,ipc 通讯被滥用等等。
因此,在 Egg 3 里面,我们将默认单进程模式,把原来的 cluster 模型变为一个可选的独立插件,让架构师能灵活定制。
社区生态建设
对于一个开源项目来说,社区生态建设和运营是一个很重要的事,这点上我们只能拿 50 分。
做的好的点:
Egg 的文档的口碑,在国内还是不错的。其实刚开源后很长一段时间都没把文档放出来,直到几个月后我们在 CNode 直播写文档,基本形成了现在的官网。
在前三年,Issue 的活跃度很高,很多问题基本上都是秒答的,记得当时阿里云的同事还给我做了个统计:
- 然后也持续在 知乎专栏 做 Node.js 方面的科普,近百篇经验总结分析,帮助了不少开发者,收获了数万个点赞。
这段话是之前在知乎看到的,让我们印象很深,我们对社区回馈的价值得到了认可:
Java 之所以在企业级应用领域得到广泛应用,正是由于有各种优秀的企业级框架及周边工具。 而国内 Node 社区此前在这方面的积累几乎为零,这正是阻碍 Node 从写个博客写个 Web API 到真正大型系统的最大障碍。 EggJS 直接把阿里在企业级应用的实践提炼并贡献给社区,这是对 Node 社区非常大的贡献。
不足的点:
- 整个社区的运营其实是没有太多规划的,没有运营好。
上面提到,Egg 在内部是没有实体组织的,是一个比较松散的组织,没有对应的 KPI,我们也不像 SOFA、OB 这样的团队,有专门的开发者运营人员,所以很多时候我们更多是用爱发电。再加上团队里面写代码厉害的人很多,但能写好一篇分享的人,不多。
- 来自社区的回馈很少,没有形成正循环。
曾经一度很迷惑的点,日常交流的时候,经常会有人打个招呼,说他们公司在用 Egg。
但长达数年的时间内,我在社区很少看到有 Egg 的经验分享,对我个人的正反馈不强。
包括最近蚂蚁开源办公室想了解下,国内有哪些企业正在使用,我也只能说个大概。
- 没有走向国际化。
一开始的时候,我们是希望走出去的,回馈给社区的,毕竟我们几个也都是 Koa 等开源项目的维护者。Node TSC Director - Rod Vagg 也曾在 Twitter 祝贺了我们发布 1.0。
可惜,几个核心的负责人的英语交流能力都一般,磕磕绊绊完成英文文档翻译后,由于各自在公司内部承担起更大的责任,精力不足,就直接弃疗了,在 issue 里面都直接中文回复,相当于直接放弃了国际社区。
Egg 3.0
上面讲了很多,最后总结下在 Egg 3.0 要做的一些事:
最主要的是回归 Egg 初心,把最核心的 Loader 这一层做到极致,提供完善的扩展机制和分析能力,方便架构师更好的构建上层框架,更方面的治理大规模应用实践。
其次是更多的实践场景分享,如基于 TS 的 DDD 实践、工具场景(如轻,足够轻,轻到足够可以替代掉 Webpack 等内置的 Express 实现的 devserver),Socket 之类的非 Web 场景。
还有配套基础设施的建设,如性能分析工具 Easy-Monitor,包管理 CNPM 都是基于 Egg 的。
最后是拥抱社区生态:更好的 Serverless 支持、更好的 TS 支持,以及更 Open 的社区共建。(我们正在和字节、蔚来等团队讨论 China Open Node.js Framework Spec
)
Serverless 时代
所以我们的新课题是:如何服务好主航道,为前端开发者提供 高效省心、稳定可靠的基础设施。
我们的探索有:
- 针对垂直场景做精细化的定制,如聚合场景,提供云端一体化的 Function 能力,往往就足够了。
- 底层的基础设施能力的完善,如后端中间件接入能力的 ServiceMesh 化,自动扩缩容机制等等,往云原生方向发展。
- 研发平台侧也很重要,对应用的持续自理,配套的 APM、包管理、日志、链路跟踪、安全治理等的建设。
但这块的挑战也很大:
- 即使是阿里这样的大厂,每个人对 Serverless 的理解也都不一致,Serverless 的基建也很不成熟,还有好多年的坑要慢慢填。
- 现阶段的很多优化,都跟基础设施强耦合,从而很难回馈给社区。
- 中间件接入能力 Mesh 化后,框架层就变得很薄了,还有存在的价值么?
不过曙光就在眼前了。
关于我
我是天猪,目前在蚂蚁体验技术部 广州分部,负责前端基础设施的建设,团队主要以 Node.js 为主,局部会用 go 写 mesh,用 rust 写模块,开源了 eggjs, cnpm 等项目,等你加入。