简单实现express(>4.0)框架

简单实现express(>4.0)框架

express的源码并不多, 初看起来有点绕来绕去, 看得烦了, 就萌生了自己写一个简单express的想法, 想着如果可以自己写个简单的demo出来, 再看源码应该会清晰些

因为已经知道express的原理: 将每一个中间函数都存起来, 然后通过路由匹配, 找到匹配的函数, 通过next一个接一个的执行对应的函数

最终写出来的框架应该具有以下功能

  1. 多个回调函数可以按照顺序执行(next)
  2. 有对应的路由映射, 路由不匹配的中间函数不执行
  3. 支持多个中间函数作为参数, 异常处理函数
  4. 解决req.params 和 req.query 获取请求参数
  5. 支持路由分发, 每个express实例可以处理独立的某一类路由
  6. 支持各种请求方法
1. 多个回调函数可以按照顺序执行(next)

了解express框架的话应该会知道, express框架有最核心的三个函数,
handle(处理协议)
use(将中间件函数存储起来)
next(寻找每一个匹配的中间件函数)

那么首先创建一个最初始的expressDemo.js文件, 内容如下, 包含了启动函数, 以及一个空的handle函数, 一个空的use函数
注意: 这里并没有在app函数里面加上next参数, 具体看第5点

 expressDemo.js
const http = require('http');

module.exports = function () {
    const app = function (req, res) {
        app.handle(req, res);
    }
    Object.assign(app, application);  // application对象的属性全部复制到app
    return app;
}

const application = {};
/**启动函数 */
application.listen = function () {
    const server = http.createServer(this); // this就是指上面的app
    return server.listen.apply(server, arguments);
}

/**路由执行函数 */
application.handle = function (req, res) {

}

/** */
application.use = function (req, res, next) {

}

要让每一个中间函数按照顺序执行, 我们可以使用一个数组把所有的中间函数存起来, 然后依次处理即可, 那么接下来填充一下use函数和handle函数,
注意: 我们加上一个默认的中间件函数, 该函数在所有的中间件函数都遍历完之后执行.

module.exports = function () {
    const app = function (req, res) {
        app.handle(req, res);
    }
    Object.assign(app, application);
    app.stack = []; // ++++ app上新增一个stack数组用来存储所有的中间函数
    return app;
}

application.use = function (fn) {
    this.stack.push(fn); // ++++ 使用use函数将中间函数放入stack
}

application.handle = function (req, res, callback) {
  const stack = this.stack;
    // 默认是不存在callback的, 如果执行到最后没有中间函数,
    // 我们必须要有一个最终的函数给客户端返回, 这里就是定义最后的一个执行函数
    const done = callback || function (req, res) {
        console.error('final');
        res.end('final');
    }

    let index = 0;

    function next() {
        const fn = stack[index];
        index++;
        if (fn) {
            fn(req, res, next);
        } else {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            done(req, res); 
        }
    }

    next();
}

创建一个appDemo.js 运行一下

const expressDemo = require('./expressDemo');
const app = expressDemo();

app.use(function(req, res, next) { // 中间件1
    console.log('test express demo 1111');
    next()
});


app.use(function(req, res, next) { // 中间件2
    console.log('test express demo 2222');
    next()
});

app.listen(9005, function () {
    console.log('server is running');
})

访问 localhost:9005, 看控制台打印和网页输出, 跟预想的情况一样, 先执行中间件函数, 当找不到中间件函数时, 执行最终处理函数,
这里打印两次是因为发送了一次option请求和一次get请求

在这里插入图片描述

2. 有对应的路由映射, 路由不匹配的中间函数不执行

上面只是简单的实现了中间件函数顺序运行,
但是这些中间函数在所有路由下均会执行, 所以我们需要加上路由匹配逻辑, 当中间件函数对应的路由和实际路由不匹配时, 不执行该函数

对于use函数做如下修改, 主要是讲中间件函数封装到一个layer对象中, 增加新的参数path
该修改同时也支持参数传入多个中间件函数

application.use = function (path) {
    let callbacks;
    if (typeof path !== 'string' && typeof path !== 'function') {
        throw new Error('application.use 第一个参数必须为字符串或者函数');
    }
    if (typeof path == 'string') {
        if (path == '*' || path == '/') {
            path == '';
        }
        callbacks = Array.prototype.slice.call(arguments, 1); // 从第二个参数开始都认为是中间函数
        if (callbacks.length == 0) {
            throw new Error('application.use 需要至少一个中间函数');
        }
    }
    if (typeof path == 'function') {
        // 转为数组, arguments只是类数组结构而已
        callbacks = Array.prototype.slice.call(arguments, 0); 
        path = ''; // 该代码放在后面, 放在前面相当于给path重新赋值, 导致第一个中间函数丢失
    }
    for (let i = 0; i < callbacks.length; i++) {
        const middleware = callbacks[i];
        // 把中间函数和对应的path组成一个对象, 在next函数里面就可以根据路由来判断中间函数是否执行了
        const layer = { 
            path: path,
            fn: middleware
        }
        this.stack.push(layer); // 使用use函数将中间函数放入stack
    }
}

那么对应的handle函数也做对应修改, 加上路由匹配规则

application.handle = function (req, res, callback) {
    const stack = this.stack;
    // 默认是不存在callback的, 如果执行到最后没有中间函数,
    // 我们必须要有一个最终的函数给客户端返回, 这里就是定义最后的一个执行函数
    const done = callback || function (req, res) {
        console.error('final');
        res.end('final');
    }

    let index = 0;

    function next() {
        const layer = stack[index];
        index++;
        if (layer) {
            if (layer.path == req.url || layer.path == '') { // layer.path = '' 表示全路由匹配, 
                layer.fn(req, res, next);
            } else {
                next();
            } 
        } else {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            done(req, res); 
        }
    }

    next();
}

修改appDemo.js测试

const expressDemo = require('./expressDemo');
const app = expressDemo();

app.use(function(req, res, next) { // 中间件1
    console.log('test express demo 1111');
    next()
});


app.use('/test', function(req, res, next) { // 中间件2
    console.log('path success');
    next()
});

app.listen(9005, function () {
    console.log('server is running');
})

分别访问/test 和 / test2 得到如下结果
在这里插入图片描述
到这里就简单完成了对应的路由映射, 但是实际上并不精确
因为部分路由可能是/a/:bb 这种形式, 或者使用 /route1 想匹配 /route1/2, /rout1/route2这种路由, 再或者带有参数 /aut?a=1这种
这些情况下我们使用路由相等来匹配是行不通的, 因此这里引入一个模块

path-to-regexp

该模块用来根据路由生成对应的正则表达式, 正好满足我们的需求, 详细使用可查询该模块文档.

那么use函数可以改为如下, 在layer对象中新增一个属性regExp, 表示该函数对应路由的正则表达式
注意到还有一个新的参数 keys 后面会用到

application.use = function (path) {
    let callbacks;
    if (typeof path !== 'string' && typeof path !== 'function') {
        throw new Error('application.use 第一个参数必须为字符串或者函数');
    }
    if (typeof path == 'string') {
        if (path == '*' || path == '/') {
            path == '';
        }
        callbacks = Array.prototype.slice.call(arguments, 1); // 从第二个参数开始都认为是中间函数
        if (callbacks.length == 0) {
            throw new Error('application.use 需要至少一个中间函数');
        }
    }
    if (typeof path == 'function') {
        // 转为数组, arguments只是类数组结构而已
        callbacks = Array.prototype.slice.call(arguments, 0); 
        path = ''; // 该代码放在后面, 放在前面相当于给path重新赋值, 导致第一个中间函数丢失
    }
    for (let i = 0; i < callbacks.length; i++) {
        const middleware = callbacks[i];
        // keys是用来存储path里面的模糊字段的, 比如path = /a/:b/:c 
        // 那么经过pathRegexp, keys就会变为 [{name: 'b', xxx}, {name: 'c'}] 这种形式
        const keys = [];
        // 把中间函数和对应的path组成一个对象, 在next函数里面就可以根据路由来判断中间函数是否执行了
        const layer = { 
            path: path,
            fn: middleware,
            // end: false 表示是否结尾匹配, 当为false时, 表示 /a 生成的正则,  也可以匹配 /a/b
            regExp: pathRegexp.pathToRegexp(path, keys, {end: false}),
            /**path里面的模糊参数, 顺便也存起来 */
            keys: keys
        }
        this.stack.push(layer); // 使用use函数将中间函数放入stack
    }
}

对应handle里面的next函数改为

function next() {
        const layer = stack[index];
        index++;
        if (layer) {
            if ( layer.path == '' || req.url.match(layer.regExp)) { // layer.path = '' 表示全路由匹配, 
                layer.fn(req, res, next);
            } else {
                next();
            } 
        } else {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            done(req, res); 
        }
    }

分别访问 /test 和 / test/2 控制台打印结果一致

3. 支持多个中间函数作为参数, 异常处理函数

在第二点中已经完成了多个中间函数作为参数的逻辑
接下来要做的就是异常处理, 异常处理其实也可以当成是最后一个中间函数,跟最开始自定义的done类似, 异常函数按照规范有4个参数, 第一个为err.

判断是不是异常处理函数, 只需要判断函数的length是不是4即可, 因为函数的length表示函数定义是参数的长度

修改next函数如下, 添加普通中间件函数和异常处理函数的判断

function next(err) {
        const layer = stack[index];
        index++;

        if (!layer) {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            return done(req, res);
        }

        if (err) {
            // 函数的lenth返回的是该函数定义时参数的长度, 为4说明是错误处理函数
            if (layer.fn.length == 4) {
                return layer.fn(err, req, res, next);
            }
            return next(err);
        }

        if (layer.path == '' || req.url.match(layer.regExp)) { // layer.path = '' 表示全路由匹配, 
            // 错误处理函数也是全匹配的, 所以要判断函数参数长度
            if (layer.fn.length == 3) {
                return layer.fn(req, res, next);
            }
        }
        next(err);
    }

修改appDemo.js

const expressDemo = require('./expressDemo');
const app = expressDemo();

app.use(function(req, res, next) { // 中间件1
    console.log('test express demo 1111');
    next()
});


app.use('/err', function(req, res, next) { 
    next(new Error('new error')) // 抛异常
});

app.use(function(err, req, res, next) { // 错误处理函数
    console.log('catch err', err);
    res.end('error');
});

app.listen(9005, function () {
    console.log('server is running');
})

访问 /err 得到预期结果,
因为错误处理函数处理外之后没有继续调用next,所以我们定义的最终done函数不会执行.
在这里插入图片描述

4. 解决req.params 和 req.query 获取请求参数

使用express框架的话, 知道可以通过req.query和req.params获取路由相关的参数

  • 对于路由 /a/:b 访问 a/1 时 req.params = {b: 1}
  • 访问 /a?c=2 时, req.query ={c:2}

接下来我们要做的就是实现这两个获取路由参数的功能

req.query

实际上我们只需要解析请求url 里面 ?后面的字符串就可以得到想要的结构,
这里用两个模块,
parseurl 可以用来将url ? 后面的参数提取出来,
qs 则是将提取出来的字符串转为json

测试下这两个模块的使用 .
在这里插入图片描述
因此expressDemo.js做如下修改

module.exports = function () {
    const app = function (req, res) {
        app.handle(req, res);
    }
    Object.assign(app, application); // application对象的属性全部复制到app
    app.stack = [];

    // 默认就启用 req.query的中间函数
    app.use(function (req, res, next) {
        // 将url解码
        req.url = decodeURIComponent(req.url);
        // 解析url, 得到解析对象 
        const result = parseUrl(req); 
        const query = qs.parse(result.query);
        req.query = query;
        next();
    })
    return app;
}

稍微修改appDemo.js 访问 /err?a=1, 得到预期结果
在这里插入图片描述

req.params

req.params的生成既需要知道路由定义时的模糊参数, 也需要知道路由处理时的实际参数值,
所以我们在use函数里面获取路由里面的参数名
在handle – next 函数里面获取路由实际的参数值

上面我们用到了path-to-regexp, 同时我们在layer中还存储了一个keys数组, 在这里都会用到.

那么写一个关于path-to-regexp的测试函数

const pathRegexp = require('path-to-regexp');

const reg = pathRegexp.pathToRegexp('/user/:aa/:bb', keys = [], {end: false});
const result = '/user/2/5'.match(reg);
console.log(result)
console.log(keys)

执行得到的结果如下, 可以看到
keys里面, 每个元素的name对应路由的参数名,
result里面的结果对应实际的参数值
并且是一一对应关系.
在这里插入图片描述
前面我们在layer中存储了一个keys, 里面可以获取路由对应的参数名,
通过str.match, 我们可以获取到对应的匹配结果,

那么修改use函数, layer新增一个params属性, 用来保存处理请求过程中解析路由产生的 params

 // 把中间函数和对应的path组成一个对象, 在next函数里面就可以根据路由来判断中间函数是否执行了
        const layer = { 
            path: path,
            fn: middleware,
            // end: false 表示是否结尾匹配, 当为false时, 表示 /a 生成的正则,  也可以匹配 /a/b
            regExp: pathRegexp.pathToRegexp(path, keys, {end: false}),
            /**path里面的模糊参数, 顺便也存起来 */
            keys: keys,
            params: {}, // 表示该中间函数处理请求时解析得到的params
        }

修改handle里面的next函数, 通过正则匹配获取路由里面每个参数的值,
然后将得到的params 赋值给 req.params, 那么就实现了req.params获取参数的功能,
写到这里可以发现, req.params不同于req.query, 需要依赖于use函数中的路由值才能生成

function next(err) {
        const layer = stack[index];
        index++;

        if (!layer) {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            return done(req, res);
        }

        if (err) {
            // 函数的lenth返回的是该函数定义时参数的长度, 为4说明是错误处理函数
            if (layer.fn.length == 4) {
                return layer.fn(err, req, res, next);
            }
            return next(err);
        }

        const matchResult = req.url.match(layer.regExp); // 获取match结果
        if (matchResult) {
            for (let i = 0; i < layer.keys.length; i++) {
                // 将 路由参数名 和对应的值组合得到params
                layer.params[layer.keys[i].name] = matchResult[i + 1];  
            }
        }
         // layer.params赋值给req.params
        req.params = layer.params || {};
        if (layer.path == '' || matchResult) { // layer.path = '' 表示全路由匹配, 
            // 错误处理函数也是全匹配的, 所以要判断函数参数长度
            if (layer.fn.length == 3) {
                return layer.fn(req, res, next);
            }
        }
        next(err);
    }

修改appDemo.js

const expressDemo = require('./expressDemo');
const app = expressDemo();

app.use(function(req, res, next) { // 中间件1
    console.log('test express demo 1111');
    console.log('req.query', req.query)
    console.log('req.params', req.params)
    next()
});


app.use('/test/:aa', function(req, res, next) { 
    console.log('req.params', req.params)
    res.end(req.params.aa);
});

app.use(function(err, req, res, next) { // 错误处理函数
    console.log('catch err', err);
    res.end('error');
});

app.listen(9005, function () {
    console.log('server is running');
})

访问 /test/11 得到如下预期结果
在这里插入图片描述

5. 支持路由分发, 每个express实例处理的路由独立

如果完成上一步, 那么已经解决了一部分问题了
但是这么写还存在不少问题,
1 实际开发过程中, 不可能所有的路由都写在一个文件里面, 正常应该是一个文件对应一类路由。
如果使用该框架, 那么每个文件都必须使用同一个express对象, 也就是express对象必须作为参数传递到每一个文件中, 这么写起来看着也不舒服.
2 那如果某类路由需要用一种中间价, 某类路由需要用另外一种中间件, 只能用路由来划分中间件的使用范围, 比如下面这种, 其实写起来就发现前面的路由有些多余.

// user.js
app.use('/user', mid1)

// note.js
app.use('/note', mid2)

用过express都知道express有如下用法

/// app.js
app = express()
app.use('/user', require('./user'))
app.use('/note', require('./note'))


/// user.js
var router = express.Router()
router.use('/create', function(){}) // 相当于 /user/create
module.exports = router;

/// note.js
var router = express.Router()
router.use('/delete', function(){})  // 相当于 /note/delete
module.exports = router;

这种写法避免了传递express对象, 同时对于一类路由, 在app.js中已经注册的话, 就不用写路由前缀了

接下来我们就结局是上面提到的两个问题
原理就是把express对象作为中间件函数,
循环过程中, 当有一个函数是express对象时, 调用该对象的handle方法,
同时把上层的next函数作为第三个参数穿进去也就是 handle(req, res, next)
在这里插入图片描述

回到最初的expressDemo文件, 看到
这里我们并没有跟加上next参数, 因为http.createServer的回调函数本身就不存在额外的第三个参数,

 const app = function (req, res) {
        app.handle(req, res);
    }
    Object.assign(app, application); // application对象的属性全部复制到app
    app.stack = [];

但是现在,当express对象作为中间函数时, 存在第三个参数next了, 该next是上一层的next函数

对expressDemo做如下修改, 添加了next参数

module.exports = function () {
    const app = function (req, res, next) {
        app.handle(req, res, next);
    }
    Object.assign(app, application); // application对象的属性全部复制到app
    app.stack = [];

    // 默认就启用 req.query的中间函数
    app.use(function (req, res, next) {
        // 将url解码
        req.url = decodeURIComponent(req.url);
        // 解析url, 得到解析对象 
        const result = parseUrl(req); 
        const query = qs.parse(result.query);
        req.query = query;
        next();
    })
    return app;
}

再次回到handle函数 可以看到这里的callback参数,

  • 当express对象不是中间件函数 ( 子express对象 ) 时, callback是空, 所以done函数使我们自定义的一个最终函数,
  • 当express对象是中间件函数时, callback是上层express对象(父express对象)传进来的next函数,

当子express对象里面stack的函数执行完毕, 调用done函数, 就又回到了外层express对象里面, 继续调用外层express对象的stack 里面的下一个函数.

application.handle = function (req, res, callback) {
    const stack = this.stack;
    // 默认是不存在callback的, 如果执行到最后没有中间函数,
    // 我们必须要有一个最终的函数给客户端返回, 这里就是定义最后的一个执行函数
    const done = callback || function (req, res) {
        console.error('final');
        res.end('final');
    }

    let index = 0;

    function next(err) {
        const layer = stack[index];
        index++;

        if (!layer) {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            return done(req, res);
        }

        if (err) {
            // 函数的lenth返回的是该函数定义时参数的长度, 为4说明是错误处理函数
            if (layer.fn.length == 4) {
                return layer.fn(err, req, res, next);
            }
            return next(err);
        }

        const matchResult = req.url.match(layer.regExp); // 获取match结果
        if (matchResult) {
            for (let i = 0; i < layer.keys.length; i++) {
                // 将 路由参数名 和对应的值组合得到params
                layer.params[layer.keys[i].name] = matchResult[i + 1];  
            }
        }
         // layer.params赋值给req.params
        req.params = layer.params || {};
        if (layer.path == '' || matchResult) { // layer.path = '' 表示全路由匹配, 
            // 错误处理函数也是全匹配的, 所以要判断函数参数长度
            if (layer.fn.length == 3) {
                return layer.fn(req, res, next);
            }
        }
        next(err);
    }

    next();
}

实际上我们只做了微小的修改之后(app函数里面添加了参数next)就已经实现了路由分发功能了,
修改appDemo.js
新建两个express对象, 然后都作为父express对象的中间函数,
注意: app3对应的路由并没有加上 app3前缀

const expressDemo = require('./expressDemo');
const app = expressDemo();

const app2 = expressDemo();
const app3 = expressDemo();

app.use('/app2', app2);
app.use('/app3', app3);

app2.use('/app2/2', function (req, res, next) {
    res.end('/app2/2')
})
app3.use('/3', function (req, res, next) {
    res.end('/app3/3')
})


app.use(function(err, req, res, next) { // 错误处理函数
    console.log('catch err', err);
    res.end('error');
});

app.listen(9005, function () {
    console.log('server is running');
})

分别访问/app3/3 和 /app2/2, 得到结果如下,
在这里插入图片描述
发现/app2/2正常匹配到
/app3/3 的路由没有匹配到,
显然即便用express对象作为中间函数, 还是需要路由整个匹配, 那么如果想要实现express的那种结果
就像如下: 在user.js里面可以省略/user的路由前缀

/// app.js
app = express()
app.use('/user', require('./user'))
app.use('/note', require('./note'))


/// user.js
var router = express.Router()
router.use('/create', function(){}) // 相当于 /user/create
module.exports = router;

现在需要我们做路由拼接, 对use函数做如下修改
主要是在express对象作为中间函数时, 将传进去的path作为路由前缀存储起来

application.use = function (path) {
    let callbacks;
    if (typeof path !== 'string' && typeof path !== 'function') {
        throw new Error('application.use 第一个参数必须为字符串或者函数');
    }
    if (typeof path == 'string') {
        if (path == '*' || path == '/') {
            path == '';
        }
        callbacks = Array.prototype.slice.call(arguments, 1); // 从第二个参数开始都认为是中间函数
        if (callbacks.length == 0) {
            throw new Error('application.use 需要至少一个中间函数');
        }
    }
    if (typeof path == 'function') {
        // 转为数组, arguments只是类数组结构而已
        callbacks = Array.prototype.slice.call(arguments, 0); 
        path = ''; // 该代码放在后面, 放在前面相当于给path重新赋值, 导致第一个中间函数丢失
    }
    for (let i = 0; i < callbacks.length; i++) {
        const middleware = callbacks[i];
        // 说明middleware是express对象, 
        if (middleware.handle ) { 
            // middleware.prefix有值说明已经被作为中间函数了, 
            if (middleware.prefix) {
                continue; 
            }
            middleware.prefix = path;
            // 如果该express对象提前使用了use函数, 那么作为中间函数时, 更新每个layer的相关路由属性
            for (let layer of middleware.stack) {
                const keys = []
                layer.path = middleware.prefix + layer.path;
                layer.regExp = pathRegexp.pathToRegexp(path, keys, {end: false})
                layer.keys = keys;
            }
        }
        // keys是用来存储path里面的模糊字段的, 比如path = /a/:b/:c 
        // 那么经过pathRegexp, keys就会变为 [{name: 'b', xxx}, {name: 'c'}] 这种形式
        const keys = [];
        // 完整的path为 express对象的prefix + path
        const regExp = pathRegexp.pathToRegexp(this.prefix + path, keys, {end: false});
        // 把中间函数和对应的path组成一个对象, 在next函数里面就可以根据路由来判断中间函数是否执行了
        const layer = { 
            path: this.prefix + path,
            fn: middleware,
            // end: false 表示是否结尾匹配, 当为false时, 表示 /a 生成的正则,  也可以匹配 /a/b
            regExp: regExp,
            /**path里面的模糊参数, 顺便也存起来 */
            keys: keys,
            params: {}, // 表示该中间函数处理请求时解析得到的params
        }
        this.stack.push(layer); // 使用use函数将中间函数放入stack
    }
}

再次访问 /app3/3 得到理想结果
在这里插入图片描述

其实最开始为什么将 空字符串 作为 全路由匹配而不是 * 和这 /, 主要是因为要对这种路由拼接做处理

  • 假如 * 作为 全匹配标识, 那么拼接得到的路由可能就是 /user*,
  • 假如 / 作为 全匹配标识, 那么拼接得到的路由可能就是 /user/,

都需要对路由进行再处理, 所以最开始使用的空字符串作为匹配所有路由的标识, 懒~

6. 支持各种请求方法

现在我们的代码对各种请求方法都一视同仁, 但是实际情况中, 每种请求方法都有它自己的含义, 要设计出restful风格的接口, 必须提供对应的方法

实现app.get函数, 实际跟app.use是一模一样的, 只不过是在next函数中找匹配函数时, 还需要匹配请求方法

修改use函数, 该函数返回一个layer数组

application.use = function (path) {
    const returnLayer = []; // 保存生成的各个layer
    let callbacks;
    if (typeof path !== 'string' && typeof path !== 'function') {
        throw new Error('application.use 第一个参数必须为字符串或者函数');
    }
    if (typeof path == 'string') {
        if (path == '*' || path == '/') {
            path == '';
        }
        callbacks = Array.prototype.slice.call(arguments, 1); // 从第二个参数开始都认为是中间函数
        if (callbacks.length == 0) {
            throw new Error('application.use 需要至少一个中间函数');
        }
    }
    if (typeof path == 'function') {
        // 转为数组, arguments只是类数组结构而已
        callbacks = Array.prototype.slice.call(arguments, 0); 
        path = ''; // 该代码放在后面, 放在前面相当于给path重新赋值, 导致第一个中间函数丢失
    }
    for (let i = 0; i < callbacks.length; i++) {
        const middleware = callbacks[i];
        // 说明middleware是express对象, 
        if (middleware.handle ) { 
            // middleware.prefix有值说明已经被作为中间函数了, 
            if (middleware.prefix) {
                continue; 
            }
            middleware.prefix = path;
            // 如果该express对象提前使用了use函数, 那么作为中间函数时, 更新每个layer的相关路由属性
            for (let layer of middleware.stack) {
                const keys = []
                layer.path = middleware.prefix + layer.path;
                layer.regExp = pathRegexp.pathToRegexp(path, keys, {end: false})
                layer.keys = keys;
            }
        }
        // keys是用来存储path里面的模糊字段的, 比如path = /a/:b/:c 
        // 那么经过pathRegexp, keys就会变为 [{name: 'b', xxx}, {name: 'c'}] 这种形式
        const keys = [];
        // 完整的path为 express对象的prefix + path
        const regExp = pathRegexp.pathToRegexp(this.prefix + path, keys, {end: false});
        // 把中间函数和对应的path组成一个对象, 在next函数里面就可以根据路由来判断中间函数是否执行了
        const layer = { 
            path: this.prefix + path,
            fn: middleware,
            // end: false 表示是否结尾匹配, 当为false时, 表示 /a 生成的正则,  也可以匹配 /a/b
            regExp: regExp,
            /**path里面的模糊参数, 顺便也存起来 */
            keys: keys,
            params: {}, // 表示该中间函数处理请求时解析得到的params,
            method: 'all', // 该中间函数对应的请求方法
        }
        this.stack.push(layer); // 使用use函数将中间函数放入stack
        returnLayer.push(layer);
    }
    return returnLayer
}

在expressDemo.js最后面添加一段代码, 这段代码就完成了所有的请求方法对应的函数处理


const METHODS = ['get', 'post', 'delete', 'put', 'all'];

for(let method of METHODS) {
    application[method] = function () {
        const layers = this.use.apply(this, arguments);
        for (let layer of layers) {
            layer.method = method
        }
    }
}

修改handle中next函数的匹配规则, 添加请求方法匹配

function next(err) {
        const layer = stack[index];
        index++;

        if (!layer) {
            // 当不存在中间函数时, 如果前面的函数没有返回值给客户端, 那么在这个最终函数处理
            return done(req, res);
        }

        if (err) {
            // 函数的lenth返回的是该函数定义时参数的长度, 为4说明是错误处理函数
            if (layer.fn.length == 4) {
                return layer.fn(err, req, res, next);
            }
            return next(err);
        }

        const matchResult = req.url.match(layer.regExp); // 获取match结果
        if (matchResult) {
            for (let i = 0; i < layer.keys.length; i++) {
                // 将 路由参数名 和对应的值组合得到params
                layer.params[layer.keys[i].name] = matchResult[i + 1];  
            }
        }
         // layer.params赋值给req.params
        req.params = layer.params || {};
        // layer.path = '' 表示全路由匹配,  layer.method == 'all' 表示请求方法全匹配
        if ((layer.path == '' || matchResult) && (layer.method == 'all' || layer.method.toUpperCase() == req.method)) { 
            // 错误处理函数也是全匹配的, 所以要判断函数参数长度
            if (layer.fn.length == 3) {
                return layer.fn(req, res, next);
            }
        }
        next(err);
    }

修改appDemo.js

const expressDemo = require('./expressDemo');
const app = expressDemo();

app.post('/method', function (req, res, next) {
    res.end('post method')
})

app.get('/method', function (req, res, next) {
    console.log('get method 1')
     next();
}, function (req, res, next) {
    res.end('get method 1');
})


app.use(function(err, req, res, next) { // 错误处理函数
    console.log('catch err', err);
    res.end('error');
});

app.listen(9005, function () {
    console.log('server is running');
})

浏览器访问 /method (get方法) 得到预期结果
在这里插入图片描述

细节优化

到上一步为止, 我们已经完成了express的完整工作流程, 接下来就是完善各种细节, 将各个功能独立到文件中, 如果能理解这些代码, 相信再看express不至于那么绕了.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值