Koa的源码学习

144 阅读5分钟

Koa 是我在Node学习中接触的第一个Web开发框架,刚开始使用时,只知道跟着这么用,却不明白为什么这样用,又是怎么实现的。最近跟着大佬学习了 Koa 的源码,写篇笔记记录下学习过程。

HTTP 服务器

Koa 是基于 Node 提供的 HTTP 模块的一个Web框架,将一个或多个Koa应用程序安装在一起以形成具有单个 HTTP 服务器的更大应用程序。

const http = require("http");
http.createServer((req, res) => {
    // 处理请求
    res.status = 200;
    res.setHeader("Content-Type", "text/html;charset=utf-8");
    // 结束请求并响应
    res.end("你好,世界!");
}).listen(3000, () => {
    console.log("服务器启动成功!");
});

运行以上代码,一个简单的 HTTP 服务器就搭建成功了,当有请求访问3000端口时,就会触发http.createServer内的请求处理函数,我们可以在该函数内针对不同请求,返回不同的响应结果。

了解 HTTP 服务器的初步搭建后,我们来开始阅读 Koa 的源码吧~

Koa

可以看到,Koa 的核心文件只有4个--application.js、context.js、request.js、response.js,其中application.js是 Koa 的主文件,其他几个js都集成在application.js,进行相应处理后,将 Koa 实例导出。

在正式学习 Koa 前,先确定一下学习的流程。就像要开发页面前得先给设计稿一样,得先知道想要实现的效果再去进行开发,我们开始了解 Koa 源码前,先设想我们想要的效果,再去思考源码实现。

完整的 Koa 应用示例:

const Koa = require("koa");
const app = new Koa();
app.use(async (ctx, next) => {
    ctx.status = 200;
    ctx.body = "你好,世界!";
});
app.listen(3000, () => {
    console.log("服务启动成功");
});

以上代码是 Koa 的简单应用,可以看到,此处没有引用 HTTP 模块,说明 HTTP 模块是在 Koa 内部引用、创建的,并且将处理方法进行了封装,只暴露了一个use方法挂载在 Koa 实例上;提供的中间件函数是一个异步方法,提供的参数ctx/next也不是createServer方法提供的req/res,而是Koa重新定义的。

那我们将这几个不同分步来进行学习:

  • HTTP 模块、use方法的封装
  • ctx对象的封装
  • next方法的封装

HTTP 模块、use方法的封装

demo1.js

const Koa = require("koa");
const app = new Koa();
app.use((req, res) => {
    res.status = 200;
    res.end("你好,世界!");
});
app.listen(3000, () => {
    console.log("服务启动成功");
});

application.js

const http = require("http");
module.exports = class Application {
    constructor() {
        this.callbackFunc;
    }
    listen(...args) {
        // 将server封装在listen方法中,
        const server = http.createServer(this.callback());
        server.listen(...args);
    }
    callback() {
        return (req, res) => {
            // 实际的请求处理函数
            this.callbackFunc(req, res);
        }
    }
    use(fn) {
        // 将传入use方法的中间件函数先存储起来
        this.callbackFunc = fn;
    }
}
node demo1.js

ctx对象的封装

demo2.js

...
app.use((ctx) => {
    ctx.status = 200;
    ctx.res.setHeader("Content-Type", "text/html;charset=utf-8");
    ctx.body = ("你好,世界!");
});
...

我们先来稍微分析一下ctx对象,它是一个上下文对象,这里的上下文是 Koa,那么这个对象指向的this就是当前使用的 Koa 实例。通过该对象可以访问req/res,那么这两个参数也都挂载到了ctx上,并且还能将其他方法、属性挂载上去。在中间件函数中,不会有res.end()方法,说明在 Koa 里面封装了请求结束的方法。

context.js

阅读context.js,其中有一段代码是关键:

// 设置代理库 委托代理
const delegate = require('delegates');
const proto = module.exports = {
    ...
}
/**
 * Response delegation.
 */
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .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');

其中proto对象就是ctx对象,里面使用了delegate方法将其他对象的一些属性挂载到ctx对象上,从而ctx也能访问到这些属性。这里的delegate是一个代理库,如果不依赖这个库,写一段简单的代码实现对象间的属性复制:

let proto = {};
function delegateSet(property, name) {
    proto.__defineSetter__(name, function (val) {
        this[property][name] = val;
    });
}
function delegateGet(property, name) {
    proto.__defineGetter__(name, function () {
        return this[property][name];
    })
}

request.js

request.js主要是定义一些Request对象的属性和方法,以供使用:

const url = require('url');
module.exports = {
    get query() {
        return url.parse(this.req.url).query;
    }
};

response.js

response.js主要是定义一些Response对象的属性和方法,以供使用:

module.exports = {
    get body() {
        return this._body;
    },

    set body(data) {
        this._body = data;
    },

    get status() {
        return this.res.statusCode;
    },

    set status(statusCode) {
        if (typeof statusCode !== 'number'){
            throw new Error('Something is wrong!');
        } 

        this.res.statusCode = statusCode;
    }
};

context.js

let proto = {};
// __defineSetter__/__defineGetter__ 是新出的方法,代替了Object.defineProperty方法
function delegateSet(property, name) {
    proto.__defineSetter__(name, function (val) {
        this[property][name] = val;
    });
}
function delegateGet(property, name) {
    proto.__defineGetter__(name, function () {
        return this[property][name];
    })
}

let requestSet = [];
let requestGet = ['query'];

let responseSet = ['body', 'status'];
let responseGet = responseSet;

requestGet.forEach(ele => delegateGet('request', ele));
requestSet.forEach(ele => delegateSet('request', ele));
responseSet.forEach(ele => delegateSet('response', ele));
responseGet.forEach(ele => delegateGet('response', ele));
module.exports = proto;

application.js

const http = require("http");
const response = require("./response");
const request = require("./request");
const context = require("./context");

class Application {
    constructor() {
        this.callbackFunc;
        // Object.create 在现有对象的__proto__基础上创建新对象
        this.context = Object.create(context);
        this.request = Object.create(request);
        this.response = Object.create(response);
    }
    listen(...args) {
        // 将server封装在listen方法中,
        const server = http.createServer(this.callback());
        server.listen(...args);
    }
    callback() {
        let fn = this.callbackFunc;
        return (req, res) => {
            // 实际的请求处理函数
            let ctx = this.createContext(req, res);
            this.callbackFunc(ctx);
            this.responseBody(ctx);
        }
    }
    use(fn) {
        // 将传入use方法的中间件函数先存储起来
        this.callbackFunc = fn;
    }
    // 创建ctx上下文对象
    createContext(req, res) {
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response);

        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        return ctx;
    }
    // 处理请求结果
    responseBody(ctx) {
        let content = ctx.body;
        if (typeof content === 'string') {
            ctx.res.end(content);
        } else if (typeof content === 'object') {
            ctx.res.end(JSON.stringify(content));
        }
    }
}
module.exports = Application;
node demo2.js

result

next方法的封装

emm...还没写完,写完即刻更~~~

本文旨在记录和分享学习心得,如若有更好的见解,欢迎留言讨论,谢谢~~~