内容简介:关于Node JS 的后端框架,不管是无论那个框架的中间件, 路由等的处理,都是从这里是一个入口,之前我们已经分析过了Eggjs 框架,但是没有分析Nodejs实现后端框架实现的底层原理,因为Eggjs 是基于Koa 实现的一个上层框架, 我们这次来通过Express来分析下Nodejs 实现后端框架的底层原理。
关于Node JS 的后端框架,不管是 Express
, Koa
, 甚至 Eggjs
(Eggjs 是基于Koa 底层封装的框架),都是基于NodeJS 的 http
模块进行处理的,其最重要的是方法是
const server = http.createServer((rep, res) => { res.end('hello world') }) server.listen(prot, () => { console.log('Server Started. Port: '+ prot) }) 复制代码
无论那个框架的中间件, 路由等的处理,都是从这里是一个入口, 对服务器的资源的任何访问都会先进入 http.createServer
方法的回调函数,也就是下面的方法
(rep, res) => { res.end('hello world') } 复制代码
之前我们已经分析过了Eggjs 框架,但是没有分析Nodejs实现后端框架实现的底层原理,因为Eggjs 是基于Koa 实现的一个上层框架, 我们这次来通过Express来分析下Nodejs 实现后端框架的底层原理。
源码结构
我们先从 express clone 一份源码,其对应的 lib 文件夹就是 express
框架的整个源码
其最重要的几个文件是:
- express.js (项目的入口文件,暴露除了很多对象,其中最重要的是一个
createApplication
方法)(重点) - application.js (最核心的一个文件,但是是对上面
createApplication
方法,返回的 app 对象去挂载很多方法)(重点) - request.js 和response.js 两个文件,主要是对
http.createServer
方法中的rep 和 res 进行相应的封装处理 - utils.js 只是封装了一些帮助方法
- View.js 模版引擎的相关的方法
- router 文件夹,是express实现的关键,也就是路由的处理,我们的任何一个请求,其实对应的就是一个路由, 然后返回相应的资源(重点)
- middleware, 是定义中间件的文件夹,不过其中只有两个很简单的内置中间件, 因为Express的很多中间件都是第三方的库
我们下面根据 启动服务* 和 ** 访问服务 两个流程来分析 express
, 会针对上面标注为(重点)的相应的文件,进行详细的分析.
启动服务
express.js
我们先从怎么使用开始,作为入口,下面是一个简单的express的demo.
const express = require('./lib/express') const app = module.exports = express() app.get('/', (req, res) => { res.end('hello world') }) if (!module.parent) { app.listen(3000); console.log('Express started on port 3000') } 复制代码
上面一段简单的代码,我们就已经搭建好了一个后端服务,当我们用浏览器打开 http://localhost:3000/
时,就会显示 hello world.
,下面我们就来看看是怎么实现的.
const app = module.exports = express()
可以看出 express()
应该是express.js 文件里面暴露出来的一个方法, 其对应的脚本是: exports = module.exports = createApplication;
createApplication
方法如下:
function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false);// 合并prototype mixin(app, proto, false);// 合并proto app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init(); return app; } 复制代码
这个方法,返回一个 app 对象, 这个对象相当于继承与 EventEmitter.prototype
和一个 proto
对象原型, 然后执行了 app.init()
方法,这个方法主要是做一些初始化工作并清空 cache
, engines
, settings
, 并且去初始化一些配置,比如说:
this.enable('x-powered-by'); this.set('etag', 'weak'); this.set('env', env); this.set('query parser', 'extended'); this.set('subdomain offset', 2); this.set('trust proxy', false); 复制代码
this.set
设置的值是保存在 settings中的
, 比如我们我们可以 this.settings['x-powered-by']
可以在应用中任何的地方去调用.所以这里有一个扩展出一个应用:
const express = require('./lib/express'); const app = module.exports = express(); app.set('config', { url: 'http://localhost:8080', userInfo: { name: 'ivan fan', age: 18 } }) app.get('/', (req, res) => { const config = app.get('config') console.log(config) res.end('hello world') }) if (!module.parent) { app.listen(3000); console.log('Express started on port 3000'); } 复制代码
上面我们通过 app.set
去设置一个 config
的值,我们在其他的地方可以通过 app.get
去获取这个值,这样看起来感觉没有什么用途,因为我们可以直接定义一个变量就可以,没必要通过 app.set
, 但是如果我们的应用很大的时候,我们将项目拆分成了很多单独的文件,我们只是共享了 app
对象,但是在多个js文件中可能需要公用一个全局的配置,我们可以创建一个config.json文件,在不同的页面都去import 进来,但是如果如果我们在 app.js
中将这个配置注入到 app
中其他的地方,只要通过 app.get
就可以达到共享的作用。
总结:
- express.js 只是暴露除了一个
createApplication
方法, 并且返回了一个app
对象 - 给app对象的原型做了相应的处理
- 给app 进行初始化设置
application.js
上面我们已经分析了 express.js
文件,知道其返回了一个 app
对象,但是我们至今位置没有看到哪里定义了 listen
和 get
方法。
我们在上面分析发现,执行了 mixin(app, proto, false);
方法,这个是在app 原型上去添加了另外一个原型,而 proto
指向的就是 application.js
文件, 下面我们就来具体分析这个文件。
listen
首先我们找到 listen 方法,其代码如下:
app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); }; 复制代码
这个就是我们在一开始说的,所有的Nodejs 后端框架都是基于 http 这个模块的实现的,所以这个里我们就已经实现了一个后端的服务。
get
在我们的demo 中,我们有调用一个 app.get
方法,其代码如下:
app.get('/', (req, res) => { const config = app.get('config') console.log(config) res.end('hello world') }) 复制代码
但是我们找遍了整个 application.js
文件,都没有找到这个方法在哪里实现的, get
只是 http
请求众多方法的其中一个, http方法,还有'post','put','delete'等一些列方法,为了简洁,express 引用了第三方库 methods , 这个库几乎涵盖了http 请求的常见方法,所以通过循环去给 app
挂载不同的方法(Koa 也是这样处理)
methods.forEach(function(method){ app[method] = function(path){ if (method === 'get' && arguments.length === 1) { // app.get(setting) return this.set(path); } this.lazyrouter(); var route = this._router.route(path); route[method].apply(route, slice.call(arguments, 1)); return this; }; }); 复制代码
首先 this.lazyrouter();
方法是去给 app
对象挂载一个 _router
的路由( Router )属性, 然后我们在看下 route
方法:
proto.route = function route(path) { var route = new Route(path); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; }; 复制代码
从上面的代码可知, var route = this._router.route(path);
, route
也就是this.stack 中的 layer
中的 reoute
, 所以最后回调函数是挂载在stack 的layer 上面的。
route[method].apply(route, slice.call(arguments, 1));
将 app.get
的回调函数挂载在 route
属性上面,其代码如下(删除异常处理代码):
methods.forEach(function(method){ Route.prototype[method] = function(){ var handles = flatten(slice.call(arguments)); for (var i = 0; i < handles.length; i++) { var handle = handles[i]; var layer = Layer('/', {}, handle); layer.method = method; this.methods[method] = true; this.stack.push(layer); } return this; }; }); 复制代码
我们先不具体分析代码逻辑, 我们可以根据上面的图片,分析下app对象下的一个数据结构:
- 在app 上面挂载一个
_router
属性, (router/index.js) - 在
_router
下面有一个stack
的属性,其是一个数组 -
stack
数组中,保存的都是一个Layer
类型的对象 -
Layer
对象中又挂载了一个route
(Route)的对象 -
route
对象保存了path
(path:/abc),methods
, 同样也有一个stack
的属性,也是一个数组, 同样里面保存的也是一个Layer
对象 -
Layer
里面挂载了一个重要的属性handle
, 其实从现在的分析看,这个handle
就是我们app.get
方法的第二个回调函数参数.
上面我们已经分析了 express
启动的过程,下面我们来分析访问服务 express
处理的过程,也就是我们访问 http://localhost:3000/
时, express
到底做了些什么.
访问服务
从一开始,我们就知道,对服务器的方法,首先都会进入 http.createServer
的回调函数,而且 express
是通过 listen
方法,执行这个方法的
app.listen = function listen() { var server = http.createServer(this); return server.listen.apply(server, arguments); }; 复制代码
其中 this
就是 app
实例,也就是在 express.js
文件中定义的,如下:
var app = function(req, res, next) { app.handle(req, res, next); }; 复制代码
在这个方法中,会调用 handle
方法,下面我们来分析下这个方法
handle
handle
的代码如下:
app.handle = function handle(req, res, callback) { var router = this._router; var done = callback || finalhandler(req, res, { env: this.get('env'), onerror: logerror.bind(this) }); if (!router) { debug('no routes defined on app'); done(); return; } router.handle(req, res, done); }; 复制代码
然后会执行 router.handle(req, res, done);
, 在上面我们已经得知, this._router
指向的是 router/index.js
这个文件夹的对象,下面我们进入到这个 handle
方法中, 这个方法很长,但是其实就是根据我们访问的路径来查找对应的 Layer
, 其关键代码是:
while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; ... } 复制代码
通过 matchLayer(layer, path);
去匹配layer. 找到 Layer
后,然后去执行 layer.handle_request(req, res, next);
Layer.prototype.handle_request = function handle(req, res, next) { var fn = this.handle; if (fn.length > 3) { // not a standard request handler return next(); } try { fn(req, res, next); } catch (err) { next(err); } }; 复制代码
var fn = this.handle;
这个 fn
其实指向的就是 app.get
里面的第二参数,也就是回调函数,
app.get('/', (req, res) => { res.end('hello world') }) 复制代码
然后就相当于请求完成了。
总结
上面我们已经分析了,Express 在启动的整个过程,主要是进行数据的一些加载处理和路由的处理,而且也分析了我们在请求Server时的整个过程。
后续我会继续分析 use 的用法,并且针对 express.static 源码来分析Express 中间件的处理和总结中间件的使用方式,以及 express.static
对缓存的处理( Etag
, Last-Modified
)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- libmodbus源码分析(3)从机(服务端)功能源码分析
- Eureka 源码(二):服务注册
- Dubbo源码解析之服务集群
- Eureka 源码剖析(五):服务下线
- Laravel 核心——IoC 服务容器源码解析(服务器解析)
- Laravel 核心——IoC 服务容器源码解析(服务器绑定)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员2010精华本
程序员杂志社 / 电子工业 / 2011-1 / 49.00元
《程序员(2010精华本)》主要内容:《程序员》创刊10年来,每年末编辑部精心打造的“合订本”已经形成一个品牌,得到广大读者的认可和喜爱。今年,《程序员》杂志内容再次进行了优化整合,除了每期推出的一个大型专题策划,各版块也纷纷以专题、策划的形式,将每月的重点进行了整合,让内容非常具有凝聚力,如专题篇、人物篇、实践篇等。另外杂志的版式、色彩方面也有了很大的飞跃,给读者带来耳目一新的阅读体验。一起来看看 《程序员2010精华本》 这本书的介绍吧!