Express 源码分析及简易封装

在这里插入图片描述


阅读原文


前言

Express 是 NodeJS 的 Web 框架,与 Koa 的轻量相比,功能要更多一些,依然是当前使用最普遍的 NodeJS 框架,本篇参考 Express 的核心逻辑来实现一个简易版,Express 源码较多,逻辑复杂,看一周可能也看不完,若是你已经使用过 Express,又想快速的了解 Express 经常使用功能的原理,那读这篇文章是一个好的选择,也能够为读真正的源码作铺垫,本篇内容每部分代码较多,由于按照 Express 的封装思想很难拆分,因此建议以星号标注区域为主其余代码为辅。html


搭建基本服务

下面咱们使用 Express 来搭建一个最基本的服务,只有三行代码,只能访问不能响应。node

// 三行代码搭建的最基本服务
// 引入 Express
const express = require("express");

// 建立服务
const app = express();

// 监听服务
app.listen(3000);

从上面咱们能够分析出,express 模块给咱们提供了一个函数,调用后返回了一个函数或对象给上面有 listen 方法给咱们建立了一个 http 服务,咱们就按照官方的设计返回一个函数 appexpress

// 文件:express.js
const http = require("http");

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {}

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

咱们建立一个模块 express.js,导出了 createApplication 函数并返回在内部建立 app 函数,createApplication 等于咱们引入 Express 模块时所调用的那个函数,返回值就是咱们接收的 app,在 createApplication 返回的 app 函数上挂载了静态方法 listen,用于帮助咱们启动 http 服务。json

createApplication 函数内咱们使用引入的 http 模块建立了服务,并调用了建立服务 serverlisten 方法,将 app.listen 的全部参数传递进去,这就等于作了一层封装,将真正建立服务器的过程都包在了 app.listen 内部,咱们本身封装的 Express 模块只有在调用导出函数并调用 app.listen 时才会真正的建立服务器和启动服务器,至关于将原生的两步合二为一。数组


路由的实现

Express 框架中有多个路由方法,方法名分别对应不一样的请求方式,能够帮助咱们匹配路径和请求方式,在彻底匹配时执行路由内部的回调函数,以、目的是在不一样路由不一样请求方法的状况下让服务器作出不一样的响应,路由的使用方式以下。浏览器

// 路由的使用方式
// 引入 Express
const express = require("express");

// 建立服务
const app = express();

// 建立路由
app.get("/", function (req, res) {
    res.end("home");
});

app.post("/about", function (req, res) {
    res.end("about");
});

app.all("*", function (req, res) {
    res.end("Not Found");
});

// 监听服务
app.listen(3000);

若是启动上面的服务,经过浏览器访问定义的路由时能够匹配到 app.getapp.postapp.all 并执行回调,但其实咱们能够发现这些方法的名字是与请求类型严格对应的,不只仅这几个,下面来看看实现路由的核心逻辑(直接找到星号提示新增或修改位置便可)。缓存

// 文件:express.js
const http = require("http");

// ***************************** 如下为新增代码 *****************************
// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
// ***************************** 以上为新增代码 *****************************

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
// ***************************** 如下为新增代码 *****************************
        // 获取方法名统一转换成小写
        let method = req.method.toLowerCase();

        // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

        // 循环匹配路径
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循环取得每一层
            let layer = app.routes[i];

            // 若是说路径和请求类型都能匹配,则执行该路由层的回调
            if (
                (reqPath === layer.pathname || layer.pathname === "*") &&
                (method === layer.method || layer.method === "all")
            ) {
                return layer.hanlder(req, res);
            }
        }

        // 若是都没有匹配上,则响应错误信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
// ***************************** 以上为新增代码 *****************************
    }

// ***************************** 如下为新增代码 *****************************
    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");
// ***************************** 以上为新增代码 *****************************

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

咱们的逻辑大致能够分为两个部分,路由方法的建立以及路由的匹配,首先是路由方法的建立阶段,每个方法的内部所作的事情就是将路由的路径、请求方式和回调函数做为对象的属性,并将对象存入一个数组中统一管理,因此咱们建立了 app.routes 数组用来存储这些路由对象。服务器

方法名对应请求类型,请类型有不少,咱们不会一一的建立每个方法,因此选择引入专门存储请求类型名称的 methods 模块,其实路由方法逻辑相同,咱们封装了 createRouteMethod 方法用来生成不一样路由方法的函数体,之因此这样作是由于有个特殊的路由方法 app.all,致使请求类型有差异,其余的能够从 methods 中取,app.all 咱们定义类型为 all 经过 createRouteMethod 函数的参数传入。app

接着就是循环 methods 调用 createRouteMethod 函数建立路由方法,并单首创建 app.all 方法。框架

路由匹配阶段实在函数 app 内完成的,由于启动服务接收到请求时会执行 createServer 中的回调,即执行 app,先经过原生自带的 req.method 取出请求方式并处理成小写,经过 req.path 取出完整路径并分红路由名和查询字符串两个部分。

循环 app.routes 用取到请求的类型和路由名称匹配,二者都相等则执行对应路由对象上的回调函数,在判断条件中,请求方式兼容了咱们以前定义的 all,为了全部的请求类型只要路由匹配均可以执行 app.all 的回调,请求路径兼容了 *,由于若是某个路由方法定义的路径为 *,则任意路由均可以执行这个路由对象上的回调。


扩展请求对象属性

且在路由内部能够经过 req 访问一些原生没有的属性如 req.pathreq.queryreq.hostreq.params,这说明 Express 在实现的过程当中对 req 进行了处理。

// req 属性的使用
// 引入 Express
const express = require("express");

// 建立服务
const app = express();

// 建立路由
app.get("/", function (req, res) {
    console.log(req.path);
    console.log(req.query);
    console.log(req.host);
    res.end("home");
});

app.get("/about/:id/:name", function (req, res) {
    console.log(req.params);
    res.end("about");
});

// 监听服务
app.listen(3000);

在上面的使用中咱们写了两个路由,分别打印了原生所不具有而 Express 帮咱们处理并新增的属性,下面咱们就来在以前本身实现的 express.js 的基础上增长这些属性(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");

// ***************************** 如下为新增代码 *****************************
const querystring = require("querystring");
// ***************************** 以上为新增代码 *****************************

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
        // 获取方法名统一转换成小写
        let method = req.method.toLowerCase();

        // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

// *************************** 如下为修改代码 *****************************
        req.path = reqPath; // 将路径名赋值给 req.path
        req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
        req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

        // 循环匹配路径
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循环取得每一层
            let layer = app.routes[i];

            // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
            if (layer.regexp) {
                let result = pathname.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                // 若是匹配到结果且请求方式匹配
                if (result && (method === layer.method || layer.method === "all")) {
                    // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                    req.params = layer.paramNames.reduce(function (memo, key, index) {
                        memo[key] = result[index + 1];
                        return memo;
                    }, {});

                    // 执行对应的回调
                    return layer.hanlder(req, res);
                }
            } else {
                // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                if (
                    (reqPath === layer.pathname || layer.pathname === "*") &&
                    (method === layer.method || layer.method === "all")
                ) {
                    return layer.hanlder(req, res);
                }
            }
// ***************************** 以上为修改代码 *****************************
        }

        // 若是都没有匹配上,则响应错误信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
    }

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

// ***************************** 如下为新增代码 *****************************
            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
            if (pathname.indexOf(":") !== -1) {
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }
// ***************************** 以上为新增代码 *****************************

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

上面代码有些长,咱们一点一点分析,首先是 req.path,就是咱们浏览器地址栏里查询字符串前的路径,值其实就是咱们以前从 req.url 中解构出来的 pathname,咱们只须要将 pathname 赋值给 req.path 便可。

req.query 是浏览器地址栏的查询字符串传递的参数,就是咱们从 req.url 解构出来的查询字符串,借助 querystring 模块将查询字符串处理成对象赋值给 req.query 便可。

req.host 是访问的主机名,请求头中的 host 包含了主机名和端口号,咱们只要截取出前半部分赋值给 req.host 便可。

最复杂的是 req.params 的实现,大概分为两个步骤,首先是在路由方法建立时须要检查定义的路由是否含有路由参数,若是有则取出参数的键存入数组 paramNames 中,而后建立一个匹配路由参数的正则,经过 replace 实现正则字符串的建立,再经过 RegExp 构造函数来建立正则,并挂在路由对象上,之因此使用 replace 是由于建立的规则内的分组要和路由参数的个数是相同的,咱们将这些逻辑完善进了 createRouteMethod 函数中。


实现响应方法 send 和 sendFile

以前的例子中咱们都是用原生的 end 方法响应浏览器,咱们知道 end 方法只能接收字符串和 Buffer 做为响应的值,很是不方便,其实在 Express 中封装了一个 send 方法挂在 res 对象下,能够接收数组、对象、字符串、Buffer、数字处理后响应给浏览器,在 Express 内部一样封装了一个 sendFile 方法用于读取请求的文件。

// send 响应
// 引入 Express
const express = require("express");
const path = require("path");

// 建立服务
const app = express();

// 建立路由
app.get("/", function (req, res) {
    res.send({ name: "panda", age: 28 });
});

app.get("/test.txt", function (req, res) {
    // 必须传入绝对路径
    res.sendFile(path.join(__dirname, req.path));
});

// 监听服务
app.listen(3000);

经过咱们的分析,封装的 send 方法应该是将 end 不支持的类型数据转换成了字符串,在内部再次调用 end,而 sendFile 方法规定参数必须为绝对路径,内部实现应该是利用可读流读取文件内容相应给浏览器,下面是两个方法的实现(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
const querystring = require("querystring");

// ***************************** 如下为新增代码 *****************************
const util = require("util");
const httpServer = require("_http_server"); // 存储 node 服务相关信息
const fs = require("fs");
// ***************************** 以上为新增代码 *****************************

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
        // 获取方法名统一转换成小写
        let method = req.method.toLowerCase();

        // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
        let [reqPath, query = ""] = req.url.split("?");

        req.path = reqPath; // 将路径名赋值给 req.path
        req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
        req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

// ***************************** 如下为新增代码 *****************************
        // 响应方法
        res.send = function (params) {
            // 设置响应头
            res.setHeader("Content-Type", "text/plain;charset=utf8");

            // 检测传入值得数据类型
            switch (typeof params) {
                case "object":
                    res.setHeader("Content-Type", "application/json;charset=utf8");
                    params = util.inspect(params); // 将任意类型的对象转换成字符串
                    break;
                case "number":
                    params = httpServer.STATUS_CODES[params]; // 数字则直接取出状态吗对应的名字返回
                    break;
                default:
                    break;
            }

            // 响应
            res.end(params);
        }

        // 响应文件方法
        res.sendFile = function (pathname) {
            fs.createReadStream(pathname).pipe(res);
        }
// ***************************** 以上为新增代码 *****************************

        // 循环匹配路径
        for (let i = 0; i < app.routes.lenth; i++) {
            // 循环取得每一层
            let layer = app.routes[i];

            // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
            if (layer.regexp) {
                let result = reqPath.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                // 若是匹配到结果且请求方式匹配
                if (result && (method === layer.method || layer.method === "all")) {
                    // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                    req.params = layer.paramNames.reduce(function (memo, key, index) {
                        memo[key] = result[index + 1];
                        return memo;
                    }, {});

                    // 执行对应的回调
                    return layer.hanlder(req, res);
                }
            } else {
                // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                if (
                    (reqPath === layer.pathname || layer.pathname === "*") &&
                    (method === layer.method || layer.method === "all")
                ) {
                    return layer.hanlder(req, res);
                }
            }
        }

        // 若是都没有匹配上,则响应错误信息
        res.end(`CANNOT ${req.method} ${reqPath}`);
    }

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
            if (pathname.indexOf(":") !== -1) {
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

有一点须要注意,在 Node 环境中想把任何对象类型转换成字符串应该使用 util.inspect 方法,而当 send 方法输入数字类型时,要返回对应状态码的名称,可经过 _http_server 模块的 STATUS_CODES 对象获取。


内置中间件的实现

Express 最大的特色就是中间件机制,中间件就是用来处理请求的函数,用来完成不一样场景的请求处理,一个中间件处理完请求后能够再传递给下一个中间件,具备回调函数 next,不执行 next 则会卡在一个位置,调用 next 则继续向下传递。

// use 的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 建立服务
const app = express();

// 建立路由
app.use(function (req, res, next) {
    res.setHeader("Content-Type", "text/html;charset=utf8");
    next();
});

// 建立路由
app.get("/", function (req, res) {
    res.send({ name: "panda", age: 28 });
});

// 监听服务
app.listen(3000);

在上面代码中使用 use 方法执行了传入的回调函数,实现公共逻辑,起到了中间件的做用,调用回调参数的 next 方法向下继续执行,下面来实现 use 方法(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存储 node 服务相关信息
const fs = require("fs");

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
// ***************************** 如下为修改代码 *****************************
        // 循环匹配路径
        let index = 0;

        function next(err) {
            // 获取第一个回调函数
            let layer = app.routes[index++];

            if (layer) {
                // 将当前中间件函数的属性解构出来
                let { method, pathname, handler } = layer;

                if (err) { // 若是存在错误将错误交给错误处理中间件,不然
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 若是不存在错误则继续向下执行
                    // 判断是中间件仍是路由
                    if (method === "middle") {
                        // 匹配路径判断
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                            // 若是匹配到结果且请求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 执行对应的回调
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 若是都没有匹配上,则响应错误信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
// ***************************** 以上为修改代码 *****************************
    }

// ***************************** 如下为新增代码 *****************************
    function init() {
        return function (req, res, next) {
            // 获取方法名统一转换成小写
            let method = req.method.toLowerCase();

            // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 将路径名赋值给 req.path
            req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
            req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

            // 响应方法
            res.send = function (params) {
                // 设置响应头
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 检测传入值得数据类型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 将任意类型的对象转换成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 数字则直接取出状态吗对应的名字返回
                        break;
                    default:
                        break;
                }

                // 响应
                res.end(params);
            }

            // 响应文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 向下执行
            next();
        }
    }
// ***************************** 以上为新增代码 *****************************

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
// ***************************** 如下为修改代码 *****************************
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
// ***************************** 以上为修改代码 *****************************
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

// ***************************** 如下为新增代码 *****************************
    // 添加中间件方法
    app.use = function (pathname, handler) {
        // 处理没有传入路径的状况
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函数并执行
        createRouteMethod("middle")(pathname, handler);
    }

    // 将初始逻辑做为中间件执行
    app.use(init());
// ***************************** 以上为新增代码 *****************************

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

use 方法第一个参数为路径,与路由相同,不传默认为 /,若是不传全部的路径都会通过该中间件,若是传入指定的值,则匹配后的请求才会经过该中间件。

中间件的执行可能存在异步的状况,但以前匹配路径使用的是 for 循环同步匹配,咱们将其修改成异步并把路由匹配的逻辑与中间件路径匹配的逻辑进行了整合,并建立了 use 方法,对是否传了第一个参数作了一个兼容,其余将带有请求方式、路径和回调的逻辑统一使用 createRouteMethod 方法建立,并传入 middle 类型,createRouteMethod 中路由参数匹配的逻辑对 middle 类型作了一个排除。

使用 Express 中间件调用 next 方法时,不传递参数和参数为 null 表明执行成功,若是传入了其余的参数,表示执行出错,会跳过全部正常的中间件和路由,直接交给错误处理中间件处理,并将 next 传入的参数做为错误处理中间件回调函数的第一个参数 err,后面三个参数分别为 reqresnext

代码种建立了 index 变量,默认调用了一次 next 方法,每次而后取出数组 app.routes 中的路由对象的回调函数执行,并在内部执行 handler,而 handler 回调中又调用了 next 方法,就这样将整个中间件和路由的回调串联起来。

咱们发如今第一次调用 next 以前的全部逻辑,如给 req 添加属性,给 res 添加方法,都是公共逻辑,是任何中间件和路由在匹配以前都会执行的逻辑,咱们既然有了中间件方法 app.user,能够将这些逻辑抽取出来做为一个单独的中间件回调函数执行,因此建立了 init 函数,内部返回了一个函数做为回调函数,形参为 reqresnext,并在init 调用返回的函数内部调用 next 向下执行。


内置模板引擎的实现

Express 框架中内置支持了 ejsjade 等模板,使用方法 “三部曲” 以下。

// 模板的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 建立服务
const app = express();

// 一、指定模板引擎,其实就是模板文件的后缀名
app.set("view engine", "ejs");

// 二、指定模板的存放根目录
app.set("views", path.resolve(__dirname, "views"));

// 三、若是要自定义模板后缀和函数的关系
app.engine(".html", require("./ejs").__express);

// 建立路由
app.get("/user", function (req, res) {
    //使用指定的模板引擎渲染 user 模板
    res.render("user", { title: "用户管理" });
});

// 监听服务
app.listen(3000);

上面将模板根目录设置为 views 文件夹,并规定了模板类型为 ejs,能够同时给多种模板设置,并不冲突,若是须要将其余后缀名的模板按照另外一种模板的渲染引擎渲染则使用 app.engine 进行设置,下面看一下实现代码(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存储 node 服务相关信息
const fs = require("fs");

// ***************************** 如下为新增代码 *****************************
const path = require("path");
// ***************************** 以上为新增代码 *****************************

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
        // 循环匹配路径
        let index = 0;

        function next(err) {
            // 获取第一个回调函数
            let layer = app.routes[index++];

            if (layer) {
                // 将当前中间件函数的属性解构出来
                let { method, pathname, handler } = layer;

                if (err) { // 若是存在错误将错误交给错误处理中间件,不然
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 若是不存在错误则继续向下执行
                    // 判断是中间件仍是路由
                    if (method === "middle") {
                        // 匹配路径判断
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                            // 若是匹配到结果且请求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 执行对应的回调
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 若是都没有匹配上,则响应错误信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 获取方法名统一转换成小写
            let method = req.method.toLowerCase();

            // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 将路径名赋值给 req.path
            req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
            req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

            // 响应方法
            res.send = function (params) {
                // 设置响应头
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 检测传入值得数据类型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 将任意类型的对象转换成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 数字则直接取出状态吗对应的名字返回
                        break;
                    default:
                        break;
                }

                // 响应
                res.end(params);
            }

            // 响应文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

// ***************************** 如下为新增代码 *****************************
            // 模板渲染方法
            res.render = function (filename, data) {
                // 将文件名和模板路径拼接
                let filepath = path.join(app.get("views"), filename);

                // 获取扩展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 若是没有扩展名,则使用默认的扩展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 读取模板文件并使用渲染引擎相应给浏览器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }
// ***************************** 以上为新增代码 *****************************

            // 向下执行
            next();
        }
    }

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
// ***************************** 如下为修改代码 *****************************
            // 知足条件说明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }
// ***************************** 以上为修改代码 *****************************

            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

    // 添加中间件方法
    app.use = function (pathname, handler) {
        // 处理没有传入路径的状况
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函数并执行
        createRouteMethod("middle")(pathname, handler);
    }

    // 将初始逻辑做为中间件执行
    app.use(init());

// ***************************** 如下为新增代码 *****************************
    // 存储设置的对象
    app.setting ={};

    // 存储模板渲染方法
    app.engines = {};

    // 添加设置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }
// ***************************** 以上为新增代码 *****************************

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

module.exports = createApplication;

在上面新增代码中设置了两个缓存 settingsengines,前者用来存储模板相关的设置,如渲染成什么类型的文件、读取模板文件的根目录,后者用来存储渲染引擎,即渲染模板的方法,这因此设置这两个缓存对象是为了实现 Express 多种不一样模板共存的功能,能够根据须要进行设置和使用,而设置的方法分别为 app.setapp.engine,有设置值的方法就应该有取值的方法,可是 app.get 方法已经被设置为路由方法了,为了语义咱们在 app.get 方法逻辑中进行了兼容,当参数为 1 个时,从 settings 中取值并返回,不然执行添加路由方法的逻辑。

以前都是准备工做,在使用时不管是中间件仍是路由中都是靠调用 res.render 方法并传入模板路径和渲染数据来真正实现渲染和响应的,render 方法是在 init 函数初始化时就挂在了 res 上,核心逻辑是取出传入的模板文件后缀名,若是存在则使用后缀名,将文件名与默认读取模板的文件夹路径拼接传递给设置的渲染引擎的渲染方法,若是不存在后缀名则默认拼接 .html 看成后缀名,再与默认读取模板路径进行拼接,在渲染函数的回调中将渲染引擎渲染的模板字符串响应给浏览器。


内置静态资源中间件的实现

Express 内部能够经过路由处理静态文件,可是若是可能请求多个文件不可能一个文件对应一个路由,所以 Express 内部实现了静态文件中间件,使用以下。

// 静态文件中间件的使用
// 引入 Express
const express = require("express");
const path = require("path");

// 建立服务
const app = express();

// 使用处理静态文件中间件
app.use(express.static(path.resolve(__dirname, "public")));

// 监听服务
app.listen(3000);

从上面使用能够看出,express.static 是一个函数,执行的时候传入了一个参数,为默认查找文件的根路径,而添加中间件的 app.use 方法传入的参数正好是回调函数,这说明 express.static 方法须要返回一个函数,形参为 reqresnext,经过调用方式咱们能看出 static 是静态方法,挂在了模块返回的函数上,实现代码以下(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存储 node 服务相关信息
const fs = require("fs");
const path = require("path");

// ***************************** 如下为新增代码 *****************************
const mime = require("mime");
// ***************************** 以上为新增代码 *****************************

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
        // 循环匹配路径
        let index = 0;

        function next(err) {
            // 获取第一个回调函数
            let layer = app.routes[index++];

            if (layer) {
                // 将当前中间件函数的属性解构出来
                let { method, pathname, handler } = layer;

                if (err) { // 若是存在错误将错误交给错误处理中间件,不然
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 若是不存在错误则继续向下执行
                    // 判断是中间件仍是路由
                    if (method === "middle") {
                        // 匹配路径判断
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                            // 若是匹配到结果且请求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 执行对应的回调
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 若是都没有匹配上,则响应错误信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 获取方法名统一转换成小写
            let method = req.method.toLowerCase();

            // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 将路径名赋值给 req.path
            req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
            req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

            // 响应方法
            res.send = function (params) {
                // 设置响应头
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 检测传入值得数据类型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 将任意类型的对象转换成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 数字则直接取出状态吗对应的名字返回
                        break;
                    default:
                        break;
                }

                // 响应
                res.end(params);
            }

            // 响应文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 模板渲染方法
            res.render = function (filename, data) {
                // 将文件名和模板路径拼接
                let filepath = path.join(app.get("views"), filename);

                // 获取扩展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 若是没有扩展名,则使用默认的扩展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 读取模板文件并使用渲染引擎相应给浏览器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }

            // 向下执行
            next();
        }
    }

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            // 知足条件说明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }

            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

    // 添加中间件方法
    app.use = function (pathname, handler) {
        // 处理没有传入路径的状况
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函数并执行
        createRouteMethod("middle")(pathname, handler);
    }

    // 将初始逻辑做为中间件执行
    app.use(init());

    // 存储设置的对象
    app.setting ={};

    // 存储模板渲染方法
    app.engines = {};

    // 添加设置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

// ***************************** 如下为新增代码 *****************************
createApplication.static = function (staticRoot) {
    return function (req, res, next) {
        // 获取文件的完整路径
        let filename = path.join(staticRoot, req.path);

        // 若是没有权限就向下执行其余中间件,若是有权限读取文件并响应
        fs.access(filename, function (err) {
            if (err) {
                next();
            } else {
                // 设置响应头类型和响应文件内容
                res.setHeader("Content-Type", `${mime.getType()};charset=utf8`);
                fs.createReadStream(filename).pipe(res);
            }
        });
    }
}
// ***************************** 以上为新增代码 *****************************

module.exports = createApplication;

这个方法的核心逻辑是获取文件的路径,检查文件的权限,若是没有权限,则调用 next 交给其余中间件,这里注意的是 err 错误对象不要传递给 next,由于后面的中间件还要执行,若是传递后会直接执行错误处理中间件,有权限的状况下就正常读取文件内容,给 Content-Type 响应头设置文件类型,并将文件的可读流经过 pipe 方法传递给可写流 res,即响应给浏览器。


实现重定向

Express 中有一个功能在咱们匹配到的某一个路由中调用能够直接跳转到另外一个路由,即 302 重定向。

// 使用重定向
// 引入 Express
const express = require("express");
const path = require("path");

// 建立服务
const app = express();

// 建立路由
app.get("/user", function (req, res, next) {
    res.end("user");
});

app.get("/detail", function (req, res, next) {
    // 访问 /detail 重定向到 /user
    res.redirect("/user");
});

// 监听服务
app.listen(3000);

看到上面的使用方式,咱们根据前面的套路知道是 Expressres 对象上给挂载了一个 redirect 方法,参数为状态码(可选)和要跳转路由的路径,而且这个方法应该在 init 函数调用时挂在 res 上的,下面是实现的代码(直接找到星号提示新增或修改位置便可)。

// 文件:express.js
const http = require("http");

// methods 模块返回存储全部请求方法名称的数组
const methods = require("methods");
const querystring = require("querystring");
const util = require("util");
const httpServer = require("_http_server"); // 存储 node 服务相关信息
const fs = require("fs");
const path = require("path");
const mime = require("mime");

function createApplication() {
    // 建立 app 函数,身份为总管家,用于将请求分派给别人处理
    let app = function (req, res) {
        // 循环匹配路径
        let index = 0;

        function next(err) {
            // 获取第一个回调函数
            let layer = app.routes[index++];

            if (layer) {
                // 将当前中间件函数的属性解构出来
                let { method, pathname, handler } = layer;

                if (err) { // 若是存在错误将错误交给错误处理中间件,不然
                    if (method === "middle", handle.length === 4) {
                        return hanlder(err, req, res, next);
                    } else {
                        next(err);
                    }
                } else { // 若是不存在错误则继续向下执行
                    // 判断是中间件仍是路由
                    if (method === "middle") {
                        // 匹配路径判断
                        if (
                            pathname === "/" ||
                            pathname === req.path ||
                            req.path.startWidth(pathname)
                        ) {
                            handler(req, res, next);
                        } else {
                            next();
                        }
                    } else {
                        // 若是路由对象上存在正则说明存在路由参数,不然正常匹配路径和请求类型
                        if (layer.regexp) {
                            let result = req.path.match(layer.regexp); // 使用路径配置的正则匹配请求路径

                            // 若是匹配到结果且请求方式匹配
                            if (result && (method === layer.method || layer.method === "all")) {
                                // 则将路由对象 paramNames 属性中的键与匹配到的值构建成一个对象
                                req.params = layer.paramNames.reduce(function (memo, key, index) {
                                    memo[key] = result[index + 1];
                                    return memo;
                                }, {});

                                // 执行对应的回调
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        } else {
                            // 若是说路径和请求类型都能匹配,则执行该路由层的回调
                            if (
                                (req.path === layer.pathname || layer.pathname === "*") &&
                                (method === layer.method || layer.method === "all")
                            ) {
                                return layer.hanlder(req, res);
                            } else {
                                next();
                            }
                        }
                    }
                }
            } else {
                // 若是都没有匹配上,则响应错误信息
                res.end(`CANNOT ${req.method} ${req.path}`);
            }
        }

        next();
    }

    function init() {
        return function (req, res, next) {
            // 获取方法名统一转换成小写
            let method = req.method.toLowerCase();

            // 访问路径解构成路由和查询字符串两部分 /user?a=1&b=2
            let [reqPath, query = ""] = req.url.split("?");

            req.path = reqPath; // 将路径名赋值给 req.path
            req.query = querystring.parse(query); // 将查询字符串转换成对象赋值给 req.query
            req.host = req.headers.host.split(":")[0]; // 将主机名赋值给 req.host

            // 响应方法
            res.send = function (params) {
                // 设置响应头
                res.setHeader("Content-Type", "text/plain;charset=utf8");

                // 检测传入值得数据类型
                switch (typeof params) {
                    case "object":
                        res.setHeader("Content-Type", "application/json;charset=utf8");
                        params = util.inspect(params); // 将任意类型的对象转换成字符串
                        break;
                    case "number":
                        params = httpServer.STATUS_CODES[params]; // 数字则直接取出状态吗对应的名字返回
                        break;
                    default:
                        break;
                }

                // 响应
                res.end(params);
            }

            // 响应文件方法
            res.sendFile = function (pathname) {
                fs.createReadStream(pathname).pipe(res);
            }

            // 模板渲染方法
            res.render = function (filename, data) {
                // 将文件名和模板路径拼接
                let filepath = path.join(app.get("views"), filename);

                // 获取扩展名
                let extname = path.extname(filename.split(path.sep).pop());

                // 若是没有扩展名,则使用默认的扩展名
                if (!extname) {
                    extname = `.${app.get("view engine")}`
                    filepath += extname;
                }

                // 读取模板文件并使用渲染引擎相应给浏览器
                app.engines[extname](filepath, data, function (err, html) {
                    res.setHeader("Content-Type", "text/html;charset=utf8");
                    res.end(html);
                });
            }

// ***************************** 如下为新增代码 *****************************
            // 重定向方法
            res.redirect = function (status, target) {
                // 若是第一个参数是字符串类型说明没有传状态码
                if (typeof status === "string") {
                    // 将第二个参数(重定向的目标路径)设置给 target
                    target = status;

                    // 再把状态码设置成 302
                    status = 302;
                }

                // 响应状态码,设置重定向响应头
                res.statusCode = status;
                res.setHeader("Location", target);
                res.end();
            }
// ***************************** 以上为新增代码 *****************************

            // 向下执行
            next();
        }
    }

    // 存储路由层的请求类型、路径和回调
    app.routes = [];

    // 返回一个函数体用于将路由层存入 app.routes 中
    function createRouteMethod(method) {
        return function (pathname, handler) {
            // 知足条件说明是取值方法
            if (method === "get" && arguments.length === 1) {
                return app.settings[pathname];
            }

            let layer = {
                method,
                pathname, // 不包含查询字符串
                handler
            };

            // 若是含有路由参数,如 /xxx/:aa/:bb,取出路由参数的键 aa bb 存入数组并挂在路由对象上
            // 并生匹配 /xxx/aa/bb 的正则挂在路由对象上
            if (pathname.indexOf(":") !== -1 && pathname.method !== "middle") {
                let paramNames = []; // 存储路由参数

                // 将路由参数取出存入数组,并返回正则字符串
                let regStr = pathname.replace(/:(\w+)/g, function (matched, attr) {
                    paramNames.push(attr);
                    return "(\\w+)";
                });

                let regexp = new RegExp(regStr); // 生成正则类型
                layer.regexp = regexp; // 将正则挂在路由对象上
                layer.paramNames = paramNames; // 将存储路由参数的数组挂载对象上
            }

            // 把这一层放入存储全部路由层信息的数组中
            app.routes.push(layer);
        }
    }

    // 循环构建全部路由方法,如 app.get app.post 等
    methods.forEach(function (method) {
        // 匹配路由的 get 方法
        app[method] = createRouteMethod(method);
    });

    // all 方法,通吃全部请求类型
    app.all = createRouteMethod("all");

    // 添加中间件方法
    app.use = function (pathname, handler) {
        // 处理没有传入路径的状况
        if (typeof handler !== "function") {
            handler = pathname;
            pathname = "/";
        }

        // 生成函数并执行
        createRouteMethod("middle")(pathname, handler);
    }

    // 将初始逻辑做为中间件执行
    app.use(init());

    // 存储设置的对象
    app.setting ={};

    // 存储模板渲染方法
    app.engines = {};

    // 添加设置的方法
    app.set = function (key, value) {
        app.use[key] = value;
    }

    // 添加渲染引擎的方法
    app.engine = function (ext, renderFile) {
        app.engines[ext] = renderFile;
    }

    // 启动服务的 listen 方法
    app.listen = function () {
        // 建立服务器
        const server = http.createServer(app);

        // 监听服务,可能传入多个参数,如第一个参数为端口号,最后一个参数为服务启动后回调
        server.listen(...arguments);
    }

    // 返回 app
    return app;
}

createApplication.static = function (staticRoot) {
    return function (req, res, next) {
        // 获取文件的完整路径
        let filename = path.join(staticRoot, req.path);

        // 若是没有权限就向下执行其余中间件,若是有权限读取文件并响应
        fs.access(filename, function (err) {
            if (err) {
                next();
            } else {
                // 设置响应头类型和响应文件内容
                res.setHeader("Content-Type", `${mime.getType()};charset=utf8`);
                fs.createReadStream(filename).pipe(res);
            }
        });
    }
}

module.exports = createApplication;

其实 res.redirect 方法的核心逻辑就是处理参数,若是没有传状态码的时候将参数设置给 target,将状态码设置为 302,并设置重定向响应头 Location


总结

到此为止 Express 的大部份内置功能就都简易的实现了,因为 Express 内部的封装思想,以及代码复杂、紧密的特色,各个功能代码很难单独拆分,总结一下就是很难表述清楚,只能经过大量代码来堆砌,好在每一部分实现我都标记了 “重点”,但看的时候仍是要经历 “痛苦”,这已经将 Express 中的逻辑 “阉割” 到了必定的程度,读 Express 的源码必定比读这篇文章更须要耐心,固然若是你已经读到了这里证实困难都被克服了,继续加油。