Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《看源码学web》【看koa源码学 web 中间件 模型】 #4

Open
gamebody opened this issue Feb 14, 2019 · 0 comments
Open

《看源码学web》【看koa源码学 web 中间件 模型】 #4

gamebody opened this issue Feb 14, 2019 · 0 comments

Comments

@gamebody
Copy link
Owner

koa源码分析

koa的源码很简单,我们来分析一下。
主要下面的两点:

  1. 请求如何流转
  2. 中间件的处理
  3. 错误处理
  4. 内部context

请求如何流转

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
    ctx.body = 'Hello World';
});

app.listen(3000);
// application.js

class Application extends Emitter {
    // ....
    listen(...args) {
        debug('listen');
        const server = http.createServer(this.callback()); // `this.callback()`每次有请求都会促发
        return server.listen(...args);
    }
    // ...

    callback() {
        const fn = compose(this.middleware);

        if (!this.listenerCount('error')) this.on('error', this.onerror);

        const handleRequest = (req, res) => {
            const ctx = this.createContext(req, res);
            return this.handleRequest(ctx, fn);
        };

        return handleRequest;
    }

    // ...
    // 为了能够代理,挂在一些属性
    createContext(req, res) {
        const context = Object.create(this.context);
        const request = context.request = Object.create(this.request);
        const response = context.response = Object.create(this.response);
        context.app = request.app = response.app = this;
        context.req = request.req = response.req = req;
        context.res = request.res = response.res = res;
        request.ctx = response.ctx = context;
        request.response = response;
        response.request = request;
        context.originalUrl = request.originalUrl = req.url;
        context.state = {};
        return context;
    }

    handleRequest(ctx, fnMiddleware) {
        const res = ctx.res;
        res.statusCode = 404;
        const onerror = err => ctx.onerror(err);
        const handleResponse = () => respond(ctx);
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }
}

// 返回不同格式的数据
function respond(ctx) {
    // allow bypassing koa
    if (false === ctx.respond) return;

    if (!ctx.writable) return;

    const res = ctx.res;
    let body = ctx.body;
    const code = ctx.status;

    // ignore body
    if (statuses.empty[code]) {
        // strip headers
        ctx.body = null;
        return res.end();
    }

    if ('HEAD' == ctx.method) {
        if (!res.headersSent && isJSON(body)) {
        ctx.length = Buffer.byteLength(JSON.stringify(body));
        }
        return res.end();
    }

    // status body
    if (null == body) {
        if (ctx.req.httpVersionMajor >= 2) {
        body = String(code);
        } else {
        body = ctx.message || String(code);
        }
        if (!res.headersSent) {
        ctx.type = 'text';
        ctx.length = Buffer.byteLength(body);
        }
        return res.end(body);
    }

    // responses
    if (Buffer.isBuffer(body)) return res.end(body);
    if ('string' == typeof body) return res.end(body);
    if (body instanceof Stream) return body.pipe(res);

    // body: json
    body = JSON.stringify(body);
    if (!res.headersSent) {
        ctx.length = Buffer.byteLength(body);
    }
    res.end(body);
}

看node文档我们知道http.createServer(this.callback())这行代码,this.callback()每次有请求都会促发。
并且每次请求都会经过所有的中间件,每个中间件函数就可以对ctx为所欲为,而且ctx上还挂载着原生的req和res,这样就更灵活了。

中间件的处理

this.middleware = [];

const fn = compose(this.middleware);
重点看下compose的代码

function compose (middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }

    /**
    * @param {Object} context
    * @return {Promise}
    * @api public
    */

    return function (context, next) {
        // last called middleware #
        let index = -1
        return dispatch(0)
        function dispatch (i) {
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            let fn = middleware[i]
            if (i === middleware.length) fn = next
            if (!fn) return Promise.resolve()
            try {
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

首先middleware是数组,里面放着一个个的函数,调用compose并运行时,实际返回一个promise(所以支持async await),每个中间件的函数中传入了context和下一个运行的函数,在中间件中调用next就可以执行下一个中间件函数。(算是koa的核心代码吧,NBNB)

内部context代理

ctx之所以可以访问 包装对象 response 和 request 的原因

// context.js

delegate(proto, 'response')
    .method('attachment')
    .method('redirect')
    .method('remove')
    .method('vary')
    .method('set')
    .method('append')
    .method('flushHeaders')
    .access('status')
    .access('message')
    .access('body')
    .access('length')
    .access('type')
    .access('lastModified')
    .access('etag')
    .getter('headerSent')
    .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
    .method('acceptsLanguages')
    .method('acceptsEncodings')
    .method('acceptsCharsets')
    .method('accepts')
    .method('get')
    .method('is')
    .access('querystring')
    .access('idempotent')
    .access('socket')
    .access('search')
    .access('method')
    .access('query')
    .access('path')
    .access('url')
    .access('accept')
    .getter('origin')
    .getter('href')
    .getter('subdomains')
    .getter('protocol')
    .getter('host')
    .getter('hostname')
    .getter('URL')
    .getter('header')
    .getter('headers')
    .getter('secure')
    .getter('stale')
    .getter('fresh')
    .getter('ips')
    .getter('ip');

我们看看 delegate 的源码

function Delegator(proto, target) {
    if (!(this instanceof Delegator)) return new Delegator(proto, target);
    this.proto = proto;
    this.target = target;
    this.methods = [];
    this.getters = [];
    this.setters = [];
    this.fluents = [];
}


Delegator.prototype.getter = function(name){
    var proto = this.proto;
    var target = this.target;
    this.getters.push(name);

    proto.__defineGetter__(name, function(){
        return this[target][name];
    });

    return this;
};

Delegator.prototype.setter = function(name){
    var proto = this.proto;
    var target = this.target;
    this.setters.push(name);

    proto.__defineSetter__(name, function(val){
        return this[target][name] = val;
    });

    return this;
};

原理很简单就是 利用 __defineSetter____defineGetter__。嘿嘿,这样你就可以在ctx使用包装的 response 和 request,还有原生node的req、res啦

错误处理

onerror(err) {
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
}

官方文档
默认情况下,将所有错误输出到 stderr,除非 app.silent 为 true。 当 err.status 是 404 或 err.expose 是 true 时默认错误处理程序也不会输出错误。 要执行自定义错误处理逻辑,如集中式日志记录,您可以添加一个 “error” 事件侦听器

app.on('error', err => {
    log.error('server error', err)
});

好了剩下的就是response,request类了,这两个很简单,就是封装了一些原生不好用的方法。

@gamebody gamebody changed the title 《看源码学web》 《看源码学web》【看koa源码学 web 中间件 模型】 Feb 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant