上一篇文章咱们讲了怎么用Node.js原生API来写一个web服务器,尽管代码比拟丑,然而基本功能还是有的。然而个别咱们不会间接用原生API来写,而是借助框架来做,比方本文要讲的Express。通过上一篇文章的铺垫,咱们能够猜想,Express其实也没有什么黑魔法,也仅仅是原生API的封装,次要是用来提供更好的扩展性,应用起来更不便,代码更优雅。本文照例会从Express的根本应用动手,而后本人手写一个Express来代替他,也就是源码解析。

本文可运行代码曾经上传GitHub,拿下来一边玩代码,一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

简略示例

应用Express搭建一个最简略的Hello World也是几行代码就能够搞定,上面这个例子起源官网文档:

const express = require('express');const app = express();const port = 3000;app.get('/', (req, res) => {  res.send('Hello World!');});app.listen(port, () => {  console.log(`Example app listening at http://localhost:${port}`);});

能够看到Express的路由能够间接用app.get这种办法来解决,比咱们之前在http.createServer外面写一堆if优雅多了。咱们用这种形式来改写下上一篇文章的代码:

const path = require("path");const express = require("express");const fs = require("fs");const url = require("url");const app = express();const port = 3000;app.get("/", (req, res) => {  res.end("Hello World");});app.get("/api/users", (req, res) => {  const resData = [    {      id: 1,      name: "小明",      age: 18,    },    {      id: 2,      name: "小红",      age: 19,    },  ];  res.setHeader("Content-Type", "application/json");  res.end(JSON.stringify(resData));});app.post("/api/users", (req, res) => {  let postData = "";  req.on("data", (chunk) => {    postData = postData + chunk;  });  req.on("end", () => {    // 数据传完后往db.txt插入内容    fs.appendFile(path.join(__dirname, "db.txt"), postData, () => {      res.end(postData); // 数据写完后将数据再次返回    });  });});app.listen(port, () => {  console.log(`Server is running on http://localhost:${port}/`);});

Express还反对中间件,咱们写个中间件来打印出每次申请的门路:

app.use((req, res, next) => {  const urlObject = url.parse(req.url);  const { pathname } = urlObject;  console.log(`request path: ${pathname}`);  next();});

Express也反对动态资源托管,不过他的API是须要指定一个文件夹来独自寄存动态资源的,比方咱们新建一个public文件夹来寄存动态资源,应用express.static中间件配置一下就行:

app.use(express.static(path.join(__dirname, 'public')));

而后就能够拿到动态资源了:

手写源码

手写源码才是本文的重点,后面的不过是铺垫,本文手写的指标就是本人写一个express来替换后面用到的express api,其实就是源码解析。在开始之前,咱们先来看看用到了哪些API

  1. express(),第一个必定是express函数,这个运行后会返回一个app的实例,前面用的很多办法都是这个app上的。
  2. app.listen,这个办法相似于原生的server.listen,用来启动服务器。
  3. app.get,这是解决路由的API,相似的还有app.post等。
  4. app.use,这是中间件的调用入口,所有中间件都要通过这个办法来调用。
  5. express.static,这个中间件帮忙咱们做动态资源托管,其实是另外一个库了,叫serve-static,因为跟Express架构关系不大,本文就先不讲他的源码了。

本文所有手写代码全副参照官网源码写成,办法名和变量名尽量与官网保持一致,大家能够对照着看,写到具体的办法时我也会贴出官网源码的地址。

express()

首先须要写的必定是express(),这个办法是所有的开始,他会创立并返回一个app,这个app就是咱们的web服务器

// express.jsvar mixin = require('merge-descriptors');var proto = require('./application');// 创立web服务器的办法function createApplication() {  // 这个app办法其实就是传给http.createServer的回调函数  var app = function (req, res) {  };  mixin(app, proto, false);  return app;}exports = module.exports = createApplication;

上述代码就是咱们在运行express()的时候执行的代码,其实就是个空壳,返回的app临时是个空函数,真正的app并没在这里,而是在proto上,从上述代码能够看出proto其实就是application.js,而后通过上面这行代码将proto上的货色都赋值给了app

mixin(app, proto, false);

这行代码用到了一个第三方库merge-descriptors,这个库总共没有几行代码,做的事件也很简略,就是将proto下面的属性挨个赋值给app,对merge-descriptors源码感兴趣的能够看这里:https://github.com/component/merge-descriptors/blob/master/index.js。

Express这里之所以应用mixin,而不是一般的面向对象来继承,是因为它除了要mixin proto外,还须要mixin其余库,也就是须要多继承,我这里省略了,然而官网源码是有的。

express.js对应的源码看这里:https://github.com/expressjs/express/blob/master/lib/express.js

app.listen

下面说了,express.js只是一个空壳,真正的appapplication.js外面,所以app.listen也是在这里。

// application.jsvar app = exports = module.exports = {};app.listen = function listen() {  var server = http.createServer(this);  return server.listen.apply(server, arguments);};

下面代码就是调用原生http模块创立了一个服务器,然而传的参数是this,这里的this是什么呢?回忆一下咱们应用express的时候是这样用的:

const app = express();app.listen(3000);

所以listen办法的理论调用者是express()的返回值,也就是下面express.js外面createApplication的返回值,也就是这个函数:

var app = function (req, res) {};

所以这里的this也是这个函数,所以我在express.js外面就加了正文,这个函数是http.createServer的回调函数。当初这个函数是空的,实际上他应该是整个web服务器的解决入口,所以咱们给他加上解决的逻辑,在外面再加一行代码:

var app = function(req, res) {  app.handle(req, res);    // 这是真正的服务器解决入口};

app.handle

app.handle也是挂载在app上面的,所以他理论也在application.js这个文件外面,上面咱们来看看他干了什么:

app.handle = function handle(req, res) {  var router = this._router;  // 最终的解决办法  var done = finalhandler(req, res);  // 如果没有定义router  // 间接完结返回  if (!router) {    done();    return;  }  // 有router,就用router来解决  router.handle(req, res, done);}

下面代码能够看出,理论解决路由的是router,这是Router的一个实例,并且挂载在this上的,咱们这里还没有给他赋值,如果没有赋值的话,会间接运行finalhandler并且完结解决。finalhandler也是一个第三方库,GitHub链接在这里:https://github.com/pillarjs/finalhandler。这个库的性能也不简单,就是帮你解决一些收尾的工作,比方所有路由都没匹配上,你可能须要返回404并记录下error log,这个库就能够帮你做。

app.get

下面说了,在具体解决网络申请时,实际上是用app._router来解决的,那么app._router是在哪里赋值的呢?事实上app._router的赋值有多个中央,一个中央就是HTTP动词解决办法上,比方咱们用到的app.get或者app.post。无论是app.get还是app.post都是调用的router办法来解决,所以能够对立用一个循环来写这一类的办法。

// HTTP动词的办法var methods = ['get', 'post'];methods.forEach(function (method) {  app[method] = function (path) {    this.lazyrouter();    var route = this._router.route(path);    route[method].apply(route, Array.prototype.slice.call(arguments, 1));    return this;  }});

下面代码HTTP动词都放到了一个数组外面,官网源码中这个数组也是一个第三方库保护的,名字就叫methods,GitHub地址在这里:https://github.com/jshttp/methods。我这个例子因为只须要两个动词,就简化了,间接用数组了。这段代码其实给app创立了跟每个动词同名的函数,所有动词的处理函数都是一样的,都是去调router外面的对应办法来解决。这种将不同局部抽取进去,从而复用独特局部的代码,有点像我之前另一篇文章写过的设计模式----享元模式。

咱们留神到下面代码除了调用router来解决路由外,还有一行代码:

this.lazyrouter();

lazyrouter办法其实就是咱们给this._router赋值的中央,代码也比较简单,就是检测下有没有_router,如果没有就给他赋个值,赋的值就是Router的一个实例:

app.lazyrouter = function lazyrouter() {  if (!this._router) {    this._router = new Router();  }}

app.listenapp.handlemethods解决办法都在application.js外面,application.js源码在这里:https://github.com/expressjs/express/blob/master/lib/application.js

Router

写到这里咱们发现咱们曾经应用了Router的多个API,比方:

  1. router.handle
  2. router.route
  3. route[method]

所以咱们来看下Router这个类,上面的代码是从源码中简化进去的:

// router/index.jsvar setPrototypeOf = require('setprototypeof');var proto = module.exports = function () {  function router(req, res, next) {    router.handle(req, res, next);  }  setPrototypeOf(router, proto);  return router;}

这段代码对我来说是比拟奇怪的,咱们在执行new Router()的时候其实执行的是new proto()new proto()并不是我奇怪的中央,奇怪的是他设置原型的形式。我之前在讲JS的面向对象的文章提到过如果你要给一个类加上类办法能够这样写:

function Class() {}Class.prototype.method1 = function() {}var instance = new Class();

这样instance.__proto__就会指向Class.prototype,你就可应用instance.method1了。

Express.js的上述代码其实也是实现了相似的成果,setprototypeof又是一个第三方库,作用相似Object.setPrototypeOf(obj, prototype),就是给一个对象设置原型,setprototypeof存在的意义就是兼容老规范的JS,也就是加了一些polyfill,他的代码在这里。所以:

setPrototypeOf(router, proto);

这行代码的意思就是让router.__proto__指向protorouter是你在new proto()时的返回对象,执行了下面这行代码,这个router就能够拿到proto上的全副办法了。像router.handle这种办法就能够挂载到proto上了,成为proto.handle

绕了一大圈,其实就是JS面向对象的应用,给router增加类办法,然而为什么应用这么绕的形式,而不是像我下面那个Class那样用呢?这我就不是很分明了,可能有什么历史起因吧。

路由架构

Router的根本构造晓得了,要了解Router的具体代码,咱们还须要对Express的路由架构有一个整体的意识。就以咱们这两个示例API来说:

get /api/users

post /api/users

咱们发现他们的path是一样的,都是/api/users,然而他们的申请办法,也就是method不一样。Express外面将path这一层提取进去作为了一个类,叫做Layer。然而对于一个Layer,咱们只晓得他的path,不晓得method的话,是不能确定一个路由的,所以Layer上还增加了一个属性route,这个route上也存了一个数组,数组的每个项存了对应的method和回调函数handle。整个构造你能够了解成这个样子:

const router = {  stack: [    // 外面很多layer    {      path: '/api/users'      route: {          stack: [          // 外面存了多个method和回调函数          {            method: 'get',            handle: function1          },          {            method: 'post',            handle: function2          }        ]        }    }  ]}

晓得了这个构造咱们能够猜到,整个流程能够分成两局部:注册路由匹配路由。当咱们写app.getapp.post这些办法时,其实就是在router上增加layerroute。当一个网络申请过去时,其实就是遍历layerroute,找到对应的handle拿进去执行。

留神route数组外面的构造,每个项按理来说应该应用一种新的数据结构来存储,比方routeItem之类的。然而Express并没有这样做,而是将它和layer合在一起了,给layer增加了methodhandle属性。这在首次看源码的时候可能造成困惑,因为layer同时存在于routerstack上和routestack上,肩负了两种职责。

router.route

这个办法是咱们后面注册路由的时候调用的一个办法,回顾下后面的注册路由的办法,比方app.get

app.get = function (path) {  this.lazyrouter();  var route = this._router.route(path);  route.get.apply(route, Array.prototype.slice.call(arguments, 1));  return this;}

联合下面讲的路由架构,咱们在注册路由的时候,应该给router增加对应的layerrouterouter.route的代码就不难写出了:

proto.route = function route(path) {  var route = new Route();  var layer = new Layer(path, route.dispatch.bind(route));     // 参数是path和回调函数  layer.route = route;  this.stack.push(layer);  return route;}

Layer和Route构造函数

下面代码新建了RouteLayer实例,这两个类的构造函数其实也挺简略的。只是参数的申明和初始化:

// layer.jsmodule.exports = Layer;function Layer(path, fn) {  this.path = path;  this.handle = fn;  this.method = '';}
// route.jsmodule.exports = Route;function Route() {  this.stack = [];  this.methods = {};    // 一个放慢查找的hash表}

route.get

后面咱们看到了app.get其实通过上面这行代码,最终调用的是route.get

route.get.apply(route, Array.prototype.slice.call(arguments, 1));

也晓得了route.get这种动词处理函数,其实就是往route.stack上增加layer,那咱们的route.get也能够写进去了:

var methods = ["get", "post"];methods.forEach(function (method) {  Route.prototype[method] = function () {    // 反对传入多个回调函数    var handles = flatten(slice.call(arguments));    // 为每个回调新建一个layer,并加到stack上    for (var i = 0; i < handles.length; i++) {      var handle = handles[i];      // 每个handle都应该是个函数      if (typeof handle !== "function") {        var type = toString.call(handle);        var msg =          "Route." +          method +          "() requires a callback function but got a " +          type;        throw new Error(msg);      }      // 留神这里的层级是layer.route.layer      // 后面第一个layer曾经做个path的比拟了,所以这里是第二个layer,path能够间接设置为/      var layer = new Layer("/", handle);      layer.method = method;      this.methods[method] = true; // 将methods对应的method设置为true,用于前面的疾速查找      this.stack.push(layer);    }  };});

这样,其实整个router的构造就构建进去了,前面就看看怎么用这个构造来解决申请了,也就是router.handle办法。

router.handle

后面说了app.handle实际上是调用的router.handle,也晓得了router的构造是在stack上增加了layerrouter,所以router.handle须要做的就是从router.stack上找出对应的layerrouter并执行回调函数:

// 真正解决路由的函数proto.handle = function handle(req, res, done) {  var self = this;  var idx = 0;  var stack = self.stack;  // next办法来查找对应的layer和回调函数  next();  function next() {    // 应用第三方库parseUrl获取path,如果没有path,间接返回    var path = parseUrl(req).pathname;    if (path == null) {      return done();    }    var layer;    var match;    var route;    while (match !== true && idx < stack.length) {      layer = stack[idx++]; // 留神这里先执行 layer = stack[idx]; 再执行idx++;      match = layer.match(path); // 调用layer.match来检测以后门路是否匹配      route = layer.route;      // 没匹配上,跳出当次循环      if (match !== true) {        continue;      }      // layer匹配上了,然而没有route,也跳出当次循环      if (!route) {        continue;      }      // 匹配上了,看看route上有没有对应的method      var method = req.method;      var has_method = route._handles_method(method);      // 如果没有对应的method,其实也是没匹配上,跳出当次循环      if (!has_method) {        match = false;        continue;      }    }    // 循环完了还没有匹配的,就done了,其实就是404    if (match !== true) {      return done();    }    // 如果匹配上了,就执行对应的回调函数    return layer.handle_request(req, res, next);  }};

下面代码还用到了几个LayerRoute的实例办法:

layer.match(path): 检测以后layerpath是否匹配。

route._handles_method(method):检测以后routemethod是否匹配。

layer.handle_request(req, res, next):应用layer的回调函数来解决申请。

这几个办法看起来并不简单,咱们前面一个一个来实现。

到这里其实还有个疑难。从他整个的匹配流程来看,他寻找的其实是router.stack.layer这一层,然而最终应该执行的回调却是在router.stack.layer.route.stack.layer.handle。这是怎么通过router.stack.layer找到最终的router.stack.layer.route.stack.layer.handle来执行的呢?

这要回到咱们后面的router.route办法:

proto.route = function route(path) {  var route = new Route();  var layer = new Layer(path, route.dispatch.bind(route));  layer.route = route;  this.stack.push(layer);  return route;}

这里咱们new Layer的时候给的回调其实是route.dispatch.bind(route),这个办法会再去route.stack上找到正确的layer来执行。所以router.handle真正的流程其实是:

  1. 找到path匹配的layer
  2. 拿出layer上的route,看看有没有匹配的method
  3. layermethod都有匹配的,再调用route.dispatch去找出真正的回调函数来执行。

所以又多了一个须要实现的函数,route.dispatch

layer.match

layer.match是用来检测以后path是否匹配的函数,用到了一个第三方库path-to-regexp,这个库能够将path转为正则表达式,不便前面的匹配,这个库在之前写过的react-router源码中也呈现过。

var pathRegexp = require("path-to-regexp");module.exports = Layer;function Layer(path, fn) {  this.path = path;  this.handle = fn;  this.method = "";  // 增加一个匹配正则  this.regexp = pathRegexp(path);  // 疾速匹配/  this.regexp.fast_slash = path === "/";}

而后就能够增加match实例办法了:

Layer.prototype.match = function match(path) {  var match;  if (path != null) {    if (this.regexp.fast_slash) {      return true;    }    match = this.regexp.exec(path);  }  // 没匹配上,返回false  if (!match) {    return false;  }  // 不然返回true  return true;};

layer.handle_request

layer.handle_request是用来调用具体的回调函数的办法,其实就是拿出layer.handle来执行:

Layer.prototype.handle_request = function handle(req, res, next) {  var fn = this.handle;  fn(req, res, next);};

route._handles_method

route._handles_method就是检测以后route是否蕴含须要的method,因为之前增加了一个methods对象,能够用它来进行疾速查找:

Route.prototype._handles_method = function _handles_method(method) {  var name = method.toLowerCase();  return Boolean(this.methods[name]);};

route.dispatch

route.dispatch其实是router.stack.layer的回调函数,作用是找到对应的router.stack.layer.route.stack.layer.handle并执行。

Route.prototype.dispatch = function dispatch(req, res, done) {  var idx = 0;  var stack = this.stack; // 留神这个stack是route.stack  // 如果stack为空,间接done  // 这里的done其实是router.stack.layer的next  // 也就是执行下一个router.stack.layer  if (stack.length === 0) {    return done();  }  var method = req.method.toLowerCase();  // 这个next办法其实是在router.stack.layer.route.stack上寻找method匹配的layer  // 找到了就执行layer的回调函数  next();  function next() {    var layer = stack[idx++];    if (!layer) {      return done();    }    if (layer.method && layer.method !== method) {      return next();    }    layer.handle_request(req, res, next);  }};

到这里其实Express整体的路由构造,注册和执行流程都实现了,贴下对应的官网源码:

Router类:https://github.com/expressjs/express/blob/master/lib/router/index.js

Layer类:https://github.com/expressjs/express/blob/master/lib/router/layer.js

Route类:https://github.com/expressjs/express/blob/master/lib/router/route.js

中间件

其实咱们后面曾经隐含了中间件,从后面的构造能够看出,一个网络申请过去,会到router的第一个layer,而后调用next到到第二个layer,匹配上layerpath就执行回调,而后始终这样把所有的layer都走完。所以中间件是啥?中间件就是一个layer,他的path默认是/,也就是对所有申请都失效。依照这个思路,代码就简略了:

// application.js// app.use就是调用router.useapp.use = function use(fn) {  var path = "/";  this.lazyrouter();  var router = this._router;  router.use(path, fn);};

而后在router.use外面再加一层layer就行了:

proto.use = function use(path, fn) {  var layer = new Layer(path, fn);  this.stack.push(layer);};

总结

  1. Express也是用原生APIhttp.createServer来实现的。
  2. Express的次要工作是将http.createServer的回调函数拆出来了,构建了一个路由构造Router
  3. 这个路由构造由很多层layer组成。
  4. 一个中间件就是一个layer
  5. 路由也是一个layerlayer上有一个path属性来示意他能够解决的API门路。
  6. path可能有不同的method,每个method对应layer.route上的一个layer
  7. layer.route上的layer尽管名字和router上的layer一样,然而性能侧重点并不一样,这也是源码中让人困惑的一个点。
  8. layer.route上的layer的主要参数是methodhandle,如果method匹配了,就执行对应的handle
  9. 整个路由匹配过程其实就是遍历router.layer的一个过程。
  10. 每个申请来了都会遍历一遍所有的layer,匹配上就执行回调,一个申请可能会匹配上多个layer
  11. 总体来看,Express代码给人的感觉并不是很完满,特地是Layer类肩负两种职责,跟软件工程强调的繁多职责准则不符,这也导致RouterLayerRoute三个类的调用关系有点凌乱。而且对于继承和原型的应用都是很老的形式。可能也是这种不完满催生了Koa的诞生,下一篇文章咱们就来看看Koa的源码吧。
  12. Express其实还对原生的reqres进行了扩大,让他们变得更好用,然而这个其实只相当于一个语法糖,对整体架构没有太大影响,所以本文就没波及了。

本文可运行代码曾经上传GitHub,拿下来一边玩代码,一边看文章成果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

参考资料

Express官网文档:http://expressjs.com/

Express官网源码:https://github.com/expressjs/express/tree/master/lib

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。

作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~