express源码阅读

express源码阅读

简介:这篇文章的主要目的是分析express的源码,可是网络上express的源码评析已经数不胜数,因此本文章另辟蹊径,准备仿制一个express的轮子,固然轮子的主体思路是阅读express源码所得。javascript

源码地址:expross前端

1. 搭建结构

有了想法,下一步就是搭建一个山寨的框架,万事开头难,就从创建一个文件夹开始吧!java

首先创建一个文件夹,叫作expross(你没有看错,山寨从名称开始)。node

expross
 |
 |-- application.js

接着建立application.js文件,文件的内容就是官网的例子。git

var http = require('http');

http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World');
}).listen(3000);

一个简单的http服务就建立完成了,你能够在命令行中启动它,而expross框架的搭建就从这个文件出发。github

1.1 第一划 Application

在实际开发过程当中,web后台框架的两个核心点就是路由和模板。路由说白了就是一组URL的管理,根据前端访问的URL执行对应的处理函数。怎样管理一组URL和其对应的执行函数呢?首先想到的就是数组(其实我想到的是对象)。web

建立一个名称叫作router的数组对象。express

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_requestmatchmatch用来匹配请求路径是否符合该层,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进入secondexpress引入了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 的其余部分。

相关文章
相关标签/搜索