express源码阅读
简介:这篇文章的主要目的是分析express的源码,但是网络上express的源码评析已经数不胜数,所以本文章另辟蹊径,准备仿制一个express的轮子,当然轮子的主体思路是阅读express源码所得。
源码地址:expross
1. 搭建结构
有了想法,下一步就是搭建一个山寨的框架,万事开头难,就从建立一个文件夹开始吧!
首先建立一个文件夹,叫做expross(你没有看错,山寨从名称开始)。
expross
|
|-- application.js
接着创建application.js文件,文件的内容就是官网的例子。
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
}).listen(3000);
一个简单的http服务就创建完成了,你可以在命令行中启动它,而expross框架的搭建就从这个文件出发。
1.1 第一划 Application
在实际开发过程中,web后台框架的两个核心点就是路由和模板。路由说白了就是一组URL的管理,根据前端访问的URL执行对应的处理函数。怎样管理一组URL和其对应的执行函数呢?首先想到的就是数组(其实我想到的是对象)。
创建一个名称叫做router的数组对象。
var http = require('http');
//路由
var router = [];
router.push({path: '*', fn: function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('404');
}}, {path: '/', fn: function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
}});
http.createServer(function(req, res) {
//自动匹配
for(var i=1,len=router.length; i<len; i++) {
if(req.url === router[i].path) {
return router[i].fn(req, res);
}
}
return router[0].fn(req, res);
}).listen(3000);
router数组用来管理所有的路由,数组的每个对象有两个属性组成,path
表示路径,fn
表示路径对应的执行函数。一切看起来都很不错,但是这并不是一个框架,为了组成一个框架,并且贴近express,这里继续对上面的代码进一步封装。
首先定义一个类:Application
var Application = function() {}
在这个类上定义二个函数:
Application.prototype.use = function(path, cb) {};
Application.prototype.listen = function(port) {};
把上面的实现,封装到这个类中。use
函数表示增加一个路由,listen
函数表示监听http服务器。
var http = require('http');
var Application = function() {
this.router = [{
path: '*',
fn: function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Cannot ' + req.method + ' ' + req.url);
}
}];
};
Application.prototype.use = function(path, cb) {
this.router.push({
path: path,
fn: cb
});
};
Application.prototype.listen = function(port) {
var self = this;
http.createServer(function(req, res) {
for(var i=1,len=self.router.length; i<len; i++) {
if(req.url === self.router[i].path) {
return self.router[i].fn(req, res);
}
}
return self.router[0].fn(req, res);
}).listen(port);
};
可以像下面这样启动它:
var app = new Application();
app.use('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
});
app.listen(3000);
看样子已经和express的外观很像了,为了更像,这里创建一个expross的文件,该文件用来实例化Application。代码如下:
var Application = require('./application');
exports = module.exports = createApplication;
function createApplication() {
var app = new Application();
return app;
}
为了更专业,调整目录结构如下:
-----expross
| |
| |-- index.js
| |
| |-- lib
| |
| |-- application.js
| |-- expross.js
|
|---- test.js
运行node test.js
,走起……
1.2 第二划 Layer
为了进一步优化代码,这里抽象出一个概念:Layer。代表层的含义,每一层就是上面代码中的router数组的一个项。
Layer含有两个成员变量,分别是path和handle,path代表路由的路径,handle代表路由的处理函数fn。
------------------------------------------------
| 0 | 1 | 2 | 3 |
------------------------------------------------
| Layer | Layer | Layer | Layer |
| |- path | |- path | |- path | |- path |
| |- handle| |- handle| |- handle| |- handle|
------------------------------------------------
router 内部
创建一个叫做layer的类,并为该类添加两个方法,handle_request
和match
。match
用来匹配请求路径是否符合该层,handle_request
用来执行路径对应的处理函数。
function Layer(path, fn) {
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.path = path;
}
//简单处理
Layer.prototype.handle_request = function (req, res) {
var fn = this.handle;
if(fn) {
fn(req, res);
}
}
//简单匹配
Layer.prototype.match = function (path) {
if(path === this.path) {
return true;
}
return false;
}
因为router数组中存放的将是Layer对象,所以修改Application.prototype.use
代码如下:
Application.prototype.use = function(path, cb) {
this.router.push(new Layer(path, cb));
};
当然也不要忘记Application
构造函数的修改。
var Application = function() {
this.router = [new Layer('*', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Cannot ' + req.method + ' ' + req.url);
})];
};
接着改变listen
函数,将其主要的处理逻辑抽取成handle
函数,用来匹配处理请求信息。这样可以让函数本身的语意更明确,并且遵守单一原则。
Application.prototype.handle = function(req, res) {
var self = this;
for(var i=0,len=self.router.length; i<len; i++) {
if(self.router[i].match(req.url)) {
return self.router[i].handle_request(req, res);
}
}
return self.router[0].handle_request(req, res);
};
listen
函数变得简单明了。
Application.prototype.listen = function(port) {
var self = this;
http.createServer(function(req, res) {
self.handle(req, res);
}).listen(port);
};
运行node test.js
,走起……
1.3 第三划 router
在Application
类中,成员变量router
负责存储应用程序的所有路由和其处理函数,既然存在这样一个对象,为何不将其封装成一个Router
类,这个类负责管理所有的路由,这样职责更加清晰,语意更利于理解。
so,这里抽象出另一个概念:Router,代表一个路由组件,包含若干层的信息。
建立Router类,并将原来Application
内的代码移动到Router类中。
var Router = function() {
this.stack = [new Layer('*', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Cannot ' + req.method + ' ' + req.url);
})];
};
Router.prototype.handle = function(req, res) {
var self = this;
for(var i=0,len=self.stack.length; i<len; i++) {
if(self.stack[i].match(req.url)) {
return self.stack[i].handle_request(req, res);
}
}
return self.stack[0].handle_request(req, res);
};
Router.prototype.use = function(path, fn) {
this.stack.push(new Layer(path, fn));
};
为了利于管理,现将路由相关的文件放到一个目录中,命名为router。将Router类文件命名为index.js保存到router文件夹内,并将原来的layer.js移动到该文件夹。现目录结构如下:
-----expross
| |
| |-- index.js
| |
| |-- lib
| |
| |-- router
| | |
| | |-- index.js
| | |-- layer.js
| |
| |
| |-- application.js
| |-- expross.js
|
|---- test.js
修改原有application.js文件,将代码原有router的数组移除,新增加_router对象,该对象是Router类的一个实例。
var Application = function() {
this._router = new Router();
};
Application.prototype.use = function(path, fn) {
var router = this._router;
return router.use(path, fn);
};
Application.prototype.handle = function(req, res) {
var router = this._router;
router.handle(req, res);
};
到现在为止,整体的框架思路已经非常的明确,一个应用对象包括一个路由组件,一个路由组件包括n个层,每个层包含路径和处理函数。每次请求就遍历应用程序指向的路由组件,通过层的成员函数match
来进行匹配识别URL访问的路径,如果成功则调用层的成员函数handle_request
进行处理。
运行node test.js
,走起……
1.4 第四划 route
如果研究过路由相关的知识就会发现,路由其实是由三个参数构成的:请求的URI、HTTP请求方法和路由处理函数。之前的代码只处理了其中两种,对于HTTP请求方法这个参数却刻意忽略,现在是时候把它加进来了。
按照上面的结构,如果加入请求方法参数,肯定会加入到Layer里面。但是再加入之前,需要仔细分析一下路由的常见方式:
GET /pages
GET /pages/1
POST /page
PUT /pages/1
DELETE /pages/1
HTTP的请求方法有很多,上面的路由列表是一组常见的路由样式,遵循REST原则。分析一下会发现大部分的请求路径其实是相似或者是一致的,如果将每个路由都建立一个Layer添加到Router里面,从效率或者语意上都稍微有些不符,因为他们是一组URL,负责管理page相关信息的URL,能否把这样类似访问路径相同而请求方法不同的路由划分到一个组里面呢?
答案是可以行的,这就需要再次引入一个概念:route,专门来管理具体的路由信息。
------------------------------------------------
| 0 | 1 | 2 | 3 |
------------------------------------------------
| item | item | item | item |
| |- method| |- method| |- method| |- method|
| |- handle| |- handle| |- handle| |- handle|
------------------------------------------------
route 内部
在写代码之前,先梳理一下上面所有的概念之间的关系:application、expross、router、route和layer。
--------------
| Application | ---------------------------------------------------------
| | | ----- ----------- | 0 | 1 | 2 | 3 | ... |
| |-router | ----> | | Layer | ---------------------------------------------------------
-------------- | 0 | |-path | | item | item | item | item | |
application | | |-route | ----> | |- method| |- method| |- method| |- method| ... |
|-----|-----------| | |- handle| |- handle| |- handle| |- handle| |
| | Layer | ---------------------------------------------------------
| 1 | |-path | route
| | |-route |
|-----|-----------|
| | Layer |
| 2 | |-path |
| | |-route |
|-----|-----------|
| ... | ... |
----- -----------
router
application代表一个应用程序。expross是一个工厂类负责创建application对象。router是一个路由组件,负责整个应用程序的路由系统。route是路由组件内部的一部分,负责存储真正的路由信息,内部的每一项都代表一个路由处理函数。router内部的每一项都是一个layer对象,layer内部保存一个route和其代表的URI。
如果一个请求来临,会现从头至尾的扫描router内部的每一层,而处理每层的时候会先对比URI,匹配扫描route的每一项,匹配成功则返回具体的信息,没有任何匹配则返回未找到。
创建Route类,定义三个成员变量和三个方法。path代表该route所对应的URI,stack代表上图中route内部item所在的数组,methods用来快速判断该route中是是否存在某种HTTP请求方法。
var Route = function(path) {
this.path = path;
this.stack = [];
this.methods = {};
};
Route.prototype._handles_method = function(method) {
var name = method.toLowerCase();
return Boolean(this.methods[name]);
};
Route.prototype.get = function(fn) {
var layer = new Layer('/', fn);
layer.method = 'get';
this.methods['get'] = true;
this.stack.push(layer);
return this;
};
Route.prototype.dispatch = function(req, res) {
var self = this,
method = req.method.toLowerCase();
for(var i=0,len=self.stack.length; i<len; i++) {
if(method === self.stack[i].method) {
return self.stack[i].handle_request(req, res);
}
}
};
在上面的代码中,并没有定义前面结构图中的item对象,而是使用了Layer对象进行替代,主要是为了方便快捷,从另一种角度看,其实二者是存在很多共同点的。另外,为了利于理解,代码中只实现了GET方法,其他方法的代码实现是类似的。
既然有了Route类,接下来就改修改原有的Router类,将route集成其中。
Router.prototype.handle = function(req, res) {
var self = this,
method = req.method;
for(var i=0,len=self.stack.length; i<len; i++) {
if(self.stack[i].match(req.url) &&
self.stack[i].route && self.stack[i].route._handles_method(method)) {
return self.stack[i].handle_request(req, res);
}
}
return self.stack[0].handle_request(req, res);
};
Router.prototype.get = function(path, fn) {
var route = this.route(path);
route.get(fn);
return this;
};
Router.prototype.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, function(req, res) {
route.dispatch(req, res)
});
layer.route = route;
this.stack.push(layer);
return route;
};
代码中,暂时去除use方法,创建get方法用来添加请求处理函数,route方法是为了返回一个新的Route对象,并将改层加入到router内部。
最后修改Application类中的函数,去除use方法,加入get方法进行测试。
Application.prototype.get = function(path, fn) {
var router = this._router;
return router.get(path, fn);
};
Application.prototype.route = function (path) {
return this._router.route(path);
};
测试代码如下:
var expross = require('./expross');
var app = expross();
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World');
});
app.route('/book')
.get(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Get a random book');
});
app.listen(3000);
运行node test.js
,走起……
1.5 第五划 next
next 主要负责流程控制。在实际的代码中,有很多种情况都需要进行权限控制,例如:
var expross = require('./expross');
var app = expross();
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('first');
});
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('second');
});
app.listen(3000);
上面的代码如果执行会发现永远都返回first
,但是有的时候会根据前台传来的参数动态判断是否执行接下来的路由,怎样才能跳过first
进入second
?express
引入了next
的概念。
跳转到任意layer,成本是比较高的,大多数的情况下并不需要。在express中,next跳转函数,有两种类型:
跳转到下一个处理函数。执行
next()
。跳转到下一组route。执行
next('route')
。
要想使用next的功能,需要在代码书写的时候加入该参数:
var expross = require('./expross');
var app = expross();
app.get('/', function(req, res, next) {
console.log('first');
next();
});
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('second');
});
app.listen(3000);
而该功能的实现也非常简单,主要是在调用处理函数的时候,除了需要传入req、res之外,再传一个流程控制函数next。
Router.prototype.handle = function(req, res) {
var self = this,
method = req.method,
i = 1, len = self.stack.length,
stack;
function next() {
if(i >= len) {
return self.stack[0].handle_request(req, res);
}
stack = self.stack[i++];
if(stack.match(req.url) && stack.route
&& stack.route._handles_method(method)) {
return stack.handle_request(req, res, next);
} else {
next();
}
}
next();
};
修改原有Router的handle函数。因为要控制流程,所以for循环并不是很合适,可以更换为while循环,或者干脆使用类似递归的手法。
代码中定义一个next函数,然后执行next函数进行自启动。next内部和之前的操作类似,主要是执行handle_request函数进行处理,不同之处是调用该函数的时候,将next本身当做参数传入,这样可以在内部执行该函数进行下一个处理,类似给handle_request赋予for循环中++的能力。
按照相同的方式,修改Route的dispatch函数。
Route.prototype.dispatch = function(req, res, done) {
var self = this,
method = req.method.toLowerCase(),
i = 0, len = self.stack.length, stack;
function next(gt) {
if(gt === 'route') {
return done();
}
if(i >= len) {
return done();
}
stack = self.stack[i++];
if(method === stack.method) {
return stack.handle_request(req, res, next);
} else {
next();
}
}
next();
};
代码思路基本和上面的相同,唯一的差别就是增加route判断,提供跳过当前整组处理函数的能力。
Layer.prototype.handle_request = function (req, res, next) {
var fn = this.handle;
if(fn) {
fn(req, res, next);
}
}
Router.prototype.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, function(req, res, next) {
route.dispatch(req, res, next)
});
layer.route = route;
this.stack.push(layer);
return route;
};
最后不要忘记修改Layer的handle_request函数和Router的route函数。
1.6 后记
该小结基本结束,当然如果要继续还可以写很多内容,包括错误处理、函数重载、高阶函数(生成各种HTTP函数),以及各种神奇的用法,如继承、缓存、复用等等。
但是我觉得搭建结构这一结已经将express的基本结构捋清了,如果重头到尾的走下来,再去读框架的源码应该是没有问题的。
接下来继续山寨express 的其他部分。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。