以小白的角度解读Koa源码

2020-04-01 17:06:17 浏览数 (1)

前言

使用Koa已有一段时间,为什么会从Express转向Koa呢,那还是得从Express上说起。对于服务端的Web框架来说,Express更为贴近「Web Framework」这一概念,比如自带的路由,经过多年的运行,也使其生态丰富稳定。

但是说到Express的坏处,大家可能都会想起它的callback,使用不当必然会引起回调地狱。而此时此刻的Koa,正是解决了这个问题,不仅如此,Koa是基于Node的下一代Web框架,由Express团队打造,特点是「优雅、简洁、灵活、体积小」,几乎所有功能都需要通过中间件实现。

「Promise」「Async/Await」是未来主流的异步编程方式,Node应用中需要优雅的异步处理方式,而Koa恰好来得很是时候。下面以小白的角度对Koa源码进行一次解读。

解读目标

Koa的源码非常精简,代码量非常少。正所谓「短小精悍」,在Koa上体现得淋漓尽致。读完源码后发现Koa还有很多插件的源码也值得一读,这篇文章先从基础解读开始,理解Koa最核心的东西。

  • 中间件调用顺序:「洋葱模型」
  • 理解Koa源码

洋葱模型

在了解洋葱模型之前,我们需要知道每一个中间件的周期:

  • 前期处理
  • 交给并等待下一个中间件处理
  • 后期处理

多个中间件处理的过程,就形成了洋葱模型。这样实现的好处在于可非常方便的实现后续处理逻辑,而第一个中间件也能得到最后一个中间件的处理结果。

Koa使用app.use()方法来加载中间件,功能基本都由中间件实现。加载完多个中间件后,跟栈的执行顺序一样,以「先进后出」的顺序执行。中间件带有2个参数:ctx对象next函数

Koa将requestresponse封装进ctx,当调用了next()就会执行下一个中间件。当执行完最后一个中间件,就会执行上一级调用的中间件。整个过程可理解成一刀切洋葱,切的顺序就是中间件的调用顺序,非常有趣。

洋葱模型的具体实现原理可通过插件「Koa-Compose」的源码理解,这里只做一下简单的介绍。app.use()的作用是将中间件添加到中间件数组middleware,将中间件数组middleware传入Compose()函数,Compose()函数返回一个匿名函数,匿名函数返回Promise对象,第一个参数是context,第二个参数是next(),在有下一个中间件需要执行的情况下,next()其实就是下一个要运行的中间件函数。返回Promise,是为了方便await调用。

说到context,可与「Express」做一下小比较。对Express来说,并没有提供上下流信息,需要手动处理。Express不支持洋葱模型那样的数据流入流出处理能力,需要引入插件。因此,Koa就胜在此处。

下面用一个简单的例子来理解洋葱模型:

代码语言:javascript复制
const Koa = require("koa");
const app = new Koa();

app.use((ctx, next) => {
    console.log("第一个中间件");
    next();
    console.log("第一个中间件执行完毕");
});

app.use((ctx, next) => {
    console.log("第二个中间件");
    next();
    console.log("第二个中间件执行完毕");
});

app.use((ctx, next) => {
    console.log("第三个中间件");
});

app.listen(8090);

// 输出结果:
// 第一个中间件
// 第二个中间件
// 第三个中间件
// 第二个中间件执行完毕
// 第一个中间件执行完毕

理解源码

下载Koa的源码,主要代码都在lib文件下,仅有4个文件,分别是:request.jsresopnse.jscontext.jsapplication.js。对于这4个文件,可大致分成3类:req/res(请求与响应)、context(上下文)、application

❝req和res ❞

对应的是request.jsresponse.js,分别代表着请求信息和返回信息。2个文件都是对外暴露一个对象,使用gettersetter来读写对象的属性。

request.js部分源码:

代码语言:javascript复制
module.exports = {
    get header() {
        return this.req.headers;
    },
    set header(val) {
        this.req.headers = val;
    }
};

❝context ❞

运行环境的上下文信息存在context.js。上下文包括了requestresponse,在context.js里引用了delegate.js库来对request和response的代理。上面说到洋葱模型时,中间件的第一个参数ctx,其实就是context的缩写。因此有ctx.req=ctx.request=context.requestctx.res=ctx.response=context.response

代码语言:javascript复制
const delegate = require("delegates");

delegate(proto, "response")
    .method("attachment")
    .method("redirect");

delegate(proto, "request")
    .method("acceptsLanguages")
    .method("acceptsEncodings");

❝application ❞

对应的是application.js文件,是最重要的一个文件。在这里将各个函数拆分,分析,理解。

  • 「listen()」:Koa通过app.listen(8090)来启动端口,可看到listen函数http.createServer()用于创建一个服务器,接受一个请求监听函数this.callback()
代码语言:javascript复制
listen(...args) {
    debug("listen");
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
  • 「callback()」callback负责合并中间件,通过compose()合并存在this.middleware里的所有中间件。compose函数由插件koa-compose引入。callback函数返回handleRequest()处理函数,handleRequest函数作为创建服务器之后接受的处理函数
代码语言:javascript复制
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;
}
  • 「handleRequest()」handleRequest函数通过createContext()创建请求的上下文,将状态设为404。onFinished(res, onerror)通过引入第三方库on-finished来监听服务器的失败响应,传入的onerror就是ctx.onerror(err)。最后返回fnMiddleware(ctx).then(handleResponse).catch(onerror),就是将所有合并起来的中间件成功执行完后就执行handleResponse响应函数,异常则执行onerror,就是ctx.onerror(err)
代码语言:javascript复制
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);
}
  • 「respond()」respond函数其实就是所有中间件执行成功后的响应函数,这里对不同的响应主体进行了响应的处理。为了更好的理解,每一行代码相应的理解注释在代码下面。
代码语言:javascript复制
function respond(ctx) {
    // allow bypassing koa
    // 这里说明可通过设置上下文的respond为false,则会跳过respond处理
    if (ctx.respond === false) return;
    if (!ctx.writable) return;
    const res = ctx.res;
    let body = ctx.body;
    const code = ctx.status;
    // ignore body
    // 如果状态码表示没有响应主体时,则设置响应主体为null
    if (statuses.empty[code]) {
        // strip headers
        ctx.body = null;
        return res.end();
    }
    if (ctx.method == "HEAD") {
        if (!res.headersSent && isJSON(body)) {
            ctx.length = Buffer.byteLength(JSON.stringify(body));
        }
        return res.end();
    }
    // status body
    if (body == null) {
        // 如果响应主体为空,这里将body设置成状态码,或者设置成message
        if (ctx.req.httpVersionMajor >= 2) {
            body = String(code);
        } else {
            body = ctx.message || String(code);
        }
        // 如果响应头为发送时需要设置Content-Type与Content-Length
        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 (typeof body === "string") 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);
}

总结

对Koa源码的简单解析就写到这啦。读完源码之后发现,不能只停留在使用上面,更应该花点时间来理解背后的源码,在解读源码的时候,也许会让自己有意外的收获哦。Koa还有很多插件的源码值得去探究,比如koa-composekoa-router这些插件的源码都值得一读。不断学习进步是消除焦虑的唯一方法,继续努力呀~

结语

欢迎在下方进行评论,喜欢本文的「点个赞」「收个藏」,同时也希望各位朋友对文章里的要点进行补充或提出自己的见解。

关注IQ前端

「关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔」

0 人点赞