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 的官方例子开始

var connect = require('connect');
var http = require('http');var app = connect();// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());// store session state in browser cookie
var cookieSession = require('cookie-session');
app.use(cookieSession({keys: ['secret1', 'secret2']
}));// parse urlencoded request bodies into req.body
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({extended: false}));// respond to all requests
app.use(function(req, res){res.end('Hello from Connect!\n');
});//create node.js http server and listen on port
http.createServer(app).listen(3000);

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

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

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

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

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

function handle () {function app(req, res, next) { app.handle(req, res, next)}app.handle = function (req, res, next) {console.log(this);}app.statck = [];return app;
}var app = handle();app() // ==> { [Function: app] handle: [Function], stack: [] }app.apply({}) // ==>{ [Function: app] handle: [Function], stack: [] }

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

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

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

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

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

var merge = require('utils-merge');module.exports = createServer;var proto = {};function createServer() {// 函数对象,这个对象能调用,能加属性function app(req, res, next){ app.handle(req, res, next); }merge(app, proto); // ===等于调用 Object.assignmerge(app, EventEmitter.prototype); // === 等于调用 Object.assignapp.route = '/';app.stack = [];return app;
}

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

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

proto.handle = function handle(req, res, out) {var index = 0;var protohost = getProtohost(req.url) || ''; //获得 http://www.baidu.comvar removed = '';var slashAdded = false;var stack = this.stack;// final function handlervar done = out || finalhandler(req, res, {env: env,onerror: logerror}); // 接口 done(err);// store the original URLreq.originalUrl = req.originalUrl || req.url;function next(err) {if (slashAdded) {req.url = req.url.substr(1); // 除掉 / 之后的字符串slashAdded = false; // 已经拿掉}if (removed.length !== 0) {req.url = protohost + removed + req.url.substr(protohost.length);removed = '';}// next callbackvar layer = stack[index++];// all doneif (!layer) {defer(done, err); // 没有中间件,调用 finalhandler 进行处理,如果 err 有值,就返回 404 进行处理return;}// route datavar path = parseUrl(req).pathname || '/';var route = layer.route;// skip this layer if the route doesn't matchif (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {return next(err); // 执行下一个}// skip if route match does not border "/", ".", or endvar c = path[route.length];if (c !== undefined && '/' !== c && '.' !== c) {return next(err); // 执行下一个}// trim off the part of the url that matches the routeif (route.length !== 0 && route !== '/') {removed = route;req.url = protohost + req.url.substr(protohost.length + removed.length);// ensure leading slashif (!protohost && req.url[0] !== '/') {req.url = '/' + req.url;slashAdded = true;}}// call the layer handlecall(layer.handle, route, err, req, res, next);}next();
};

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

call 函数的代码如下:

function call(handle, route, err, req, res, next) {var arity = handle.length;var error = err;var hasError = Boolean(err);debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);try {if (hasError && arity === 4) {// error-handling middlewarehandle(err, req, res, next);return;} else if (!hasError && arity < 4) {// request-handling middlewarehandle(req, res, next);return;}} catch (e) {// replace the errorerror = e;}// continuenext(error);
}

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

还有 app.use 添加中间件:

proto.use = function use(route, fn) {var handle = fn; // fn 只是一个函数的话 三种接口 // 1. err, req, res, next 2. req, res, 3, req, res, nextvar path = route;// default route to '/'if (typeof route !== 'string') {handle = route;path = '/';}// wrap sub-appsif (typeof handle.handle === 'function') { // 自定义中的函数对象var server = handle;server.route = path;handle = function (req, res, next) {  // req, res, next 中间件server.handle(req, res, next);};}// wrap vanilla http.Serversif (handle instanceof http.Server) {handle = handle.listeners('request')[0]; // (req, res) // 最后的函数}// strip trailing slashif (path[path.length - 1] === '/') {path = path.slice(0, -1);}// add the middlewaredebug('use %s %s', path || '/', handle.name || 'anonymous');this.stack.push({ route: path, handle: handle });return this;
};

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

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

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

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

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

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

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

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

总结

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

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

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

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

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

转载于:https://www.cnblogs.com/htoooth/p/7116480.html

nodejs 实践:express 最佳实践(五) connect解析相关推荐

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

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

  2. git最佳实践_Git最佳实践如何为我节省大量的返工时间

    git最佳实践 by Hemal Patel 通过赫马尔·帕特尔 Git最佳实践如何为我节省大量的返工时间 (How Git best practices saved me hours of rewo ...

  3. devops最佳实践_DevOps最佳实践如何改善团队动力

    devops最佳实践 在过去的几个月中,我一直在写一些个人可以逐步提高成功的小规模渐进式行为. 本月,我想强调一下我认为对于取得小小的成功至关重要的团队行为. 我曾在Red Hat的AtomicOpe ...

  4. maven依赖最佳实践_Maven最佳实践

    maven依赖最佳实践 尽管Maven提供了"配置之上的约定"解决方案,但是仍然有足够多的必要配置引起严重的头痛. 在这篇文章中,我将与您分享一些最佳实践,以简化对POM文件的维护 ...

  5. 使用nodejs构建Docker image最佳实践

    文章目录 简介 准备nodejs应用程序 创建Dockerfile文件 创建.dockerignore文件 创建docker image 运行docker程序 node的docker image需要注 ...

  6. Nodejs 开发最佳实践

    1. 项目结构实践 ✔ 1.1 组件式构建你的解决方案 TL;DR: 大型项目的最坏的隐患就是维护一个庞大的,含有几百个依赖的代码库 - 当开发人员准备整合新的需求的时候,这样一个庞然大物势必减缓了开 ...

  7. HAWQ技术解析(十七) —— 最佳实践

    一.HAWQ参数配置最佳实践 (原文地址: http://hawq.incubator.apache.org/docs/userguide/2.1.0.0-incubating/bestpractic ...

  8. DDD分层架构最佳实践

    还在单体应用的时候就是分层架构一说,我们用得最多的就是三层架构.而现在已经是微服务时代,在微服务架构模型比较常用的有几个,例如:整洁架构,CQRS(命令查询分离)以及六边形架构.每种架构模型都有自己的 ...

  9. Ansible — 示例与最佳实践

    目录 文章目录 目录 最佳实践示例 Playbook Project 的目录结构 区分 Production 和 Stage Inventory 清单文件 区分 Group 和 Host Variab ...

最新文章

  1. 5 亿微博数据疑泄露,Python 爬虫如何避免踩天坑?
  2. RDKit:基于RECAP生成片段
  3. 关于WSE_CLIPSIBLINGS
  4. mac qt android开发环境搭建,Mac 下 PyQt5 的开发环境搭建
  5. python和c哪个好学-c语言和python哪个容易
  6. ubuntu镜像添加jdk_Ubuntu16.0.4安装jdk8
  7. JS使用onscroll、scrollTop实现图片懒加载
  8. python与lua闭包的一点不同
  9. java 后台线程作用_Java 后台线程介绍
  10. gifcam使用缩小内存_Vuex3.1.1更新:支持jsDelivr,修复内存泄漏
  11. C 使用异或(xor)加密/解密文件
  12. IOS学习之斯坦福大学IOS开发课程笔记(第六课)
  13. Java UDP编程
  14. 微信小程序 列表item点击事件
  15. 移动光猫RAISECOM的配置方法
  16. 电脑字母下标数字怎么打java_下标小字母大全 一个字母的右下标怎么用电脑打...
  17. Android Studio开启DDMS查看手机文件
  18. 自适应云呼HTML官网源码
  19. dplayer解析源码php调用,从demo分析ijk源码一:视频播放
  20. [附源码]java毕业设计宠物店管理系统

热门文章

  1. python软件下载安装要钱吗-PyCharm下载和安装详细步骤
  2. python退出命令-python退出指令
  3. python爬虫requests-Python爬虫(requests模块)
  4. python必背内容-学 Python 必背的42个常见单词,看看你记住了几个?
  5. python画柱状图和折线图-Python数据可视化–折线图–柱状图
  6. python工程师薪资-不止 20K,Python 工程师的薪资再度飙升!
  7. python软件怎么用-如何使用Python编写一个桌面软件系统?步骤有哪些
  8. python系统-Python OS模块常用功能 中文图文详解
  9. python安装包-Python软件包的安装(3种方法)
  10. php和python区别-php和python什么区别