2
本文基于koa 3.0.0-alpha.1版本源码进行分析

由于koa的源码量非常少,但是体现的思想非常经典和难以记忆,如果突然要手写koa代码,可能还不一定能很快写出来,因此本文将集中于如何理解以及记忆koa的代码

本文一些代码块为了演示方便,可能有一些语法排列错误,因此本文所有代码均可以视为伪代码

1. 文章内容

  1. 0到1推导koa 3.0.0-alpha.1版本源码的实现,一步一步完善简化版koa的手写逻辑
  2. 分析常用中间件koa-router的源码以及进行对应的手写
  3. 分析常用中间件koa-bodyparser的源码以及进行对应的手写

2. 核心代码分析&手写

2.1 koa-compose

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
    console.log("中间件1 start");
    await next();
    console.log("中间件1 end");
});
app.use(async (ctx, next) => {
    console.log("中间件2 start");
    await next();
    console.log("中间件2 end");
});
app.use(async (ctx, next) => {
    console.log("中间件3 start");
    await next();
    console.log("中间件3 end");
});
app.listen(3000);

上面代码块中间件运行流程如下所示
koa中间件.svg
上面的运行流程看起来就跟我们平时开发不太一样,我们可以看一个相似的场景,比如下面

  • 我们在fn1()中执行一系列的业务逻辑
  • 但是我们在fn1()遇到了await fn2(),因此我们得等待fn2()执行完毕后才能继续后面的业务逻辑
function fn1() {
    console.log("fn1执行业务逻辑1");
    await fn2();
    console.log("fn1执行业务逻辑2")
}
async function fn2() {
    console.log("fn2执行业务逻辑1");
}

我们将fn2作为参数传入

async function fn2() {
    console.log("fn2执行业务逻辑1");
}
function fn1(fn2) {
    console.log("fn1执行业务逻辑1");
    await fn2();
    console.log("fn1执行业务逻辑2")
}

如果我们有fn3fn4呢?

async function fn1(fn2) {
    console.log("fn1执行业务逻辑1");
    await fn2();
    console.log("fn1执行业务逻辑2")
}
async function fn2(fn3) {
    console.log("fn2执行业务逻辑1");
    await fn3();
    console.log("fn2执行业务逻辑2")
}
async function fn3(fn4) {
    console.log("fn3执行业务逻辑1");
    await fn4();
    console.log("fn3执行业务逻辑2")
}
async function fn4() {
    console.log("fn4执行业务逻辑1");
    console.log("fn4执行业务逻辑2")
}

那如果我们还有fn5fn6....呢?

我们使用怎样的逻辑进行这种function的嵌套?

我们可以从上面代码发现,每一个fnX()传递的都是上一个fn(X+1)()

2.1.1 使用middleware遍历所有fn

我们可以先使用一个数组进行fn的添加

middleware.push(fn);

当我们取出一个fn时,我们应该传入下一个fn,即

let fn = middleware[i];
fn(middleware[i+1]);

如果我们想要顺序传入context

let fn = middleware[i];
fn(context, middleware[i+1]);

使用middleware整合上面的逻辑,如下面所示

  • 我们使用app.use((ctx, next))传入的next()需要强制返回一个Promise,因为它可以使用await,因此我们使用Promise.resolve()包裹fn()返回的值,防止返回的不是Promise
  • 在调用fn()的时候,会传入下一个中间件作为第二个参数:middleware[i + 1]
let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        function dispatch(i) {
            let fn = middleware[i];
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, middleware[i + 1]));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    console.log("fn2执行业务逻辑2")
});
app.listen(200);

2.1.2 链式调用

async function fn1(fn2) {
    console.log("fn1执行业务逻辑1");
    await fn2();
    console.log("fn1执行业务逻辑2")
}
async function fn2(fn3) {
    console.log("fn2执行业务逻辑1");
    await fn3();
    console.log("fn2执行业务逻辑2")
}
async function fn3() {
    console.log("fn3执行业务逻辑1");
    console.log("fn3执行业务逻辑2")
}
我们如何实现fn1->fn2->fn3的链式调用呢?
fn1(fn2(fn3))

回到我们上面实现的koa源码

let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        function dispatch(i) {
            let fn = middleware[i];
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, middleware[i + 1]));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    console.log("fn2执行业务逻辑2")
});
app.listen(200);

如上面所示,我们执行了fn1(context, fn2),但是我们fn2()并没有传入fn3,这导致了链式调用被中断了,而且fn2()也不一定会返回Promise,因此我们需要对下面代码进行调整

let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        function dispatch(i) {
            let fn = middleware[i];
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, dispatch(i + 1)));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
      await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);

fn(context, middleware[i + 1])调整为fn(context, dispatch(i + 1))
这样我们就可以实现

  • fn2()返回的是Promise.resolve(),无论fn2()返回什么,都是一个Promise
  • fn2(context, dispatch(i + 1))的第二个参数传入了fn3,并且fn3是一个Promise

2.1.3 细节优化

2.1.3.1 app.use返回this

app.use()返回自己本身,可以使用链式调用

let app = {
    use(fn) {
        middleware.push(fn);
        return this;
    }
}
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
}).use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    console.log("fn2执行业务逻辑2")
});
2.1.3.2 dispatch()返回方法

dispatch(i + 1)返回的是一个执行完毕的Promise状态,不是一个方法,需要改成bind

let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
      return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        function dispatch(i) {
            let fn = middleware[i];
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
      await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);
2.1.3.3 最后一个中间件返回空的Promise.resolve

最后一个中间件调用next()时没有执行的方法,应该直接返回一个空的方法,比如上面代码中

  • console.log("fn2执行业务逻辑1")
  • await next(): 此时的next()应该是一个空的Promise方法
  • console.log("fn2执行业务逻辑2")
let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
        return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        function dispatch(i) {
            let fn = middleware[i];
            if (i === middleware.length) {
                return Promise.resolve();
            }
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);
2.1.3.4 阻止中间件中重复调用next()

阻止一个中间件重复调用next()方法,使用index记录当前的i,如果发现i<=index,说明重复调用了某一个中间件的next()方法

let middleware = [];
let context = {};

let app = {
    use(fn) {
        middleware.push(fn);
        return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        // 要求每一个fn返回都是一个Promise
        let index = -1;
        function dispatch(i) {
            if (i <= index) {
                return new Promise.reject(new Error("next()重复调用多次"));
            }
            index = i;
            let fn = middleware[i];
            if (i === middleware.length) {
                return Promise.resolve();
            }
            // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
        }
        return dispatch(0);
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
   await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);
2.1.3.5 完善错误处理逻辑
  • 重复调用next()抛出错误
  • 执行fn()过程中出错

dispatch()的外层再包裹一个新的function(),然后我们就可以使用这个function()进行统一的then()catch()处理,即下面代码中的

  • let fn = compose(this.middleware)
  • fn().then(() => {}).catch(err => {})
function compose(middleware) {
    // 返回也是一个Promise,可能是Promise.resolve(),也有可能是Promise.reject()
    return function (context) {
        // 要求每一个fn返回都是一个Promise
        let index = -1;
        function dispatch(i) {
            if (i <= index) {
                return Promise.reject(new Error("next()重复调用多次"));
            }
            index = i;
            let fn = middleware[i];
            if (i === middleware.length) {
                return Promise.resolve();
            }
            try {
                // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }

        }
        return dispatch(0);
    }

}

let app = {
    middleware: [],
    use(fn) {
        this.middleware.push(fn);
        return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        let fn = compose(this.middleware);
        let context = {};
        fn(context).then(() => {
            // 正常执行最终触发
            console.log("fn执行完毕!");
        }).catch(error => {
            console.error("fn执行错误", error);
        });
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);

运行上面代码,得到的结果为:
截屏2023-07-02 14.39.13.png

2.1.3.6 兼容compose传入next()方法

compose()返回的function(context)增加传入参数next,可以在外部进行定义传入,然后判断

  • i等于middleware.length时,middleware[i]肯定为空,判断最后一个next()是否为空
  • 如果最后一个next()不为空,则继续执行最后一次next()
  • 如果最后一个next()为空,则直接返回空的Promise.resolve,跟上面我们处理i等于middleware.length时的逻辑一样
function compose(middleware) {
    // 返回也是一个Promise,可能是Promise.resolve(),也有可能是Promise.reject()
    return function (context, next) {
        // 要求每一个fn返回都是一个Promise
        let index = -1;
        function dispatch(i) {
            if (i <= index) {
                return Promise.reject(new Error("next()重复调用多次"));
            }
            index = i;
            let fn = middleware[i];
            if (i === middleware.length) {
                // middleware[i]肯定为空,判断最后一个next()是否为空
                // 如果不为空,则继续执行最后一次
                // 如果为空,则返回Promise.resolve()
                fn = next;
            }
            if (!fn) {
                return Promise.resolve();
            }
            try {
                // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }

        }
        return dispatch(0);
    }

}

let app = {
    middleware: [],
    use(fn) {
        this.middleware.push(fn);
        return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        let fn = compose(this.middleware);
        let context = {};
        const next = function () {
            console.log("最后一个next()!");
        }
        fn(context, next).then(() => {
            // 正常执行最终触发
            console.log("fn执行完毕!");
        }).catch(error => {
            console.error("fn执行错误", error);
        });
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);
2.1.3.7 处理middleware不为数组时错误的抛出
function compose(middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
    // 返回也是一个Promise,可能是Promise.resolve(),也有可能是Promise.reject()
    return function (context, next) {
        // 要求每一个fn返回都是一个Promise
        let index = -1;
        function dispatch(i) {
            if (i <= index) {
                return Promise.reject(new Error("next()重复调用多次"));
            }
            index = i;
            let fn = middleware[i];
            if (i === middleware.length) {
                // middleware[i]肯定为空,判断最后一个next()是否为空
                // 如果不为空,则继续执行最后一次
                // 如果为空,则返回Promise.resolve()
                fn = next;
            }
            if (!next) {
                return Promise.resolve();
            }
            try {
                // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }

        }
        return dispatch(0);
    }

}

let app = {
    middleware: [],
    use(fn) {
        this.middleware.push(fn);
        return this;
    },
    listen(...args) {
        this.callback();
    },
    callback() {
        let fn = compose(this.middleware);
        let context = {};
        const next = function () {
            console.log("最后一个next()!");
        }
        fn(context, next).then(() => {
            // 正常执行最终触发
            console.log("fn执行完毕!");
        }).catch(error => {
            console.error("fn执行错误", error);
        });
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);
至此,我们已经完全实现了官方koa-compose的完整代码!

2.2 Node.js原生http模块

Koa是基于中间件模式的HTTP服务框架,底层原理就是封装了Node.js的http原生模块

在上面实现koa-compose中间件的基础上,我们增加Node.js的http原生模块,基本就是Koa的核心代码的实现

2.2.1 原生代码示例

const http = require('http');
const server = http.createServer((req, res)=> {
  res.end(`this page url = ${req.url}`);
});
server.listen(3001, function() {
  console.log("the server is started at port 3001")
})

2.2.2 增加原生http模块的相关代码

完善listen()callback()的相关方法,增加原生http模块的相关代码

function compose(middleware) {
    if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
    for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
    }
    // 返回也是一个Promise,可能是Promise.resolve(),也有可能是Promise.reject()
    return function (context, next) {
        // 要求每一个fn返回都是一个Promise
        let index = -1;
        function dispatch(i) {
            if (i <= index) {
                return Promise.reject(new Error("next()重复调用多次"));
            }
            index = i;
            let fn = middleware[i];
            if (i === middleware.length) {
                // middleware[i]肯定为空,判断最后一个next()是否为空
                // 如果不为空,则继续执行最后一次
                // 如果为空,则返回Promise.resolve()
                fn = next;
            }
            if (!next) {
                return Promise.resolve();
            }
            try {
                // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }

        }
        return dispatch(0);
    }

}

let app = {
    middleware: [],
    use(fn) {
        this.middleware.push(fn);
        return this;
    },
    listen(...args) {
        const server = http.createServer(this.callback());
        return server.listen(...args);
    },
    callback() {
        let fn = compose(this.middleware);
        return (req, res) => {
            let context = {};
            this.handleRequest(context, fn);
        }
    },
    handleRequest(context, fn) {
        const next = function () {
            console.log("最后一个next()!");
        }
        fn(context, next).then(() => {
            // 正常执行最终触发
            console.log("fn执行完毕!");
        }).catch(error => {
            console.error("fn执行错误", error);
        });
    }
};
app.use(async (ctx, next) => {
    console.log("fn1执行业务逻辑1");
    await next();
    await next();
    console.log("fn1执行业务逻辑2")
});
app.use(async (ctx, next) => {
    console.log("fn2执行业务逻辑1");
    await next();
    console.log("fn2执行业务逻辑2")
});
app.listen(200);

2.3 初始化context

app={}的形式完善为class Koa的形式,然后在构造函数中初始化contextrequestresponse的初始化,在callback()进行http.createServer()回调函数reqres的赋值

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.app = request.app = response.app = this;
  context.req = request.req = response.req = req;
  context.res = request.res = response.res = res;
  request.ctx = response.ctx = context;
  request.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  context.state = {};
  return context;
}

完整代码如下所示

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

function compose(middleware) {
  if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!");
  for (const fn of middleware) {
    if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!");
  }
  // 返回也是一个Promise,可能是Promise.resolve(),也有可能是Promise.reject()
  return function (context, next) {
    // 要求每一个fn返回都是一个Promise
    let index = -1;
    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error("next()重复调用多次"));
      }
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) {
        // middleware[i]肯定为空,判断最后一个next()是否为空
        // 如果不为空,则继续执行最后一次
        // 如果为空,则返回Promise.resolve()
        fn = next;
      }
      if (!next) {
        return Promise.resolve();
      }
      try {
        // 可能返回只是一个普通的数据,因此需要使用Promise.resolve()进行包裹返回一个Promise数据
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
    return dispatch(0);
  };
}

class Koa {
  constructor() {
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  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.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

  use(fn) {
    this.middleware.push(fn);
    return this;
  }

  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  callback() {
    let fn = compose(this.middleware);
    return (req, res) => {
      let context = this.createContext(req, res);
      this.handleRequest(context, fn);
    };
  }

  handleRequest(context, fn) {
    const next = function () {
      console.log("最后一个next()!");
    };
    fn(context, next)
      .then(() => {
        // 正常执行最终触发
        console.log("fn执行完毕!");
      })
      .catch((error) => {
        console.error("fn执行错误", error);
      });
  }
}

const app = new Koa();
app.use(async (ctx, next) => {
  console.log("fn1执行业务逻辑1");
  await next();
  await next();
  console.log("fn1执行业务逻辑2");
});
app.use(async (ctx, next) => {
  console.log("fn2执行业务逻辑1");
  await next();
  console.log("fn2执行业务逻辑2");
});
app.listen(200);

2.4 完善响应数据的逻辑

由上面初始化context的代码可以知道,我们已经将http原生模块的reqres都放入到context中,因此我们在执行完毕中间件后,我们应该对context.res进行处理,返回对应的值

handleRequest(context, fn) {
    const next = function () {
        console.log("最后一个next()!");
    };
    const handleResponse = () => {
        return this.handleResponse(context);
    };
    fn(context, next)
        .then(handleResponse)
        .catch((error) => {
            console.error("fn执行错误", error);
        });
}
handleResponse(ctx) {
    const res = ctx.res;
    let body = ctx.body;
    if (!body) {
        return res.end();
    }

    if (typeof body !== "string") {
        body = JSON.stringify(body);
    }

    res.end(body);
}
至此,我们完成了一个简化版本的Koa,完整代码放在github mini-koa

3. 常见中间件分析&手写

3.1 koa-router

3.1.1 不使用koa-router

在不使用koa-router中间件时,我们需要手动根据ctx.request.url去判断路由,如下面代码所示

const Koa = require("koa");
const fs = require("fs");
const app = new Koa();

function readFile(path) {
    return new Promise((resolve, reject) => {
        let htmlUrl = `../front/${path}`;
        fs.readFile(htmlUrl, "utf-8", (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

async function parseUrl(url) {
    let base = "404.html";
    switch (url) {
        case "/":
            base = "index.html";
            break;
        case "/login.html":
            base = "login.html";
            break;
        case "/home.html":
            base = "home.html";
            break;
    }
    // 从本地读取出该路径下html文件的内容,然后返回给客户端
    const data = await readFile(base);
    return data;
}

app.use(async (ctx) => {
    let url = ctx.request.url;
    // 判断这个url是哪一个请求
    const htmlContent = await parseUrl(url);
    ctx.status = 200;
    ctx.body = htmlContent;
});
app.listen(3000);
console.log("[demo] route is starting at port 3000");

因此我们手写koa-router时,我们需要关注几个问题:

  • 根据ctx.path判断是否符合注册的路由,如果符合,则触发注册的方法
  • 我们需要根据pathmethods进行对应数据结构的构建

3.1.2 使用koa-router的具体示例

const app = new Koa();
const router = new Router();
router.get("/", (ctx, next) => {
  // ctx.router available
});
router.get("/home", (ctx, next) => {
    // ctx.router available
});
app.use(router.routes());

3.1.3 根据示例实现koa-router

根据methods初始化所有方法,形成this["get"]this["put"]的数据结构,提供给外部调用注册路由

当有新的请求发生时,会触发中间件的逻辑执行,会根据目前ctx.pathctx.method去寻找之前是否有注册过的路径,如果有则触发注册路径的callback进行逻辑的执行

function Router(opts) {
  this.register = function (path, methods, callback, opts) {
    this.stack.push({
      path,
      methods,
      middleware: callback,
      opts,
    });
    return this;
  };

  this.routes = function () {
    // 返回所有注册的路由

    return async (ctx, next) => {
      // 每次执行中间件时,判断是否有符合register()的路由

      const path = ctx.path;
      const method = ctx.method.toUpperCase();

      let callback;

      for (const item of this.stack) {
        if (path === item.path && item.methods.indexOf(method) >= 0) {
          // 找到对应的路由
          callback = item.middleware;
          break;
        }
      }

      if (callback) {
        callback(ctx, next);
        return;
      }
      await next();
    };
  };
  
  this.opts = opts || {};
  this.methods = this.opts.methods || ["HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE"];
  this.stack = [];

  // 根据methods初始化所有方法,形成this["get"]、this["put"]的数据结构

  for (const _method of this.methods) {
    this[_method.toLowerCase()] = this[_method] = function (path, callback) {
      this.register(path, [_method], callback);
    };
  }
}

3.2 koa-bodyparser

该中间件可以简化请求体的解析流程

当我们不使用koa-bodyparser时,如下面所示

3.2.1 不使用koa-bodyparser

GET请求

  1. query是格式化好的参数对象,比如query={a:1, b:2}
  2. querystring是请求字符串,比如querystring="a=1&b=2"
let request = ctx.request;
let query = request.query;
let queryString = request.querystring;
// 也可以直接省略request,const {query, querystring} = request

POST请求
没有封装具体的方法,需要手动解析ctx.req这个原生的node.js对象
如下面例子所示,ctx.req获取到formData"userName=22&nickName=22323&email=32323"
我们需要将formData解析为{userName: 22, nickName: 22323, email: 32323}

home.post("b", async (ctx) => {
  const body = await parseRequestPostData(ctx);
  ctx.body = body;
});
async function parseRequestPostData(ctx) {
  return new Promise((resolve, reject) => {
    const req = ctx.req;
    let postData = "";
    req.addListener("data", (data) => {
      postData = postData + data;
    });

    req.addListener("end", () => {
      if (postData) {
        let parseData = transStringToObject(postData);
        resolve(parseData);
      } else {
        resolve("没有数据");
      }
    });
  });
}
async function transStringToObject(data) {
  let result = {};
  let dataList = data.split("&");
  for (let [index, queryString] of dataList.entries()) {
    let itemList = queryString.split("=");
    result[itemList[0]] = itemList[1];
  }
  return result;
}

3.2.2 使用koa-bodyparser的具体示例

const Koa = require("koa");
const fs = require("fs");
const app = new Koa();

const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");

// post请求参数解析示例
home.get("form", async (ctx) => {
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/b">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <p>email</p>
        <input name="email" /><br/>
        <button type="submit">submit</button>
      </form>
    `;
    ctx.body = html;
});
home.post("b", async (ctx) => {
    // 普通解析逻辑
    // const body = await parseRequestPostData(ctx);
    // ctx.body = body;

    // 使用koa-bodyparser会自动解析表单的数据然后放在ctx.request.body中
    let postData = ctx.request.body;
    ctx.body = postData;
});

let router = new Router();
router.use("/", home.routes()); //http://localhost:3000

app.use(bodyParser()); // 这个中间件的注册应该放在router之前!
app.use(router.routes());

app.listen(3002);

3.2.3 根据示例实现koa-bodyparser

ctx.methodPOST请求时,自动解析ctx.request.body,主要分为:

  • form类型
  • json类型
  • text类型

根据不同的类型调用不同的解析方法,然后赋值给ctx.request.body

/**
 * 注册对应的监听方法,进行request流数据的读取
 * @param req
 */
function readStreamBody(req) {
  return new Promise((resolve, reject) => {
    let postData = "";
    req.addListener("data", (data) => {
      postData = postData + data;
    });

    req.addListener("end", () => {
      if (postData) {
        resolve(postData);
      } else {
        resolve("没有数据");
      }
    });
  });
}

async function parseQuery(data) {
  let result = {};
  let dataList = data.split("&");
  for (let [index, queryString] of dataList.entries()) {
    let itemList = queryString.split("=");
    result[itemList[0]] = itemList[1];
  }
  return result;
}

async function parseJSON(ctx, data) {
  let result = {};
  try {
    result = JSON.parse(data);
  } catch (e) {
    ctx.throw(500, e);
  }
  return result;
}

function bodyParser() {
  return async (ctx, next) => {
    if (!ctx.request.body && ctx.method === "POST") {
      let body = await readStreamBody(ctx.request.req);
      // With Content-Type: text/html; charset=utf-8
      // this.is('html'); // => 'html'
      // this.is('text/html'); // => 'text/html'
      // this.is('text/', 'application/json'); // => 'text/html'
      //
      // When Content-Type is application/json
      // this.is('json', 'urlencoded'); // => 'json'
      // this.is('application/json'); // => 'application/json'
      // this.is('html', 'application/'); // => 'application/json'
      //
      // this.is('html'); // => false
      let result;
      if (ctx.request.is("application/x-www-form-urlencoded")) {
        result = await parseQuery(body);
      } else if (ctx.request.is("application/json")) {
        result = await parseJSON(ctx, body);
      } else if (ctx.request.is("text/plain")) {
        result = body;
      }
      ctx.request.body = result;
    }
    await next();
  };
}

module.exports = bodyParser;

参考

  1. Koa.js 设计模式-学习笔记
  2. Koa2进阶学习笔记

白边
206 声望35 粉丝

源码爱好者,已经完成vue2和vue3的源码解析+webpack5整体流程源码+vite4开发环境核心流程源码+koa2源码