nodejs 实践:express 最佳实践(五) connect解析

nodejs 发展很快,从 npm 上面的包托管数量就可以看出来。不过从另一方面来看,也是反映了 nodejs 的基础不稳固,需要开发者创造大量的轮子来解决现实的问题。

知其然,并知其所以然这是程序员的天性。所以把常用的模块拿出来看看,看看高手怎么写的,学习其想法,让自己的技术能更近一步。

引言

express 是 nodejs 中最流行的 web 框架。express 中对 http 中的 request 和 response 的处理,还有以中间件为核心的处理流程,非常灵活,足以应对任何业务的需求。

而 connect 曾经是 express 3.x 之前的核心,而 express 4.x 已经把 connect 移除,在 express 中自己实现了 connect 的接口。可以说 connect 造就了 express 的灵活性。

因此,我很好奇,connect 是怎么写的。

争取把每一行代码都弄懂。

connect 解析

我们要先从 connect 的官方例子开始

  1. var connect = require('connect');
  2. var http = require('http');
  3. var app = connect();
  4. // gzip/deflate outgoing responses
  5. var compression = require('compression');
  6. app.use(compression());
  7. // store session state in browser cookie
  8. var cookieSession = require('cookie-session');
  9. app.use(cookieSession({
  10. keys: ['secret1', 'secret2']
  11. }));
  12. // parse urlencoded request bodies into req.body
  13. var bodyParser = require('body-parser');
  14. app.use(bodyParser.urlencoded({extended: false}));
  15. // respond to all requests
  16. app.use(function(req, res){
  17. res.end('Hello from Connect!\n');
  18. });
  19. //create node.js http server and listen on port
  20. http.createServer(app).listen(3000);

从示例中可以看到一个典型的 connect 的使用:

  1. var app = connect()// 初始化
  2. app.use(function(req, res, next) {
  3. // do something
  4. })
  5. // http 服务器,使用
  6. http.createServer(app).listen(3000);

先倒着看,从调用的地方更能看出来,模块怎么使用的。我们就先从 http.createServer(app) 来看看。

nodejs doc 的官方文档中可以知, createServer 函数的参数是一个回调函数,这个回调函数是用来响应 request 事件的。从这里看出,示例代码中 app 中函数签就是 (req, res),也就是说 app 的接口为 function (req, res)

但是从示例代码中,我们也可以看出 app 还有一个 use 方法。是不是觉得很奇怪,js 中函数实例上,还以带方法,这在 js 中就叫 函数对象,不仅能调用,还可以带实例变量。给个例子可以看得更清楚:

  1. function handle () {
  2. function app(req, res, next) { app.handle(req, res, next)}
  3. app.handle = function (req, res, next) {
  4. console.log(this);
  5. }
  6. app.statck = [];
  7. return app;
  8. }
  9. var app = handle();
  10. app() // ==> { [Function: app] handle: [Function], stack: [] }
  11. app.apply({}) // ==>{ [Function: app] handle: [Function], stack: [] }

可以看出:函数中的实例函数中的 this 就是指当前的实例,不会因为你使用 apply 进行环境改变。

其他就跟对象没有什么区别。

再次回到示例代码,因该可以看懂了, connect 方法返回了一个函数,这个函数能直接调用,有 use 方法,用来响应 http 的 request 事件。

到此为此,示例代码就讲完了。 我们开始进入到 connect 模块的内部。

connect 只有一个导出方法。就是如下:

  1. var merge = require('utils-merge');
  2. module.exports = createServer;
  3. var proto = {};
  4. function createServer() {
  5. // 函数对象,这个对象能调用,能加属性
  6. function app(req, res, next){ app.handle(req, res, next); }
  7. merge(app, proto); // ===等于调用 Object.assign
  8. merge(app, EventEmitter.prototype); // === 等于调用 Object.assign
  9. app.route = '/';
  10. app.stack = [];
  11. return app;
  12. }

从代码中可以看出,createServer 函数把 app 函数返回了,app 函数有三个参数,多了一个 next (这个后面讲),app函数把 proto 的方法合并了。还有 EventEmitter 的方法也合并了,还增加了 route 和 stack 的属性。

从前面代码来看,响应 request 的事件的函数,是 app.handle 方法。这个方法如下:

  1. proto.handle = function handle(req, res, out) {
  2. var index = 0;
  3. var protohost = getProtohost(req.url) || ''; //获得 http://www.baidu.com
  4. var removed = '';
  5. var slashAdded = false;
  6. var stack = this.stack;
  7. // final function handler
  8. var done = out || finalhandler(req, res, {
  9. env: env,
  10. onerror: logerror
  11. }); // 接口 done(err);
  12. // store the original URL
  13. req.originalUrl = req.originalUrl || req.url;
  14. function next(err) {
  15. if (slashAdded) {
  16. req.url = req.url.substr(1); // 除掉 / 之后的字符串
  17. slashAdded = false; // 已经拿掉
  18. }
  19. if (removed.length !== 0) {
  20. req.url = protohost + removed + req.url.substr(protohost.length);
  21. removed = '';
  22. }
  23. // next callback
  24. var layer = stack[index++];
  25. // all done
  26. if (!layer) {
  27. defer(done, err); // 没有中间件,调用 finalhandler 进行处理,如果 err 有值,就返回 404 进行处理
  28. return;
  29. }
  30. // route data
  31. var path = parseUrl(req).pathname || '/';
  32. var route = layer.route;
  33. // skip this layer if the route doesn't match
  34. if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
  35. return next(err); // 执行下一个
  36. }
  37. // skip if route match does not border "/", ".", or end
  38. var c = path[route.length];
  39. if (c !== undefined && '/' !== c && '.' !== c) {
  40. return next(err); // 执行下一个
  41. }
  42. // trim off the part of the url that matches the route
  43. if (route.length !== 0 && route !== '/') {
  44. removed = route;
  45. req.url = protohost + req.url.substr(protohost.length + removed.length);
  46. // ensure leading slash
  47. if (!protohost && req.url[0] !== '/') {
  48. req.url = '/' + req.url;
  49. slashAdded = true;
  50. }
  51. }
  52. // call the layer handle
  53. call(layer.handle, route, err, req, res, next);
  54. }
  55. next();
  56. };

代码中有相应的注释,可以看出,next 方法就是一个递归调用,不断的对比 route 是否匹配,如果匹配则调用 handle, 如果不匹配,则调用下一个 handle.

call 函数的代码如下:

  1. function call(handle, route, err, req, res, next) {
  2. var arity = handle.length;
  3. var error = err;
  4. var hasError = Boolean(err);
  5. debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
  6. try {
  7. if (hasError && arity === 4) {
  8. // error-handling middleware
  9. handle(err, req, res, next);
  10. return;
  11. } else if (!hasError && arity < 4) {
  12. // request-handling middleware
  13. handle(req, res, next);
  14. return;
  15. }
  16. } catch (e) {
  17. // replace the error
  18. error = e;
  19. }
  20. // continue
  21. next(error);
  22. }

可以看出一个重点:对错误处理,connect 的要求 是函数必须是 四个参数,而 express 也是如此。如果有错误, 中间件没有一个参数的个数是 4, 就会错误一直传下去,直到后面的 defer(done, err); 进行处理。

还有 app.use 添加中间件:

  1. proto.use = function use(route, fn) {
  2. var handle = fn; // fn 只是一个函数的话 三种接口 // 1. err, req, res, next 2. req, res, 3, req, res, next
  3. var path = route;
  4. // default route to '/'
  5. if (typeof route !== 'string') {
  6. handle = route;
  7. path = '/';
  8. }
  9. // wrap sub-apps
  10. if (typeof handle.handle === 'function') { // 自定义中的函数对象
  11. var server = handle;
  12. server.route = path;
  13. handle = function (req, res, next) { // req, res, next 中间件
  14. server.handle(req, res, next);
  15. };
  16. }
  17. // wrap vanilla http.Servers
  18. if (handle instanceof http.Server) {
  19. handle = handle.listeners('request')[0]; // (req, res) // 最后的函数
  20. }
  21. // strip trailing slash
  22. if (path[path.length - 1] === '/') {
  23. path = path.slice(0, -1);
  24. }
  25. // add the middleware
  26. debug('use %s %s', path || '/', handle.name || 'anonymous');
  27. this.stack.push({ route: path, handle: handle });
  28. return this;
  29. };

从代码中,可以看出,use 方法添加中间件到 this.stack 中,其中 fn 中间件的形式有两种: function (req, res, next) 和 handle.handle(req, res, next) 这两种都可以。还有对 fn 情况进行特殊处理。

总的处理流程就是这样,用 use 方法添加中间件,用 next 编历中间件,用 finalHandle 进行最后的处理工作。

在代码中还有一个函数非常奇怪:

  1. /* istanbul ignore next */
  2. var defer = typeof setImmediate === 'function'
  3. ? setImmediate
  4. : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

defer 函数中的 fn.bind.apply(fn, arguments),这个方法主要解决了,一个问题,不定参的情况下,第一个参数函数,怎样拿到的问题,为什么这样说呢?如果中我们要达到以上的效果,需要多多少行代码?

  1. function () {
  2. var cb = Array.from(arguments)[0];
  3. var args = Array.from(arguments).splice(1);
  4. process.nextTick(function() {
  5. cb.apply(null,args);
  6. })
  7. }

这还是 connect 兼容以前的 es5 之类的方法。如果在 es6 下面,方法可以再次简化

  1. function(..args){ process.nextTick(fn.bind(...args)) }

总结

connect 做为 http 中间件模块,很好地解决对 http 请求的插件化处理的需求,把中间件组织成请求上的一个处理器,挨个调用中间件对 http 请求进行处理。

其中 connect 的递归调用,和对 js 的函数对象的使用,让值得学习,如果让我来写,就第一个调个的地方,就想不到使用 函数对象 来进行处理。

而且 next 的设计如此精妙,整个框架的使用和概念上,对程序员基本上没有认知负担,这才是最重要的地方。这也是为什么 express 框架最受欢迎。koa 相比之下,多几个概念,还使用了不常用的 yield 方法。

connect 的设计理念可以用在,类似 http 请求模式上, 如 rpc, tcp 处理等。

我把 connect 的设计方法叫做 中间件模式,对处理 流式模式,会有较好的效果。

nodejs 实践:express 最佳实践(五) connect解析的更多相关文章

  1. nodejs 实践:express 最佳实践(三) express 解析

    nodejs 实践:express 最佳实践(三) express 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的基础不稳固, ...

  2. nodejs 实践:express 最佳实践(七) 改造模块 connect2 解析

    nodejs 实践:express 最佳实践(七) 改造模块 connect2 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs 的 ...

  3. nodejs 实践:express 最佳实践(四) express-session 解析

    nodejs 实践:express 最佳实践(四) express-session 解析 nodejs 发展很快,从 npm 上面的包托管数量就可以看出来.不过从另一方面来看,也是反映了 nodejs ...

  4. nodejs 实践:express 最佳实践系列

    nodejs 实践:express 最佳实践系列 nodejs 实践:express 最佳实践(一) 项目结构 nodejs 实践:express 最佳实践(二) 中间件 nodejs 实践:expr ...

  5. nodejs 实践:express 最佳实践 (一)

    express 最佳实践 (一) 最近,一直在使用 nodejs 做项目,对 nodejs 开发可以说深有体会. 先说说 nodejs 在业务中的脚色,, 在 web同构 方面, nodejs 的优势 ...

  6. nodejs 实践:express 最佳实践 (一) 项目结构

    express 最佳实践 (一) 第二篇: express 最佳实践(二):中间件 最近,一直在使用 nodejs 做项目,对 nodejs 开发可以说深有体会. 先说说 nodejs 在业务中的脚色 ...

  7. nodejs 实践:express 最佳实践(六) express 自省获得所有的路由

    nodejs 实践:express 最佳实践(六) express 自省获得所有的路由 某些情况下,你需要知道你的应用有多少路由,这在 express 中没有方法可以.因此我这边曲线了一下,做成了一个 ...

  8. nodejs 实践:express 最佳实践(八) egg.js 框架的优缺点

    nodejs 实践:express 最佳实践(八) egg.js 框架的优缺点 优点 所有的 web开发的点都考虑到了 agent 很有特色 文件夹规划到位 扩展能力优秀 缺点 最大的问题在于: 使用 ...

  9. nodejs 实践:express 最佳实践(二) 中间件

    express 最佳实践(二):中间件 第一篇 express 最佳实践(一):项目结构 express 中最重要的就是中间件了,可以说中间件组成了express,中间件就是 express 的核心. ...

随机推荐

  1. MYSQL_与excel结合在excel中用&连接符快速创建表头_20161125

    excel &连接符快速创建表头 复制c列内容 CREATE TABLE A0001restaurant ( #用户明细表 城市 ), 区块 ), 用户ID ), 用户名称 ), 用户地址 ) ...

  2. WPF 后台触发 Validate UI‘s Element

    wpf中有validateRule类, 用于界面元素的验证, 如何后台去控制validateRule呢? 1. UI层要binding写好的ValidateRule,分为Binding和MultiBi ...

  3. 好文章!转载嵌入式LINUX

    整理了嵌入式linux学习路线供参考,希望对您有所参考价值! 一.linux入门 目前嵌入式主要开发环境有 Linux.Wince等:Linux因其开源.开发操作便利而被广泛采用.而Linux操作系统 ...

  4. 参数化之利用CSV Data Set Config从文件读取参数并关联变量

    众所周知,在进行接口测试的过程中,需要创建不同的场景(不同条件的输入,来验证不同的入参的返回结果).因而,在日常的自动化接口监控或商品监控等线上监控过程中,需要配置大量的入参来监控接口的返回是否正确. ...

  5. C#窗体控件拖动

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...

  6. 【243】◀▶IEW-Unit08

    Unit 8 Environment I. 不定式(to do)在雅思写作中的运用 1)名词 • 主语(句首) To protect the environment is everyone's dut ...

  7. 如何更改linux文件的拥有者及用户…

    本文整理自: http://blog.163.com/yanenshun@126/blog/static/128388169201203011157308/ http://ydlmlh.iteye.c ...

  8. Primer回顾 数组和指针

    数组和指针类似于vector和迭代器. 区别在于:数组的长度是固定的.数组一经创建,就不允许添加新的元素.指针则可以像迭代器一样用于遍历和检查数组中的元素. 设计良好的程序只有在强调速度时才在类实现的 ...

  9. htons和htonl函数具体应用

    htons和htonl函数具体应用 htons和htonl函数,是用来将主机字节顺序转换为网络字节顺序在进行网络抓包时,抓到的包的数据是网络字节顺序,在进行编程时,要进行主机字节顺序和网络字节顺序间的 ...

  10. echarts学习的一些笔记

    工具栏组件 Show 是否显示 Feature 具体显示的功能 saveAslmage  保存图片 Restore 还原 dataZoom  缩放视图 magicType 动态类型切换 toltip组 ...