前言
之前一直听各位前辈强推手撸源码的好处,但是由于对复杂源码的恐惧和自己的懒惰,一直都没有去分析过源码。恰巧最近看到了koa的源码,然后鼓足勇气去分析了一波。然后把自己能看懂的部分手撸了下来,实现了基本的功能。由于koa的源码太过于复杂,所以我进行了一些简化,如果大家耐心看的话,应该可以很容易就理解了。(在这里强调一下!!!对于前端小白来说,分析源码的好处,真的是多到不能再多了。不仅可以提高自己的代码能力,还可以帮助自己更深入的了解其中的运行机制。大家也要动起手来啊!!!)
主要内容
话不多说,接下来开始介绍我们的主要内容啦!
文件结构
我在这里创建了5个文件,入口文件是application.js。
lib
│── ── application.js
│── ── context.js
│── ── request.js
│── ── response.js
└─ ── compose.js
创好了文件之后,接下来我们先不急着分析源码,我们先来看一下koa有哪些功能,然后依照着这些功能来写我们的源码。
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2)
})
app.use(async (ctx) => {
console.log(3);
ctx.body = 'hello body';
})
app.listen(3000,() => {
console.log('server is running 3000')
})
这是我们引入koa的模块来写的代码,可以清楚的看到koa在里面做了哪些事情。这里需要注意的是app.use注册中间件的操作,首先我们来了解一下中间件是如何执行的。

代码内容
因为application是入口文件同时也是最主要的一个文件,所以我们就先来分析一下application的内容吧。
const Emitter = require('events');
const http = require('http')
const context = require('./context');
const request = require('./request');
const response = require('./response');
const compose = require('./compose')
module.exports = class Application extends Emitter {
constructor() {
super();
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {}
listen(...args) {}
createContext() {}
callback() {}
handleRequest(ctx, fnMiddleware) {}
}
先将Application模块抛出,在这里用到了Emitter是因为需要用到events里面的一些事件,然后创建实例时将context、request、response三个对象初始化好。
1. 从最简单的this.listen监听端口开始
因为listen事件最简单实现,所以我们先从listen入手。
// 原始的方法
const server = http.createServer((req, res) => {
res.end('hello koa');
})
server.listen(3000, () => console.log('服务监听在3000端口'));
这是原生的监听端口的方法,在createServer里面的callback创建完成时,再用listen完成监听。我们需要用koa调用listen方法,所以里面的数据不能写死,需要传参,在原生方法上面稍加修饰,就可以实现this.listen了。
listen(...args) {
// koa里面的方法
const server = http.createServer(this.callback())
server.listen(...args);
}
2. this.callback里面的具体方法
通过原生的监听方法和koa里面的监听方法对比,就可以得知callback函数里面的主要内容是什么。
callback() {
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
}
return handleRequest;
}
因为在koa里面主要用到的是参数是ctx,所以在callback里面,需要将res和req融合在一起赋值给ctx。所以我们在这里使用createContext函数将req和res合并一下。
3. createContext是如何将两个参数合并的呢
再说createContext之前,需要从文件引入开始说起。
const context = require('./context');
const request = require('./request');
const response = require('./response');
在这里引入了三个文件,先看看context文件的内容
const delegates = require('delegates');
// 代理
var proto = module.exports = {}
delegates(proto, 'response')
.access('body');
delegates(proto, 'request')
.access('method')
.access('url');
context文件抛出了一个空对象,将此空对象命名为proto然后在进行一系列的代理操作。这个代理操作很简单,引入delegates之后,在delegates里面传两个参数:第一个参数是对象的名称;第二个参数是该对象下面的需要被代理的key。然后在delegates.access()里面传入key对应的value就可以了。完成这些之后实现的效果就是当你使用proto.response.body时,可以简写为proto.body,因为response被代理了,所以可以省略。现在做的这些操作,都是为了合并req和res做准备,所以需要好好看一下。
接下来在看一下request文件的内容。
module.exports = {
get url() {
return this.req.url;
}
}
也是返回了一个对象,对象里面有一个get url的方法,这个方法的意义在于当给这个对象添加了this.req:{url: value}属性时,可以返回this.req.url,这样就可以直接通过.url获取到res.url里面的value了。
response文件也一样,仅仅抛出了一个空对象。
module.exports = {}
说完了具体引入了哪些文件及其作用,接下来要说constructor里面的内容。
constructor() {
super();
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
通过使用Object.create方法将实例的对象继承引入对象的属性,实质就是将引入对象的属性添加在实例对象的__proto__上面。
前面的铺垫工作做完了,现在到了createContext函数了。
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.req = request.req = req;
context.res = response.res = res;
return context;
}
首先在进行一次继承操作,然后在context对象上面添加了request和response属性, 然后再将传进来的req和res赋值给context里面的req和res属性,同时也赋值给了request.req和response.res。最后形成一个对象如下。
context: {
request: {
req: req,
},
response: {
res: res
},
req: req,
res: res
}
到这里,我们就将req和res赋值给ctx了,就可以实现用ctx操作了。
4. listen基本完成了,现在需要实现.use方法
当需要插入中间件的时候,才会使用到koa.ues(),并且使得中间件按顺序执行,所以可以猜想到是把中间件都存入一个数组里,然后顺序执行。
constructor() {
super();
this.middleware = []; // 新添加
}
先需要创造一个存储中间件的空数组。
use(fn) {
this.middleware.push(fn);
return this;
}
通过代码可以看到use函数就是把每一个传进来的中间件都存入middleware数组。但是传入数组之后怎么执行呢,应该在哪里执行呢?这个时候就想到listen函数了,因为在监听端口前,需要将listen函数里面createContext里的callback函数执行完毕,才可以监听端口,所以我们可以在callback函数里执行中间件。
5. 处理中间件的过程
上面说了,我们需要将中间件的处理过程放在callback函数里。
callback() {
const fn = compose(this.middleware); // 新加的代码,处理中间件函数
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
}
return handleRequest;
}
在这里是使用了compose函数来将中间件数组整合的,那我们就来看一下是怎么样整合的呢。
function compose(middleware) {
return function(context) {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
return Promise.resolve(fn(context, () => {
return dispatch( i + 1 );
}))
}
}
}
module.exports = compose;
在函数里我们创建了一个dispatch函数,给了一个参数i,然后通过参数i来确定应该执行middleware里面的第几个函数。当然我们第一开始执行dispatch的时候要传参数0,因为要从第一个中间件开始执行,然后将中间件套在Promise.resolve()里面,这样每一个中间件都具有promise方法了。koa的中间件需要传入两个参数:第一个是ctx,第二个参数是next(),所以我在这里也需要传入两个参数:第一个是context,第二个是递归dispatch函数,通过 i+1,就可以实现中间件按顺序执行,然后递归到最底层,在一层层的向外执行,就是文章开头说的洋葱模型。
6. 还有一些不知道怎么形容的处理
现在我们拿到了中间件,也拿到了ctx,接下来需要做的是把ctx挂载到中间件上面。
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
this.handleRequest(ctx, fn); // 新加的代码
}
return handleRequest;
}
因为需要拿到ctx,所以只能把函数写在handleRequest里面,这里取名重合了,但是问题不大,然后将ctx和fn传进去就OK了。
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => {
return respond(ctx)
}
return fnMiddleware(ctx).then(handleResponse)
}
在fnMiddleware中间件的执行和handleResponse函数的执行会造成异步,所以需要用到Promise,因为我在上面处理中间件的时候,已经为中间件添加了Promise,所以这里可以直接.then()就好了。那么handleResponse函数的意义是什么呢?是为了处理ctx。
app.use(async (ctx) => {
ctx.body = 'hello body';
})
当在大家写中间件的时候,会在ctx上面挂载一些东西,所以在handleResponse函数里就是为了处理ctx的。
function respond(ctx) {
const res = ctx.res;
const body = ctx.body;
res.end(body);
}
通过ctx.res获取到一开始传进去res属性,然后在通过ctx.body获取大家在写中间件时对ctx添加的body属性。最后通过res的原生方法res.end(body)就可以实现往DOM结构上面挂载body了。
到这里,我的源码分析就结束啦。为了方便大家理解,我从网上找了一张koa源码的脑图,看完源码在配合脑图理解,应该很容易就能掌握。

总结
我仅仅只是把koa一些主要的方法源码实现了一下,一些细节的操作我就不在一一实现了。大家如果看懂了的话,最好私下自己实现一遍,真的可以帮助自己提升很多。好啦,我就不废话啦,大家如果喜欢这篇文章的话,那就点个赞吧!
