企业级 Node.js Web 应用解决方案设计的零零总总

简介:

年前一直在忙着做新版 Midway 升级的事情,不少同学都知道 Midway 是淘宝的 Node.js Web 应用解决方案,目的是为了更好的做前后端分离,让前端同学开发更简单,生活更幸福(笑)。

如今 Midway 5 正式发布了,横跨了几个月的开发个工作,期间带来的感慨,也算是史上最多。

Midway 的诞生也有 2 年多的时间,我个人参与维护也有 1 年多,经历了从 v3 到 v5 的变化,最大的感慨莫过于,分分合合,以前总想着灵活性,要做分离,后来就想着统一升级,又合并回去, 折腾的是自己,也是用户,不管怎么说,之前欠着的债总是要还的,历史包袱总是框架开发者的胸口大石,不破不立才是最终的方案。

代码风格选型

随着 ES6 乃至 ES2015 的出现,generator 和 promise 配合的开发方式渐渐的趋于稳定和标准化,再结合未来 async/await 的方式,使用 Koa 1.0 是比较中和的选择,在 2.0 推出之前,可以使用 yield 的写法来简化异步操作,将大部分的异步代码扁平化,同时也可以对未来的 Koa 2.0 代码进行一个很好的兼容和补充。

有人不禁会问,为什么不用 babel ,当然这是一种选择,在 Node.js 没有原生支持这些语法特性,乃至 --harmony 也无法启用的新特性的时候,我们不会考虑使用,这是在做企业级框架的一些基本原则,在面对数千万用户的期待的时候,我们不能拿稳定性来试错。

稳定性

框架的稳定性和业务的稳定性是两个不同的方向,业务需要的是容错,而框架需要的是兜底。很多时候业务代码只需要 try/catch 就能解决,再不然 promise.catch 也好,然后logger.error 就可以了,但是框架不行。

Midway 使用的是 Master/Agent/Worker 进程方案,同时会启动 N+2 的进程,每个 Worker 进程可能会和 Master/Agent 进程进行通信,一旦有进程错误甚至挂掉,都是一个复杂的情况,所以要处理所有类型的错误就变得非常重要。

进程本身有着一些简单的处理,比如在接受到正常的信息消息的时候正常退出流程,并且杀死其他子进程(碰到过其他子进程杀不死的,所以要强制再杀一下):

// SIGTERM AND SIGINT will trigger the exit event.
process.once('SIGQUIT', function() {
  process.exit(0);
});

process.once('SIGTERM', function() {
  process.exit(0);
});

process.once('SIGINT', function() {
  process.exit(0);
});

process.on('exit', function(code) {
  killAgentWorker();
});

当然,进程也有一些奇奇怪怪的异常,这些异常必须通过日志记录,然后才能进行安全的退出或者其他自定义行为。

process.on('unhandledRejection', function(err, p) {
     //logger
});

除了以上标准流程之外,就得考虑非主进程出错退出时的情况并做相应的处理,比如 Agent 进程属于非常重要的业务进程,假如第一次启动就出问题,那必然需要强制退出,如果进程在某些情况下意外挂掉,必须有一些自重启机制来保证稳定运行,同时需要处理一些事件(之前出现过事件绑定过多内存泄露的事故)。

agentWorker.once('exit', function(code, signal) {
  coreLogger.error(err);
  // 防止事件泄漏
  agentWorker.removeAllListeners('message');
  agentWorker = null;

  if (allWorkerStartSuccess) {
    // restart agent
    setTimeout(startAgent.bind(null, opts), 1000);
  } else {
    // AgentWorker 初始化过程发生异常,主进程直接退出
    // coreLogger.error('Agent worker init exception occurs. Master exits therefor.');
    process.exit(1);
  }
});

Worker 进程虽然使用 cluster 机制来启动,但是处理方式和 Agent 差不太多,除了挂掉自启之外,还需要有一些不一样的地方,比如进程的数量,原本默认的是 CPU 的核数,但是可能会根据当前的运行环境稍稍进行一些降低以保证内存的可用。此外,进程重启次数过多可能也是一大问题,需要进行额外的计数和报警,当然代码很简单,这边就不再赘述。

当然框架稳定性不仅仅只有这些,进程的处理只是最重要的一环,整个架构的设计中都必须考虑。

框架设计

Midway 新的设计理念是 Everything is a plugin,即所有的都是插件,包括框架和普通应用,这样的设计可以最大化的复用代码,简化使用。

一个简单的应用的结构和插件的结构,乃至框架的结构大致是一样的,经过集团 Node 小组的讨论形成了一套规范,也算是一次大统一。

app_name/
├─app/   
|  ├─extends/
|  │  └─application.js      
│  ├─controllers/         
│  │  └─home.js            
│  ├─router.js             
│  └─views/               
│     └─home.xtpl 
├─bin/                     
│  ├─build.sh
│  └─server.js 
├─config/
│  ├─config.js 
│  ├─config.local.conf 
│  ├─config.prod.conf 
├─node_modules/
├─package.json              
└─README.md

看起来非常简单,除了常见的 node_modules 之外,还有一些淘宝特有的 bin/app 目录和一些 xtpl 模板文件。_bin 是启动目录,这边暂且不谈。

所有的插件的目录结构除了没有 controllers 和 routers 之外,和应用的目录结构是一样的,这其中最重要的一环就是加载方式。

Midway 的加载思路非常清晰简单:

  • 顺序加载插件
  • 把应用作为最后一个插件加载进来
  • 后边的插件覆盖之前的插件

作为一个需要满足大部分场景的框架(插件、应用),需要加载东西有几样,配置文件、Koa 扩展、中间件,控制器,路由,这个时候需要一个通用的加载方法,这个方法可能是长这个样子。

_loadFiles(files, opts) {
  //...

  loadDirs.forEach((dir)=> {
    let fileResults = globby.sync(files, {cwd: dir});

    fileResults.forEach((f)=> {
      let m = util.tryRequire(path.join(dir, f), opts.required);
      let result = (is.function(m) && !is.class(m) && needCall) ? m.apply(this, opts.inject ? [].concat(opts.inject) : [this.app]) : m;

      results.push(opts.resultHandler ? opts.resultHandler.call(this, result, f, dir, m) : result);

      if (opts.target) {
        extend(true, opts.target, result);
      }
    });
  });

  return results;
}

整个方法核心的思路就是加载(tryRequire),除此之外,就是对加载之后的内容进行判断,处理,合并,返回。所有的加载都通过这一方法来做,就目前来看,大部分场景都已经满足了(笑)。

至此,一个框架的主线已经比较明确,核心功能也可用,剩下的就是插件的开发和补充,以及一些细节的修补。

细节和纠结

一个企业级框架的开发肯定没那么简单,主线设计相对容易一些,更麻烦的是细节,往往细节才是区别不同的框架最重要的地方。

兼容性

框架的历史包袱很大一部分体现在升级和兼容性上,但是框架的大版本更新往往是很多的不兼容,要让旧版本用户升级是一件非常头疼的事情。

Midway 也一样。

以前的 Midway 使用的是 Proxy 方式,所有暴露的外部接口都从 midway.getXXXX 中体现,而现有的进程加载方式使得 Midway 从 Worker 进程变为了 Master 进程,导致无法使用原本的方式了。

经历了多次讨论,最后还是为了用户妥协,将入口的文件(require 的部分) 变为 Worker ,而真正用户启动的 server.js 变为了 midway/server,也算是一个圆满的解决方案。

测试和调试

由于将 Worker 机制内置到了 Midway 框架中,本来用户通过 app.js 的调试方式就行不通了,现在必须通过 bin/server.js 的方式来调试,略显繁琐。

根据新升级的 IPC 通信方式,我们想到了可以通过只启动一个进程的方式来调试代码。所以在测试用例中也可以不用启动多个进程来测试代码了。

在大部分情况下测试代码使用 mocha + supertest 已经可以完美的完成了,但是偶尔会在运行多个的时候抽个风,这个问题属于 Agent 进程通信在本地无法判断出相同目录下是否是同一个实例的问题,除此之外,其他还没发现问题(笑:))。

更新机制

新 Midway 的设计理念是简化开发,以往的经历告诉我们,推动用户升级是不现实的,花了许多的时间在给用户升级脚本,升级 Node.js 上,不仅给自己带来了很多不必要的工作量,也给用户带来了很多麻烦和隐患。

在新的设计中,把插件都内置到了自身的依赖中,由框架统一来处理版本,同时,把打包脚本和启动脚本也固化到了框架中,随着框架一起升级,至少在框架使用到现在,已经非常明显的减少客服量。

Midway 本身的升级由 npm tag 版本来控制,这个是由脚手架来处理的,用户每次部署install,都使用的是该版本最新的框架。

"publishConfig": {
   "tag": "release-5.1"
 },

当然这样的行为也是有隐患的,比如某个插件升级导致框架出错,不过作为一个内部的框架,我们尽可能保证插件的兼容性和稳定性,必须符合 semver 的版本规范,必须有一定的测试覆盖率,如果有不兼容的情况,整个框架都会一起升级 tag,尽可能减少给用户带来问题的机会。

写在最后

一个解决方案、一个框架的诞生背后总有一群抓耳挠腮的开发者,经常为了一些小的地方,团队会讨论许久,不光是为用户负责,也对自己负责,Midway 不会走 102 年,只是希望在能做的事情上,稍微多做一点罢了。

想来随着 Midway 5 的发布,有一阵子可以不用考虑该如何权衡和取舍了,可以更加把事情专注在服务用户,提升效率这些事情上了(笑)。

最后,铭记,不忘初心,奋勇前行。

转载自:http://taobaofed.org/blog/2016/04/08/node-web-framework-design/

作者:张挺

目录
相关文章
|
16天前
|
存储 JavaScript 前端开发
Angular 应用 node_modules 子文件夹 @types 的作用介绍
Angular 应用 node_modules 子文件夹 @types 的作用介绍
12 1
|
12天前
|
JavaScript 前端开发 API
Vue.js:构建高效且灵活的Web应用的利器
Vue.js:构建高效且灵活的Web应用的利器
|
1月前
|
内存技术
node版本与npm版本不对应的解决方案
node版本与npm版本不对应的解决方案
25 0
|
1月前
|
运维 JavaScript 前端开发
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
|
1月前
|
JavaScript 前端开发 API
Vue.js:构建现代化Web应用的灵活选择
Vue.js:构建现代化Web应用的灵活选择
40 0
|
1月前
|
Web App开发 JavaScript 前端开发
深入浅出:Node.js 在后端开发中的应用与实践
【2月更文挑战第13天】本文旨在探讨Node.js这一流行的后端技术如何在现代Web开发中被应用以及它背后的核心优势。通过深入分析Node.js的非阻塞I/O模型、事件驱动机制和单线程特性,我们将揭示其在处理高并发场景下的高效性能。同时,结合实际开发案例,本文将展示如何利用Node.js构建高性能、可扩展的后端服务,以及在实际项目中遇到的挑战和解决方案。此外,我们还将讨论Node.js生态系统中的重要工具和库,如Express.js、Koa.js等,它们如何帮助开发者快速搭建和部署应用。通过本文的探讨,读者将获得对Node.js在后端开发中应用的深入理解,以及如何有效利用这一技术来提升开发效率
|
1月前
|
JavaScript 前端开发
node.js第四天--ajax在项目中的应用
node.js第四天--ajax在项目中的应用
27 0
|
2月前
|
JavaScript 前端开发 Java
MooTools、Backbone、Sammy、Cappuccino、Knockout、JavaScript MVC、Google Web Toolkit、Google Closure、Ember、Batman 以及 Ext JS。
MooTools、Backbone、Sammy、Cappuccino、Knockout、JavaScript MVC、Google Web Toolkit、Google Closure、Ember、Batman 和 Ext JS 都是 JavaScript 框架,用于开发 Web 应用程序。它们分别提供了不同的功能和特性,以帮助开发者更高效地构建和维护 Web 应用程序。
17 2
|
2月前
|
JavaScript NoSQL Redis
深入浅出:使用 Docker 容器化部署 Node.js 应用
在当今快速发展的软件开发领域,Docker 作为一种开源的容器化技术,已经成为了提高应用部署效率、实现环境一致性和便于维护的关键工具。本文将通过一个简单的 Node.js 应用示例,引导读者从零开始学习如何使用 Docker 容器化技术来部署应用。我们不仅会介绍 Docker 的基本概念和操作,还会探讨如何构建高效的 Docker 镜像,并通过 Docker Compose 管理多容器应用。此外,文章还将涉及到一些最佳实践,帮助读者更好地理解和应用 Docker 在日常开发和部署中的强大功能。
90 0
|
2月前
|
Web App开发 JavaScript 前端开发
构建现代Web应用:Vue.js与Node.js的完美结合
在当今快速发展的Web技术领域,选择合适的技术栈对于开发高效、响应迅速的现代Web应用至关重要。本文深入探讨了Vue.js和Node.js结合使用的优势,以及如何利用这两种技术构建一个完整的前后端分离的Web应用。不同于传统的摘要,我们将通过一个实际的项目示例,展示从搭建项目架构到实现具体功能的整个过程,着重介绍了Vue.js在构建用户友好的界面方面的能力,以及Node.js在处理服务器端逻辑和数据库交互中的高效性。通过本文,读者不仅能够理解Vue.js与Node.js各自的特点,还能学习到如何将这两种技术融合应用,以提升Web应用的开发效率和用户体验。