实现express之二 : 分析express源码

601 阅读10分钟

自己曾深入的学习了一下express的源码,然后,自己根据源码,复写了一套相对简单点的express框架。 express的源码我认为从大的功能点上看,可以分为两部分:对静态资源的处理和对非静态资源的处理。在上一篇中,用几十行代码写了一个最最简单而又非常重要的微型框架,把这两部分进行涵盖。传送门:实现express之一:通过几十行代码实现一个最简单的框架

一、框架入口逻辑

看任何框架的源码,都跟我们吃一个皮皮虾一样,需要知道从哪里入口。(哈哈,纯粹是想吃了)。同样,要实现一个express,在上面的有基础原理武装基础上,就要实现入口逻辑。

/**
 * @file 使用自己写的express 创建node服务
 */

const express = require('./express/express');
const ejs = require('ejs');
const app = express();


app.use(express.static('public'));

app.use((req, res, next) => {
    console.log('所有的请求都会走这里');
    next();
});

app.get('/', (req, res) => {
    ejs.renderFile('./views/info.ejs', {
        title: 'info页面',
        description: 'info描述描述。。。'
    },
    (err, data) => {
        if (err) {
            console.log('render page error:', err);
            return;
        }
        res.end(data);
    });
});

app.listen(8000, () => {
    console.log('server started on port 8000');
})

重点来了, 通过上面的代码,我们可以简单分析出以下逻辑:

  • express本身是个函数
  • 函数有返回值(app变量),只是现在这个app是对象还是函数不好说
  • express函数有静态方法(static)

1.1 express init逻辑

进入逻辑看一下:

const mixin = require('merge-descriptors');
const EventEmitter = require('events').EventEmitter;
const proto = require('./application');

exports = module.exports = createApplication;

function createApplication() {
    let app = function(req, res, next) {
        // 执行路由请求时会执行
        app.handle(req, res, next);
    };
    // 将 EventEmitter.prototype混合给app, 那么app也就有这些方法了,都属于app的静态方法
    mixin(app, EventEmitter.prototype, false);
    // 把自己定义的方法,都也赋值给app方法,单纯为了让主入口代码更简单
    mixin(app, proto, false);
    app.init();
    return app;
}

exports.static = require('./static_service');

上面的代码中, application.js是处理express整体逻辑的,static_service是处理静态文件逻辑的。那么,我们先进入application.js,因为这里有整体逻辑,比如app.use就是这里面的。

// 首先执行 app.init(),而后又执行app.defaultConfiguration,初始化express的内部中间件
app.defaultConfiguration = function() {
    const env = process.env.NODE_ENV || 'development';
    this.set('env', env);
    // 这里是query中间件的初始化处理,后面会调用
    this.set('query parser', 'extended');
    this.set('trust proxy', false);
};

// 再次,初始化methods的监听方法,post/get/put/delete/....
methods.forEach(function(method) {
    app[method] = function(path) {
        if (method === 'get' && arguments.length === 1) {
            // ex: app.get('title')
            return this.set(path);
        }
        // express中是把path和对应的callback放到了router中
        // router专门来管理所有与请求相关的逻辑
        // 首先,如果没有实例化过基础Router的话,先进行实例化
        this._router = this._router || this.lazyrouter();
        // get/post/...并没有调用this._router.use,,
        // 而是调用的 this._router.route()
        // 这两种是有区别的
        let route = this._router.route(path);
        // 把cb传到route[method]中进行保存
        route[method].apply(route, slice.call(arguments, 1));
    }
});

// 第三点是创建app.use

app.use = function(fn) {
    let offset = 0; // 默认slice到哪个位置
    let path = '/'; // 默认path是根路由

    // 如果是app.use('aa/bb', function() {]})的情况
    // 或者是 app.use([fn1, fn2]) 类似的情况
    if (typeof fn !== 'function') {
        let arg = fn;
        while(Array.isArray(arg) && arg.length !== 0) {
            arg = arg[0];
        }

        if (typeof arg !== 'function') {
            offset = 1;
            path = fn;
        }
    }
    // 将多层的回调,处理成扁平化的一层数组
    let fns = flatten(slice.call(arguments, offset));
    if (fns.length === 0) {
        throw new TypeError('app.use() requires a middleware function')
    }
    // 设置router
    const router = this._router = this.lazyrouter();
    // 把use中的回调函数进行遍历
    fns.forEach(function(fnItem) {
        if (!fnItem || !fnItem.handle || !fnItem.set) {
            // 现在的使用情况会进这个逻辑,去调用了router的use方法, 创建layer实例等操作
            return router.use(path, fnItem);
        }
    });
    return this;
}


二、 app.use 逻辑

app.use(express.static('public')) 做了什么?

当执行上面这句,那就说明要执行app.use了,在执行app.use函数中,this._router如果没有,则执行 lazyrouter

app.lazyrouter = function() {
    if(!this._router) {
        const router = new Router({
            caseSensitive: this.enabled('case sensitive routing'), // 大小写敏感,默认不敏感
            strict: this.enabled('strict routing') // 激活严格路由,默认不启用,/uu和/uu/ 都可以正常访问
        });
        // express中默认的两个中间件: query 和 init,这里对应的是上面设置的
        // query -> 把query进行parse
        router.use(query(this.get('query parser fn')));
        // TODO 实现middleware.init
        // router.use(middleware.init(this));
        return router;
    }
    return this._router;
    
}

query(this.get('query parser fn'))这里的this.get('query parser fn'),就是通过上面的 this.set('query parser', 'extended');处理的

app.set = function(setting, value) {
    if (arguments.length === 1) {
        return this.settings[setting];
    }
    debug('set "%s" to %o', setting, value);
    this.settings[setting] = value;
    switch (setting) {
        case 'query parser':
            this.set('query parser fn', compileQueryParser(value));
            break;
        default:
            break;
    }
}

compileQueryParser(value)其实做的是,是使用node自带的querystring来解析query,还是用npm 模块 qs解析query

既然现在看到了实现 router.use(query(this.get('query parser fn')));, 那么就仔细看看app.use执行的逻辑吧。

很显然,关键点在于 new Router(...).use, 这里面做的是创建layer,并且把layer存储到this.stack中。以后在执行的时候,会从this.stack中把一个个layer拿出来执行

// 这里的proto就是 Router函数
// router/index.js route 入口
const proto = module.exports = function(options) {
    let opts = options || {};

    let router = (req, res, next) => {
        router.handle();
    }
    router.params = {};
    router._params = {};
    // 是否对大小写敏感
    router.caseSensitive = opts.caseSensitive;
    router.mergeParams = opts.mergeParams;
    // 是否启用严格模式
    router.strict = opts.strict;
    router.stack = [];
    // 将proto方法整个复制到router方法的__proto__上
    setPrototypeOf(router, proto);
    // 也就是说外边用new Router(...) 返回的是这个router函数。
    // 然后这个router函数的__proto__上带着proto的所有静态方法
    return router;
}

proto.use = function(fn) {
    let offset = 0;
    let path = '/';

    if (typeof fn !== 'function') {
        offset = 1;
        path = fn;
    }
    // 比如 use(fn1, fn2, fn3)、 use(fn1,[fn2, fn3])等等情况
    let callbacks = flatten(slice.call(arguments, offset));
    // 其实照最简单实现来说,把回调函数存到stack中就行,但是单独放到了一个Layer构造函数中,就说明还有其它工作要做
    for(let i = 0; i < callbacks.length; i++) {
        let fn = callbacks[i];
        if (typeof fn !== 'function') {
            throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn));
        }
        debug('use %o %s', path, fn.name || '<anonymous>');

        const layer = new Layer(path, {
            sensitive: this.caseSensitive,
            strict: false,
            end: false
        }, fn);
        // 这里注意一下,通过use加载的中间件,layer.route会赋值为undefined,后面会有不一致的地方进行对应,
        layer.route = undefined;
        this.stack.push(layer);
    }
    return this;
}

再看一下Layer中做了什么


function Layer(path, options, fn) {

}
// 错误处理
Layer.prototype.handle_error = function(err, req, res, next) {
// ...
}
// 正常处理回调函数
Layer.prototype.handle_request = function(req, res, next) {
// ...
}

// 路径匹配函数
Layer.prototype.match = function(path) {
    // ...
}

这一条线现在先讲到这。再继续将肯定大家就晕的一塌糊涂了。所以需要把这个阶段画图来展示:

image.png

image.png

image.png

希望通过上面的代码和详细的路径图能让大家更容易理解一些。

三、 Method请求逻辑

如果上面的逻辑感觉有些明白了,那么咱们可以继续了。

3.1 method的方法定义逻辑

为了保持讲解的简单和一致, method的方法只用 app.get()来代替,其实有很多的方法,具体可参见 npm methods

设置好应用级的中间件后,那么就应该设置路由级的中间件了

app.get('/', (req, res) => {
    // ....
})

app.post('/login', (req, res) => {
    // ...
})


为什么我会称之为路由级的中间件呢? 原因在于,我们其实是可以在Method请求的回调中加入next的,比如:

    app.get('/', (req, res, next) => {
        // ....
        next();
    })

当然,由于我们一般不会在路由请求处理中加入next,大家知道有这么一回事就行。这不作为讨论点。但是,我们需要好好看看这个路由级中间件在express中是如何实现的。

其实,在这篇刚开始的时候,我给大家提示了一下,现在详细拿出来说。代码就是这段:

// 再次,初始化methods的监听方法,post/get/put/delete/....
methods.forEach(function(method) {
    app[method] = function(path) {
        if (method === 'get' && arguments.length === 1) {
            // ex: app.get('title')
            return this.set(path);
        }
        // express中是把path和对应的callback放到了router中
        // router专门来管理所有与请求相关的逻辑
        // 首先,如果没有实例化过基础Router的话,先进行实例化
        this._router = this._router || this.lazyrouter();
        // get/post/...并没有调用this._router.use,,
        // 而是调用的 this._router.route()
        // 这两种是有区别的
        let route = this._router.route(path);
        // 把cb传到route[method]中进行保存
        route[method].apply(route, slice.call(arguments, 1));
    }
});

methods是一个npm包,有兴趣的可以看下,里面有所有的请求方式。 其实上面做的就是把这些所有的请求方式挂载上。 所以我们可以直接在业务代码中直接使用app.get()app.post()

因为我们已经有this._router了,所以可以直接使用 this._router.route(path), 这里逻辑跟app.use()类似,相同点在于,都是创建了一个layer实例,并且把这个layer实例push进this.stack。但是区别更加重要。 区别点在于:

  1. 专门又实例化了一个Route,这个Route中,也存储了一套stack(调用栈)和Method方法处理
  2. layer实例中的route变量赋值了。 在app.use()中,赋值的是undefined,这里有很多的不同,后面详细说。
  3. 并没有把开发者的cb放入layer中,而是放在了route[method].apply(route, slice.call(arguments, 1));中。

下面详细看下代码:

let route = this._router.route(path);


proto.route = function(path) {
    // 在其中实例化了一个Route
    let route = new Route(path);

    // 注意这个layer的调用和app.use的不同,记住当执行这个layer中回调的时候,执行的是route.dispatch
    const layer = new Layer(path, {
        sensitive: this.caseSensitive,
        strict: this.strict,
        end: true
    }, route.dispatch.bind(route));
    // 这里把route进行了赋值,而使用app.use时,是赋值为undefined
    // 这里也就是说把route实例保存到layer中,然后把layer存储到this.stack中
    layer.route = route;
    this.stack.push(layer);
    return route;
}

看看 new Route 做了什么

function Route(path) {
    this.path = path;
    // route中维护着自己的stack,这里面存储的其实是get、post请求的callback
    this.stack = [];

    this.methods = {};
}


// route实例中主要有stack、methods,后面在执行的时候再说

回到 route[method].apply(route, slice.call(arguments, 1));这句。 其实在new Route()的时候,就已经先进行了对Method的处理。

methods.forEach(function(method) {
    Route.prototype[method] = function() {
        // 开发者写的回调
        const handles = flatten(slice.call(arguments));

        for(let i = 0; i < handles.length; i++) {
            let handle = handles[i];
            if (typeof handle !== 'function') {
                let type = toString.call(handle);
                let msg = "Route." + method + '() requires a callback function but got a ' + type;
                throw new Error(msg);
            }
            // Layer内部还是会实例化
            var layer = Layer('/', {}, handle);
            // 加一个静态属性,为了区分这个layer属于get还是post。。。
            layer.method = method;

            this.methods[method] = true;
            this.stack.push(layer);
        }
        return this;
    }
});

通过这段代码,我相信大家可能脑子有些乱,怎么又对methods进行了遍历?

走到这里,我们有必要把 app.use()app.get() 内部的结构做一个详尽的解析了。

3.2 app.use() 和 app.get() 内部回调的存储方式解析

这里有两个执行栈(stack),一是app.use(),执行route.use, 和app.get()执行route.route时,都会new Layer()并且push到 this.stack, 这里没有什么区别。但区别之处在于:

  • app.use()执行后产生的layer,把layer.route 赋值为undefined, 而 app.get()后产生的layer,会把new Route()的实例赋值给layer.route。 (layer.route = route 这里还记得么?在上面的代码中有)
  • app.use()会把内置中间件或者开发者的中间件直接作为layer的第三个参数传入,作为layer执行时的回调。而app.get() 并没有直接把开发者的中间件传入, 而是用的 new Layer(path, {...}, route.dispatch.bind(route)) dispatch方法后面再详细说,但它的作用是执行route中自己维护的stack
  • app.get()中使用 route[method].apply(route, slice.call(arguments, 1));, 把开发者的callback存到Route自己的stack中。

还是再继续画图表示一下:

image.png

image.png

我想,大家大概能看出其中的道道了。 为什么要分开搞,那就是为了让流程上更清楚, 各人负责各人的逻辑。 如果都糅杂到一起,相信随着项目的增大,可维护性会越来越低。

四、app.use() 和app.get() 中callback的执行

根据前三步, 开发者要监听的应用级别和中间件和路由级别中间件中的callback都保存到应该保存的地方了, 下一步就仔细来看当一个请求真正打过来的时候,是如何执行的。

4.1 next是什么

相信使用过express的同学都知道有个next的存在。 很多人感觉又重要,又缥缈。 重要在于应用级中间件中如果不调用next(),那么程序就卡死了。 缥缈是因为不清楚next到底做了什么。

今天,我来带着大家揭开next神秘的面纱吧。

我们知道如果要有http服务,那就需要创建一个http服务器,在express中,这个细节被隐藏到了listen函数中。

app.listen = function() {
    let server = http.createServer(this);
    return server.listen.apply(server, arguments);
}

所以,当一个请求来了以后,会首先执行:

// express.js

let app = function(req, res, next) {
    // 执行路由请求时会执行
    app.handle(req, res, next);
};

// application.js
app.handle = function(req, res, next) {
    const router = this._router;
    // req 的处理中作为最后的兜底处理器处理错误
    let done = next || finalhandler(req, res, {
        env: this.get('env'),
        onerror: logerror.bind(this)
    });

    if (!router) {
        done();
        return;
    }
    // next函数的出处在这里!!
    router.handle(req, res, done);
}

router.handle(req, res, done) 是next函数实现的精华所在。 原理是在执行this.stack中的layer完成后,如果存在next()方法,则从this.stack中再拿出一个layer继续执行, 一直执行到this.stack为空,或者在app.get() 实现中直接res.end等不再需要继续执行的情况。

proto.handle = function(req, res, out) {
    debugger;
    let self = this;
    let idx = 0;
    let stack = this.stack;
    let parentUrl = req.baseUrl || '';
    // 一个请求结束后触发,无论是执行栈为空了,还是执行过程中出现问题了
    let done = restore(out, req, 'baseUrl', 'next', 'params');

    req.next = next;
    req.baseUrl = parentUrl;
    req.originalUrl = req.originalUrl || req.url;
    next();

    function next(err) {
        let layerError = err === 'route'
            ? null
            : err;

        if (layerError === 'router') {
            setImmediate(done, null);
        }
        // 栈中已经没有Layer了
        if (idx >= stack.length) {
            setImmediate(done, layerError);
            return;
        }

        let path = getPathname(req);
        if (path === null || path === undefined) {
            return done(layerError);
        }

        let layer;
        let match;
        let route;

        while(match !== true && idx < stack.length) {
            layer = stack[idx++];
            match = matchLayer(layer, path);
            // 拿到route,仅在app.get()的情况下
            route = layer.route;
            // 如果不匹配当前path
            if (match !== true) {
                continue;
            }
            // 在非路由中间件的情况下,route为undefined
            if (!route) {
                continue;
            }

            if (layerError) {
                match = false;
                continue;
            }
        }

        if (match !== true) {
            return done(layerError);
        }
        if (route) {
            req.route = route;
        }

        req.params = layer.params;
        let layerPath = layer.path;

        self.process_params(layer, {}, req, res, (err) => {
            if (err) {
                return next(err);
            }
            if (route) {
                // app.get(), app.set() ....
                return layer.handle_request(req, res, next);
            }

            trim_prefix(layer, layerError, layerPath, path);
        });
    }
    // TODO 这里的逻辑需要重新看
    function trim_prefix(layer, layerError, layerPath, path) {
        // TODO 需要处理
        if (layerPath.length !== 0) {
            // TODO
        }
          
        if (layerError) {
          layer.handle_error(layerError, req, res, next);
        } else {
          layer.handle_request(req, res, next);
        }
    }
}

最重要的是会把next函数传给各个callback中,比如说,app.use(function(req, res, next){}) 这个next就是上面的next函数。 所以, 当callback执行完成后,调用 next(), 则就会取下一个layer,执行。

app.get() 在执行时具体怎么处理

其实这个问题我们在上面也有说,基本原理,无非就是走到application级别的layer时, 因为之前有把route实例赋值给layer,也就是 layer.route = route (为了加深印象,这句出现多次了), 然后代码层面就会去执行route中的逻辑。 因为route实例中存在自己的stack, 那么执行route中的stack也就理所当然了。

// router/route.js

Route.prototype.dispatch = function(req, res, done) {
    let idx = 0;
    let stack = this.stack;
    if (stack.length === 0) {
        return done();
    }
    let method = req.method.toLowerCase();
    if (method === 'head' && !this.methods['head']) {
        method = 'get';
    }
    // 把实例赋值到req.route
    req.route = this;
    // 开始执行栈中内容
    next();
    function next(err) {
        // 命中退出route的信号
        if (err && err === 'route') {
            return done();
        }
        // 命中退出router的信号
        if (err && err === 'router') {
            return done(err);
        }
        // 注意这个layer是route实例中的,并不是application级别的
        let layer = stack[idx++];
        if (!layer) {
            return done(err);
        }
        if (layer.method && layer.method !== method) {
            // 如果匹配不上,则去栈中找下一项执行
            return next();
        }
        
        if (err) {
            // 如果上面兜不住
            layer.handle_error(err, req, res, next);
        } else {
            // 而且这里面的next是这段代码中的next,不要跟application级别的混了
            layer.handle_request(req, res, next);
        }
    }
}


// layer.js

Layer.prototype.handle_request = function(req, res, next) {
    const fn = this.handle;
    // 如果length >3, 则不是标准的处理函数
    if (fn.length > 3) {
        return next();
    }

    try {
        // 执行app.get(path, (req, res) => {}) 中的callback
        // 所以在这里也可以看到,其实是可以将next传出去的。
        fn(req, res, next);
    } catch (error) {
        next(error);
    }
}

通过手敲这4000多字,希望能给大家一些启发,对express有更深层的认识。

当然,并不是说express的实现方式就一定是最好的,koa也是一个非常好的框架,只是在项目中没有怎么用,也就暂时没有对其源码进行深入的研究。 koa中就不需要next也能够做到,大家有兴趣可以再去koa中搜寻一下。

大家觉得好别忘了点个赞哈,多多的赞,更能激发我不断分享,哈哈。

下一篇,我将分析 express中的静态路由,仔细分析一下 app.use(express.static('public')) 这么一句简单的代码是怎么做到加载静态资源的。传送门:实现express之三: app.use(express.static('public'))的奥秘