内容简介:关于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 服务容器源码解析(服务器绑定)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Four
Scott Galloway / Portfolio / 2017-10-3 / USD 28.00
NEW YORK TIMES BESTSELLER USA TODAY BESTSELLER Amazon, Apple, Facebook, and Google are the four most influential companies on the planet. Just about everyone thinks they know how they got there.......一起来看看 《The Four》 这本书的介绍吧!