• Application.use()
  • Application.router()
  • express核心源码模拟

一、express.use()

1.1app.use([path,] callback [, callback ...])

通过语法结构可以看到Application.use()参数分别有以下几种情形:

app.use(function(){...}); //给全局添加一个中间件
app.use(path,function(){...}); //给指定路由path添加一个中间件
app.use(function(){...}, function(){...}, ...); //给全局添加n个中间件
app.use(path,function(){...},function(){...}, ...); //给指定路由path添加n个中间件

关于path最简单也是最常用的就是字符串类型(例:‘/abcd’);除了字符串Express还提供了模板和正则格式(例:'/abc?d', '/ab+cd', '/ab\*cd', '/a(bc)?d', '/\/abc|\/xyz/');除了单个的字符串和模板还可以将多个path作为一个数组的元素,然后将这个数组作为use的path,这样就可以同时给多个路由添加中间件,详细内容可以参考官方文档:https://www.expressjs.com.cn/4x/api.html#path-examples。

关于callbakc多个或单个中间件程序这已经再语法结构中直观的体现出来了,这里重点来看看回调函数的参数:

app.use(function(req,res,next){...}); //必须提供的参数
app.use(function(err,req,res,next){...}); //错误中间件需要在最前面添加一个错误参数

关于中间件的简单应用:

let express = require('./express');
let app = express();
app.use('/',function(req,res,next){
console.log("我是一个全局中间件");
next(); //每个中间件的最末尾必须调用next
}); app.use('/',function(err,req,res,next){
console.log("我是一个全局错误中间,当发生错误是调用")
console.error(err.stack);
res.status(500).send('服务出错误了!');
//由于这个错误处理直接响应了客户端,可以不再调用next,当然后面还需要处理一些业务的话也是可以调用next的
});

1.2简单的模拟Express源码实现Appliction.use()以及各个请求方法的响应注册方法(这里个源码模拟路由概念还比较模糊,所以使用请求方法的响应注册API,而没有使用路由描述):

 //文件结构
express
index.js
//源码模拟实现
let http = require("http");
let url = require('url');
function createApplication(){
//app是一个监听函数
let app = (req,res) =>{
//取出每一个层
//1.获取请求的方法
let m = req.method.toLowerCase();
let {pathname} = url.parse(req.url,true); //通过next方法进行迭代
let index = 0;
function next(err){
//如果routes迭代完成还没有找到,说明路径不存在
if(index === app.routes.length) return res.end(`Cannot ${m} ${pathname}`);
let {method, path, handler} = app.routes[index++];//每次调用next就应该取下一个layer
if(err){
if(handler.length === 4){
handler(err,req,res,next);
}else{
next(err);
}
}else{
if(method === 'middle'){ //处理中间件
if(path === '/' || path === pathname || pathname.startsWith(path+'/')){
handler(req,res,next);
}else{
next();//如果这个中间件没有匹配到,继续通过next迭代路由容器routes
}
}else{ //处理路由
if( (method === m || method ==='all') && (path === pathname || path === '*')){ //匹配请求方法和请求路径(接口)
handler(req,res);//匹配成功后执行的Callback
}else{
next();
}
}
}
}
next();
}
app.routes = [];//路由容器
app.use = function(path,handler){
if(typeof handler !== 'function'){
handler = path;
path = '/';
}
let layer = {
method:'middle', //method是middle就表示它是一个中间件
path,
handler
}
app.routes.push(layer);
}
app.all = function(path,handler){
let layer = {
method:'all',
path,
handler
}
app.routes.push(layer);
}
console.log(http.METHODS);
http.METHODS.forEach(method =>{
method = method.toLocaleLowerCase();
app[method] = function (path,handler){//批量生成各个请求方法的路由注册方法
let layer = {
method,
path,
handler
}
app.routes.push(layer);
}
});
//內置中间件,给req扩展path、qury属性
app.use(function(req,res,next){
let {pathname,query} = url.parse(req.url,true);
let hostname = req.headers['host'].split(':')[0];
req.path = pathname;
req.query = query;
req.hostname = hostname;
next();
});
//通过app.listen调用http.createServer()挂在app(),启动express服务
app.listen = function(){
let server = http.createServer(app);
server.listen(...arguments);
}
return app;
}
module.exports = createApplication;

测试模拟实现的Express:

 let express = require('./express');

 let app = express();

 app.use('/',function(req,res,next){
console.log("我是一个全局中间件");
next();
});
app.use('/user',function(req,res,next){
console.log("我是user接口的中间件");
next();
});
app.get('/name',function(req,res){
console.log(req.path,req.query,req.hostname);
res.end('zfpx');
});
app.post('/name',function(req,res){
res.end('post name');
}); app.all("*",function(req,res){
res.end('all');
}); app.use(function(err,req,res,next){
console.log(err);
next();
}); app.listen(12306);

在windows系统下测试请求:

关于源码的构建详细内容可以参考这个视频教程:app.use()模拟构建视频教程,前面就已经说明过这个模式实现仅仅是从表面的业务逻辑,虽然有一点底层的雏形,但与源码还是相差甚远,这一部分也仅仅只是想帮助理解Express采用最简单的方式表现出来。

1.3如果你看过上面的源码或自己也实现过,就会发现Express关于中间件的添加方式除了app.use()还有app.all()及app.METHOD()。在模拟源码中我并未就use和all的差异做处理,都是采用了请求路径绝对等于path,这种方式是all的特性,use的path实际表示为请求路径的开头:

app.use(path,callback):path表示请求路径的开头部分。

app.all(path,callback):paht表示完全等于请求路径。

app.METHOD(path,callback):并不是真的有METHOD这个方法,而是指HTTP请求方法,实际上表示的是app.get()、app.post()、app.put()等方法,而有时候我们会将这些方法说成用来注册路由,这是因为路由注册的确使用这些方法,但同时这些方法也是可以用作中间的添加,这在前一篇博客中的功能解析中就有说明(Express4.x之API:express),详细见过后面的路由解析就会更加明了。

二、express.router()

2.1在实例化一个Application时会实例化一个express.router()实例并被添加到app._router属性上,实际上这个app使用的use、all、METHOD时都是在底层调用了该Router实例上对应的方法,比如看下面这些示例:

 let express = require("express");
let app = express(); app._router.use(function(req,res,next){
console.log("--app.router--");
next();
}); app._router.post("/csJSON",function(req,res,next){
res.writeHead(200);
res.write(JSON.stringify(req.body));
res.end();
}); app.listen(12306);

上面示例中的app._router.use、app._router.post分别同等与app.use、app.post,这里到这里也就说明了上一篇博客中的路由与Application的关系Express4.x之API:express

2.2Express中的Router除了为Express.Application提供路由功能以外,Express也将它作为一个独立的路由工具分离了出来,也就是说Router自身可以独立作为一个应用,如果我们在实际应用中有相关业务有类似Express.Application的路由需求,可以直接实例化一个Router使用,应用的方式如下:

let express = require('/express');
let router = express.Router();
//这部分可以详细参考官方文档有详细的介绍

2.3由于这篇博客主要是分析Express的中间件及路由的底层逻辑,所以就不在这里详细介绍某个模块的应用,如果有时间我再写一篇关于Router模块的应用,这里我直接上一份模拟Express路由的代码以供参考:

文件结构:

express //根路径
index.js //express主入口文件
application.js //express应用构造模块
router //路由路径
index.js //路由主入口文件
layer.js //构造层的模块
route.js //子路由模块

Express路由系统的逻辑结构图:

模拟代码(express核心源码模拟):

 //express主入口文件
let Application = require('./application.js'); function createApplication(){
return new Application();
} module.exports = createApplication;

index.js //express主入口文件

 //用来创建应用app
let http = require('http');
let url = require('url'); //导入路由系统模块
let Router = require('./router'); const methods = http.METHODS; //Application ---- express的应用系统
function Application(){
//创建一个內置的路由系统
this._router = new Router();
} //app.get ---- 实现路由注册业务
// Application.prototype.get = function(path,...handlers){
// this._router.get(path,'use',handlers);
// } methods.forEach(method => {
method = method.toLocaleLowerCase();
Application.prototype[method] = function(path,...handlers){
this._router[method](path,handlers);
}
}); //app.use ---- 实现中间件注册业务
//这里模拟处理三种参数模式:
// -- 1个回调函数:callback
// -- 多个回调函数:[callback,] callback [,callback...]
// -- 指定路由的中间件:[path,] callback [,callback...]
// -- 注意源码中可以处理这三种参数形式还可以处理上面数据的数组形式,以及其他Application(直接将其他app上的中间件添加到当前应用上)
Application.prototype.use = function(fn){
let path = '/';
let fns = [];
let arg = [].slice.call(arguments);
if(typeof fn !== 'function' && arg.length >= 2){
if(typeof arg[0] !== 'string'){
fns = arg;
}else{
path = arg[0];
fns = arg.slice(1);
}
}else{
fns = arg;
}
this._router.use(path,'use',fns);
} Application.prototype.all = function(fn){
let path = '/';
let fns = [];
let arg = [].slice.call(arguments);
if(typeof fn !== 'function' && arg.length >= 2){
if(typeof arg[0] !== 'string'){
fns = arg;
}else{
path = arg[0];
fns = arg.slice(1);
}
}else{
fns = arg;
}
this._router.use(path,'all',fns);
} //将http的listen方法封装到Application的原型上
Application.prototype.listen = function(){
let server = http.createServer((req,res)=>{
//done 用于当路由无任何可匹配项时调用的处理函数
function done(){
res.end(`Cannot ${req.url} ${req.method}`);
}
this._router.handle(req,res,done); //调用路由系统的handle方法处理请求
});
server.listen(...arguments);
}; module.exports = Application;

application.js //express应用构造模块

 //express路由系统
const Layer = require('./layer.js');
const Route = require('./route.js'); const http = require('http');
const methods = http.METHODS; const url = require('url'); //路由对象构造函数
function Router(){
this.stack = [];
} //router.route ---- 用于创建子路由对象route与主路由上层(layer)的关系
//并将主路由上的层缓存到路由对象的stack容器中,该层建立路径与子路由处理请求的关系
Router.prototype.route = function(path){
let route = new Route();
let layer = new Layer(path,route.dispatch.bind(route));
this.stack.push(layer);
return route;
} //router.get ---- 实现路由注册
//实际上这个方法调用router.route方法分别创建一个主路由系统层、一个子路由系统,并建立两者之间的关系,详细见Router.prototype.route
//然后获取子路由系统对象,并将回调函数和请求方法注册在这个子路由系统上 // Router.prototype.get = function(path,handlers){
// let route = this.route(path);
// route.get(handlers);
// } methods.forEach(method =>{
method = method.toLocaleLowerCase();
//注意下面这个方法会出现内存泄漏问题,有待改进
Router.prototype[method] = function(path, handlers){
let route = this.route(path);
route[method](handlers);
}
}); //router.use ---- 实现中间件注册(按照路由开头的路径匹配,即相对路由匹配)
Router.prototype.use = function(path,routerType,fns){
let router = this;
fns.forEach(function(fn){
let layer = new Layer(path,fn);
layer.middle = true; //标记这个层为相对路由中间件
layer.routerType = routerType;
router.stack.push(layer);
});
} //调用路由处理请求
Router.prototype.handle = function(req,res,out){
let {pathname} = url.parse(req.url);
let index = 0;
let next = () => {
if(index >= this.stack.length) return out();
let layer = this.stack[index++];
if(layer.middle && (layer.path === '/' || pathname === layer.path || pathname.startsWith(layer.path + '/'))){
//处理中间件
if(layer.routerType === 'use'){
layer.handle_request(req,res,next);
}else if(layer.routerType === 'all' && layer.path === pathname){
layer.handle_request(req,res,next);
}else{
next();
}
}else if(layer.match(pathname)){
//处理响应--更准确的说是处理具体请求方法上的中间件或响应
layer.handle_request(req,res,next);
}else{
next();
}
}
next();
} module.exports = Router;

index.js //路由主入口文件

 //Layer的构造函数
function Layer(path,handler){
this.path = path; //当前层的路径
this.handler = handler; //当前层的回调函数
} //判断请求方法与当前层的方法是否一致
Layer.prototype.match = function(pathname){
return this.path === pathname;
} //调用当前层的回调函数handler
Layer.prototype.handle_request = function(req,res,next){
this.handler(req,res,next);
} module.exports = Layer;

layer.js //构造层的模块

 //Layer的构造函数
function Layer(path,handler){
this.path = path; //当前层的路径
this.handler = handler; //当前层的回调函数
} //判断请求方法与当前层的方法是否一致
Layer.prototype.match = function(pathname){
return this.path === pathname;
} //调用当前层的回调函数handler
Layer.prototype.handle_request = function(req,res,next){
this.handler(req,res,next);
} module.exports = Layer;

route.js //子路由模块

测试代码:

 let express = require('./express');
let app = express(); app.use('/name',function(req,res,next){
console.log('use1');
next();
});
app.use(function(req,res,next){
console.log('use2-1');
next();
},function(req,res,next){
console.log('use2-2');
next();
}); app.all('/name',function(req,res,next){
console.log('all-1');
next();
});
app.all('/name/app',function(req,res,next){
console.log('all-2');
next();
}); app.get('/name/app',function(req,res){
res.end(req.url);
});
// console.log(app._router.stack);
app.listen(12306);

测试结果:

Express4.x之中间件与路由详解及源码分析的更多相关文章

  1. Android应用AsyncTask处理机制详解及源码分析

    1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...

  2. Java SPI机制实战详解及源码分析

    背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ...

  3. Spring Boot启动命令参数详解及源码分析

    使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...

  4. 【转载】Android应用AsyncTask处理机制详解及源码分析

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个 ...

  5. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  6. SpringMVC异常处理机制详解[附带源码分析]

    目录 前言 重要接口和类介绍 HandlerExceptionResolver接口 AbstractHandlerExceptionResolver抽象类 AbstractHandlerMethodE ...

  7. Hadoop RCFile存储格式详解(源码分析、代码示例)

    RCFile   RCFile全称Record Columnar File,列式记录文件,是一种类似于SequenceFile的键值对(Key/Value Pairs)数据文件.   关键词:Reco ...

  8. ArrayList用法详解与源码分析

    说明 此文章分两部分,1.ArrayList用法.2.源码分析.先用法后分析是为了以后忘了查阅起来方便-- ArrayList 基本用法 1.创建ArrayList对象 //创建默认容量的数组列表(默 ...

  9. [转]SpringMVC拦截器详解[附带源码分析]

      目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:ht ...

随机推荐

  1. JS中的bind 、call 、apply

    # 一 .bind 特点: ### 1.返回原函数的拷贝,我们称这个拷贝的函数为绑定函数 ### 2.将函数中的this固定为调用bind方法时的第一个参数,所以称之为绑定函数.注意是名词而非动词. ...

  2. idea的生成类注释和方法注释

    sttings中选择 类注释 /** * @program: ${PROJECT_NAME} * * @description: ${description} * * @author: xiaozha ...

  3. docker的镜像加速

    docker加速配置 1,阿里云镜像加速 1.登录:https://dev.aliyun.com/search.html 2.登录阿里云 搜索   容器镜像服务  找到后如下图 ‘ 您可以通过修改da ...

  4. ATX-UI自动化环境搭建

    基础环境准备(以下都是在Mac机上搭建的) 1.android sdk安装&配置 很完美的一个资源下载网:tools.android-studio.org,下载所需的包(我下的zip包直接解压 ...

  5. Autofac依赖注入

    简介 Autofac 是一款超赞的.NET IoC 容器 . 它管理类之间的依赖关系, 从而使 应用在规模及复杂性增长的情况下依然可以轻易地修改 .它的实现方式是将常规的.net类当做 组件 处理. ...

  6. jchdl - GSL实例 - Mux4

    https://mp.weixin.qq.com/s/hh0eExVFC6cxzpvNI1cA9A 使用门实现四选一选择器. 原理图 ​​ 参考链接 https://github.com/wjcdx/ ...

  7. CSS3新增伪类有那些?

    p:first-of-type 选择属于其父元素的首个元素 p:last-of-type 选择属于其父元素的最后元素 p:only-of-type 选择属于其父元素唯一的元素 p:only-child ...

  8. Java 第十一届 蓝桥杯 省模拟赛 19000互质的个数

    问题描述 不超过19000的正整数中,与19000互质的数的个数是多少? 答案提交 这是一道结果填空的题,你只需要算出结果后提交即可.本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将 ...

  9. Java实现蓝桥杯日期问题

    历届试题 日期问题 时间限制:1.0s 内存限制:256.0MB 提交此题 问题描述 小明正在整理一批历史文献.这些历史文献中出现了很多日期.小明知道这些日期都在1960年1月1日至2059年12月3 ...

  10. Java实现 LeetCode 581 最短无序连续子数组(从两遍搜索找两个指针)

    581. 最短无序连续子数组 给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序. 你找到的子数组应是最短的,请输出它的长度. 示例 1: 输入: ...