企业级 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/

作者:张挺

时间: 2024-10-23 05:24:20

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

Node.js配合node-http-proxy解决本地开发ajax跨域问题_node.js

情景: 前后端分离,本地前端开发调用接口会有跨域问题,一般有以下3种解决方法: 1. 后端接口打包到本地运行(缺点:每次后端更新都要去测试服下一个更新包,还要在本地搭建java运行环境,麻烦) 2. CORS跨域:后端接口在返回的时候,在header中加入'Access-Control-Allow-origin':* 之类的(有的时候后端不方便这样处理,前端就蛋疼了) 3. 用nodejs搭建本地http服务器,并且判断访问接口URL时进行转发,完美解决本地开发时候的跨域问题.  用到的技术:

Egg.js 1.2.1 发布,阿里开源的企业级 Node.js 框架

Egg.js 1.2.1 发布了,egg 是阿里开源的企业级 Node.js 框架,为企业级框架和应用而生.通过 egg,团队的架构师和技术负责人可以非常容易地基于自身的技术架构在 egg 基础上扩展出适合自身业务场景的框架. 更新内容: [13587667] - fix(loader): loadPlugin 可以被拓展 [1a027ad7] - test: 使用 assert 替换 should [89b4df9d] - docs: 修复中文 router 文档中的名称错误 下载地址: So

优化Node.js Web应用运行速度的10个技巧_node.js

Node.js 受益于它的事件驱动和异步的特征,已经很快了.但是,在现代网络中只是快是不行的.如果你打算用 Node.js 开发你的下一个Web 应用的话,那么你就应该无所不用其极,让你的应用更快,异常的快.本文将介绍 10 条,经过检验得知可大大提高 Node 应用的技巧.废话不多说,让我们逐条来看看. 1. 并行 创建 Web 应用的时候,你可能要多次调用内部 API 来获取各种数据.比如说,假设在 Dashboard 页面上,你要执行下面这几个调用: 用户信息 -getUserProfil

Chair:支付宝前端团队推出的Node.js Web框架

Chair是支付宝前端团队推出的,基于Node.js的Web框架,适用于大部分的Web应用. 本文简要介绍Chair的设计思想.功能架构和开发状况. 一.Chair的由来和设计思想 历史上,支付宝前端项目都是直接基于Java后端开发的.这种架构下,前端工程师做出网页模板(基于velocity模板引擎的vm文件),交给后端的Java引擎渲染.支付宝采用的Java引擎是名为Sofa的MVC框架. 对于前端工程师来说,这种架构有很多不方便的地方.首先,需要了解后端的实现,并且依赖开发环境中的dev服务

Node.js + Web Socket 打造即时聊天程序嗨聊

前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术.像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端.瞬间就有了一统天下的感觉,来往穿梭于前后端之间代码敲得飞起,从此由前端晋升为'前后端'. 图片来自G+ 本文将使用Node.js加web socket协议打造一个网页即时聊天程序,取名为HiChat,中文翻过来就是'嗨聊',听中文名有点像是专为寂寞单身男女打造的~ 其中将会使用到express和socket.io两个包模块,下面会有介绍

node.js WEB开发中图片验证码的实现方法_node.js

用node做web开发很多都可能碰到需要验证码的地方,之前在github上搜索,有一些比如node-captcha等的类库,都需要依赖第三方的图形处理库或者软件,像我之前安装cario这个图形库时,真是费了好大一番劲,但是其实我们只用到了这些图形库的一点点小功能,比如图片的尺寸修改裁剪,或者生产验证码. 先介绍一下CImg这个c++的图形库吧,CImg是一个跨平台的C++的图像处理库,提供了加载.处理.显示.保存等一系列功能,最吸引人的地方是整个图形库就一个CImg.h这个文件,所以非常的便携绿

node.js Web应用框架Express入门指南_javascript技巧

一.安装 复制代码 代码如下: $ npm install express 或者在任何地方使用可执行的 express(1) 安装: 复制代码 代码如下: \# 译注:强烈建议这种方式$ npm install -g express 二.快速上手 最快上手 express 的方法是利用可执行的 express(1) 来生成一个应用,如下所示: 创建一个 app: 复制代码 代码如下: $ npm install -g express$ express /tmp/foo && cd /tmp

koahubjs 更新 0.07,Node.js web 快速开发框架

koahubjs 更新至 0.07 1.更新启动文件 // src/index.js启动文件 import Koahub from "koahubjs"; //默认app是项目目录 const app = new Koahub(); app.getKoa();获取koahubjs实例化,支持自定义koa中间件 app.run(3000); 2.更新koahubjs-demo 创建www目录 创建src/index.js 移除start.js 文章转载自 开源中国社区 [http://w

如何用node.js实现一个简单的web服务器

node.js实现web服务器还是比较简单的,我了解node.js是从<node入门>开始的,如果你不了解node.js也可以看看! 我根据那书一步一步的练习完了,也的确大概了解了node.js,不过里面写的路由的地方总感觉不方便,十一放假最后一天,试着写了个简单的web服务器,现在分享记录于此! http模块已提供了基本功能,所以我主要解决两个问题,1是静态资源的处理,2是动态资源的路由. 静态资源在node.js里的意思是不变的,如图片.前端js.css.html页面等. 动态资源我们一般