关于javascript:手写Expressjs源码

2次阅读

共计 13484 个字符,预计需要花费 34 分钟才能阅读完成。

上一篇文章咱们讲了怎么用 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.js
var 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.js

var 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.js
var 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.js
module.exports = Layer;

function Layer(path, fn) {
  this.path = path;

  this.handle = fn;
  this.method = '';
}
// route.js
module.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.use
app.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

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

正文完
 0