专栏首页JowayYoung谈前端以小白的角度解读Koa源码

以小白的角度解读Koa源码

前言

使用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就胜在此处。

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

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部分源码:

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

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()
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函数作为创建服务器之后接受的处理函数
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)
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函数其实就是所有中间件执行成功后的响应函数,这里对不同的响应主体进行了响应的处理。为了更好的理解,每一行代码相应的理解注释在代码下面。
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开发技巧的前端公众号,更多前端小干货等着你喔」

本文分享自微信公众号 - IQ前端(gh_4593b39979fb),作者:LazyCurry

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 可能是最全最易记的CSS选择器分类大法

    最近查看了几位同事的代码,发现很多CSS书写习惯都是清一色的类名而没有相应的选择器,层层嵌套的标签都包含至少一个类名。有些同学会问,很多文章都说「选择器」有性能...

    JowayYoung
  • JavaScript详细判断浏览器运行环境

    看到标题,大家就能想起这个需求在很多项目上都能用到。我们部署在Web服务器上的前端应用,既可以用PC浏览器访问,也可以用手机浏览器访问,再加上现在智能设备的推广...

    JowayYoung
  • 纯CSS实现密室逃脱游戏​

    大家好,以下是前端大佬「alphardex」授权笔者转发的内容。本文以「密室逃脱」的游戏场景为学习背景,着重讲解<input>的:checked与<label>...

    JowayYoung
  • 一杯茶的时间,上手 Koa2 + MySQL 开发

    凭借精巧的“洋葱模型”和对 Promise 以及 async/await 异步编程的完全支持,Koa 框架自从诞生以来就吸引了无数 Node 爱好者。然而 Ko...

    一只图雀
  • pktball游戏解析

    之前的『好玩的小游戏推荐』,只是罗列了一下图,感觉没啥意思,所以改成简单的游戏解析了。 首先有个观点要了解一下: 有部分非程序员的同学认为,在程序员眼里,大部分...

    沙因Sign
  • 微信网页开发

    套用《围城》里老学究的的一句开场白:"兄弟我刚入行的时候…“兄弟我是很不喜欢微信这样一款应用的——尽管我在2011年就已经是微信的注册用户。在我看来,第一个,能...

    一粒小麦
  • Angular2 返回时组件生命周期函数不被调用的解决方法

    这两天使用 Angular2 遇到的一个 @angular/router 的 bug:

    Alan Zhang
  • UML类图简单介绍

    企鹅需要‘知道’气候的变化,需要‘了解’气候规律。当一个类‘知道’另一个类时,可以用关联(association)。关联关系用实线箭头来表示 代码表示如下:

    用户3148308
  • Linux文件归档之tar

    tar相信大家也比较熟悉了,它是一个常见的压缩文档格式,在linux中它是用来压缩文件的一个命令。在操作之前先来张各个选项的详细解释图片

    雨落凋殇
  • 如何挖掘长尾关键

    在SEO管理工作程序中,挖词是极为重要的一个节目。一般SEO都把主要心力放在了主关键词、架构关键词下面,但是网站大部份的搜索流量来自于单个搜索数目非常多的关键词...

    申霖

扫码关注云+社区

领取腾讯云代金券