egg.js和nestjs使用场景对比

10,766 阅读13分钟

提醒大家:注意文章的时效性,毕竟不同的时间节点,框架成熟度不同。分享这篇文章主要是希望能够让大家认清框架的定位和应对的业务场景,框架没有好坏之分,只有是否适合自己。

原标题:关于nodejs-web框架的调研

作者:xingyuzhe

日期: Feb 25, 2018

原文: github.com/xingyuzhe/b…

以下内容其实是补漏, 算是对1月份一点工作的总结。

加入了新团队, 初次接触, 粗略的查看了一些项目(nodejs server端), 发现存在几个问题:

对于新成立的团队,存在以上问题可以理解, 本次是讨论和解决第一个问题。

对于集团范围内而言,同种技术存在多种不同规范和框架很正常, 但是对于一个几十人的团队而言, 资源有限,还是集中力量统一一下效率高些, 有鉴于此, 觉得做一个种子项目比较合适: 一个企业定制框架, 共同维护, 同步更新, 信息共享和沉淀最佳实践。

跟领导聊了一下, 总结了一下当前状态团队对于这块内容的一些主观诉求:

  1. 框架实现够简单, 团队能全面掌控, 低风险。
  2. 给新手从基础开始学习的机会, 就是希望除了满足业务需求之外, 团队也能得到成长。
  3. 一个企业基础框架应满足的基本要求: 约束规范、扩展机制、安全、高效业务开发
  4. 最终还是希望能沉淀出一套自己的轮子

根据上面的要求, 筛选出了eggjsnestjs2个web框架, 其中nestjs是团队一部分同学比较喜欢和尝试使用了的。

首先从外层信息做些调查。

我通读了eggjs和nestjs的文档,查阅了一些资料, 简单对比如下:

-- eggjs nestjs
github stars 7014 4291
github forks 720 250
gitHub dependents 1591/305 0/0
npm search results 565 53+
github contributors 101 31
github core contributors 4(10+) 1(2+)
github releases 49 19
github issues 86/1416 20/347
基础框架依赖 koa2 express
文档 业界良心 一般
核心原理 载入-挂载 模块容器-依赖注入(通过装饰器和元数据实现)
核心理念 微内核-插件机制 组件树-装饰器-流程控制

eggjs的特点和解决的主要问题:

  1. 通过载入代码-自动挂载代码到对象的方式解决了到处写import/require的问题, 不再需要手动维护模块之间的依赖关系, 核心就是一个loader, 虽然官方没有提及, 但是本人开下脑洞猜测, 这个模块的实现灵感有可能来自于坟头草已经一人高了的seajs, -_-, 这种挂载模式实现起来简单粗暴直接高效, 但是一个小问题就是在使用TS编写代码的时候, 会影响书写体验的流畅度, 毕竟不很OOP。
  2. 微核心 + 插件机制(这个理念其实一直以来都是代码组织的核心手段, 本人在以前开发播放器和IM客户端这块时感同身受), 大部分功能通过插件实现, 从而剥离非核心生命周期功能代码, 达到解耦/降低思维负担的目的; 另外一点就是跨模块API调用比较自由没有限制, 这个地方带来便捷的同时也有可能需要一定约束。
  3. 统一约束和规范, 对开发人员强约束, 保证不会出现千人千面的代码风格和设计。
  4. 种子项目 + 渐进式开发, 可以沉淀出自己的插件和业务框架、最佳实践
  5. 针对业务中遇到的常见问题基本都给出了解决方案, 插件丰富, 配套齐全
  6. 内部实现了进程间管理和通信功能
  7. 文档可是说是比较细致了, 基本把web这块涉及到的点都涵盖了, 仅仅通读文档对新手而言都会有不少收获
  8. 核心开发者较多, 属于团队项目, 整体看来比较严谨
  9. 沉淀时间较长, 正式发版2年, 实际发展了4年
  10. 明确定义了agent和app模式, 也实现了schedule功能

nestjs的特点和解决的主要问题:

  1. 通过模块容器-依赖注入维护组件树的模式解决了到处写import/require的问题, 不再需要手动维护模块之间的依赖关系, 另外编码方式与java十分相似。
  2. 组件树容器模式也达到了解耦效果, 实现方式上很大程度上受angular-module影响, 但是需要指定节点component的依赖关系, 跨模块调用严格依赖于声明. 其实这块功能github有独立的项目专做这个东西, 例如: InversifyJS | bottlejs, nestjs 自己实现了这个模块。
  3. 统一约束和规范, 对开发人员强约束
  4. 大量使用装饰器, 这个未支持的特性需要编译, 对代码阅读上可能会提高一些难度
  5. 流程控制上比较细致, 提出了filter、pipe、guard、interceptor这些明确的概念, 虽然这些工作在正常开发时也会做, 但是明确提出来并制定规范还是很有必要的
  6. 明确了Exception Layer的概念
  7. 默认使用/推荐TS开发
  8. 自带微服务实现
  9. 自带swagger接口文档生成功能
  10. 核心开发基本就作者一人, 属于个人项目, 提交记录比较难看, 前期的提交非常随便, 到后期才慢慢改善
  11. 2017年1月立项, 2017年5月发布第一个正式版本, 尚需时日印证

从上述情况来看, 2个框架都在123这几项做了工作, 完成了一个框架所应具备的基本诉求; 总体上看eggjs更加成熟和全面, 配套/生态相对完善的多, 低风险; nestjs则比较青涩和单一, 但是在组件树、流程控制、错误层级处理上有自己的特色, 理念上更加OOP,学习并吸收这些理念是很可取的, 但暂时不建议在核心产品线上投入使用。

eggjs

详细阅读了核心相关代码, 粗略阅读了cluster等一些模块和插件的代码, 代码读起来总体比较流畅, 发现框架的核心实现非常简单明了:

  • 框架基础依赖一个非常独立的loader模块, 功能就是加载代码文件和挂载函数到指定对象。

  • 框架本身核心类只有6个: koa本身的application, context, response, request, egg自身新增的controller, service 这2个类, 后续所有的挂载动作都在这几个类上面进行

  • 框架明确了app, framework, plugin3个概念, 依赖方式大概是这样:

  • 框架启动的核心流程主要是这样:

    由于eggjs自己实现了cluster, 自带进程管理和进程间通信功能, 所以egg自身部署时并不需要pm2这个工具, 官方文档上也对此做了解释。不过由于这块内容的引入(cluster还涉及到schedule、socket等功能),给框架本身带来了大量额外的代码和逻辑, 总体提升了框架的复杂度。

    但是有些时候由于某些原因并不希望直接使用框架提供的cluster解决方案, 另外我查看了下pm2的API, 也是有进程间通信的API的, 当然用起来可能没有自定义实现时那样自由, 也尚未听说该API有被广泛使用过, 不过, cluster相关的内容能否作为一个扩展包, 而不是强耦合进框架核心流程中? 这样框架本身更加简单纯洁, 或者更容易被接受一些?

    为此尝试抽离了eggjs的核心代码, 目标是仅保留最最核心的代码(移除cluster等周边代码), 具备egg的扩展机制和能正常直接使用eggjs的周边插件, 结果最后只需要几百行代码, 很少几个文件即可完成。后续在此基础上整理了一个上层框架, 预想作为业务项目的基础框架使用,主要涉及以下一些方面:

    1. 抽离eggjs的核心代码, 仅保留其插件机制和对应的约束规范
    2. 集成常用扩展函数、中间件、插件
    3. 集成多节点/进程下消息推送解决方案示例
    4. 集成schedule定时任务模块

    之后花了点时间用这个上层框架开发了一个抽奖小项目, 开发体验还算流畅, 虽说是草量级项目, 不过也是五脏俱全, 作为example还挺适合。

    nestjs

    粗略查看了下nestjs的源码, nestjs的核心其实就是一个IoC模块管理容器的实现, 这块内容的逻辑实现作者处理的还是相对复杂的多, 这里吐槽下作者的代码组织方式和略显随意的注释大量的接口引用和糟糕的历史提交记录...,真是额外提高了阅读的难度. TypeScript的加持和作者本人的光环也不能阻挡这一点。言归正传, 这里说下它的核心原理和流程。

    要想理解nestjs的源码先要理解和掌握以下知识:

    1. ES6的proxy,reflect
    2. TypeScript的decorator
    3. inversion of control (IoC)的基本概念
    4. 一定TypeScript基础.
    5. 如何通过decorator和元数据实现依赖注入
    • container类: 用于存储所有模块
    • scanner类: 递归提取出所有模块并存储到容器中; 提取出模块间的关联关系和模块自身的各种类以及内部联系,并存储到模块中;
    • module类: 存储自身的关联模块、组件、可注入类、控制器类
    • injector类: 依赖注入的核心环节, 在所有模块的内容和关系都被扫描出后, 来创建实例,
    • instanceLoader类: 使用Injector来加载模块的各种实例, 这个过程很复杂, 伴随递归和各种判断

    这个IoC容器实现的核心流程是这样: scanner扫描所有module并提取关系存入module->module存入 container-> injector创建实例(依赖注入)-> instanceLoader加载实例

    我们再拿nestjs实现上面eggjs版本lottery的例子, 大概是这样:

    结合源码和实际开发体验来说:

    • eggjs框架本身的模块管理(扩展机制)非常简单, 复杂度主要在于cluster这块内容和为此配套的周边设施(命令行工具、调试工具),但是这个复杂度是脱离于核心之外的东西。
    • nestjs的复杂度主要在于IoC模块管理器这块的实现上, 实际上这个东西理论上可以独立出来, 以此降低框架本身逻辑的复杂度。

    从总体上讲, eggjs相对成熟, 更贴合实际开发需求。 nestjs的优势就是在一些细节上的约束和控制以及理念上的新颖(仅相对node-web框架而言), 但是这些并不是核心诉求。

    另外eggjs团队做的工作内容相对于nestjs而言相当的多, 这些与人力、时间资源的投入是分不开的。

最后, 对于想要的新框架的处理结论已经有了:

1 社区模式(节省资源)

  • 直接在eggjs基础上做一个上层框架, 吸收nestjs的一些优点, 作为插件/扩展内容去完善框架本身.

2 造轮子模式

  • eggjs插件机制 + nestjs流程控制 + component模式(可选) => 新轮子

3 所应具备的特性

  • 约束规范
  • 沉淀/扩展模式
  • 模块依赖管理
  • 集成typescript开发环境
  • 集成API文档输出方案
  • 环境配置
  • 多节点/cluster解决方案
    • socket.io
    • schedule
  • 集成常用插件功能
    • logger
    • cookie
    • session
    • security
    • i18n
    • redis
    • sequelize
    • multipart
    • oauth
    • onerror
    • passport
    • view
    • role
    • crypto
  • 定义控制流/数据流约束规范
    • schema
    • pipe
    • interceptor
    • guard
  • migration
  • 微服务
  • 错误分级
  • 监控
  • 测试用例/代码覆盖率
  • 调试
  • 压力测试

补充: 随着各种工具的发展, 加上eggjs使用也有一段时日,最初一些模糊的预感变得清晰: 1 egg内置cluster模式带来的额外麻烦太多, 现在各种配套工具逐渐完善, 这个功能并没有什么用

2 egg模块隔离度不够但矛盾的是有时候又有限制, 简单的说就是代码组织模式上作为框架不够好, 需要自行定制上层规范; egg的是plugin模式, 相对于nest的component模式还是不够用;

3 egg的ts开发的流畅度差那么一点

4 实际的项目, 需要各种配套设施、工具、系统的协作联合,框架只是一个组成部分, 就应该只做它应该做的事就可以了

各有优劣, 诉求不一样, 选择就不一样: 1 eggjs: 短平快稳,上手极快, 文档完善,配套齐全, 能极大缩短工期。 2 nestjs: 有点追求, 复杂度高的协作项目, 核心诉求是代码组织模式的

之前egg很好的满足了我们的诉求, 但是下一个项目, 会考虑使用nestjs或者自行开发框架(足够闲的话)。

egg官方维护者atian25的评论:点击详见

atian25补充下几点:

  • Cluster 里面并没有 schedule

  • Socket 那个是为了实现高级的 IPC

  • Cluster 还是很建议使用的,如果有什么特殊的定制需求,何不如提下 RFC/ISSUE 大家讨论下

  • egg-core + egg-cluster 才成为 egg,你要是想定制,可以类似 egg-core + my-cluster(虽然不建议)

  • 框架复杂度其实反而是降低了,因为 PM2 的代码比 egg-cluster 复杂多了

  • Egg 的定位是框架的框架,跟 thinkjs/sails/nest 这些是没法直接对比的,因为不是一个层面的概念。就像你只能对比 Express 和 Koa,而不能对比 nest 和 Koa,因为 nest 是在 Express 之上的封装。

    基于 Egg 封装的针对某个领域的上层框架,才能对比。譬如完全可以用官方的 TS 方案,再封装集成几个装饰器,成为一个上层框架。就可以拿来对比了。

相关资料

Egg & Node.js 从小工坊走向企业级开发

选择JavaScript开源库时,你需要考虑这些问题

12个因素

关注公众号,发现更多精彩