前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >以小白的角度解读Koa源码

以小白的角度解读Koa源码

作者头像
JowayYoung
发布2020-04-01 17:06:17
8180
发布2020-04-01 17:06:17
举报
文章被收录于专栏:JowayYoung谈前端

前言

使用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开发技巧的前端公众号,更多前端小干货等着你喔」

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 IQ前端 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 解读目标
  • 洋葱模型
  • 理解源码
  • 总结
  • 结语
  • 关注IQ前端
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档