Koa 源码分析&手写 Koa
启动 Koa 服务
Koa 的最简 demo
const Koa = require("koa");const app = new Koa(); //new 操作符,那么 koa 抛出的肯定是一个构造函数(function)或者类(class)app.listen(8889, "0.0.0.0", () => { //Koa 实例调用了 listen 方法,并且接收了几个参数,如果先不管参数,那么 Koa 实例内肯定包含一个 listen 方法 console.log(`启动成功8889`);});class Koa { listen(...args) {}}启动成功后会调起一个服务
- node 的 http 模块的 createServer
- createServer 得到一个 server 后也拥有一个 listen 方法,并且完全对应 demo 里 app.listen 的参数
Koa 的 listen 实现如下
const http = require("http");class Koa { listen(...args) { const server = http.createServer(); return server.listen(...args); }}验证一下
const Koa = require("./listen.js");const app = new Koa();app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});洋葱模型
单个中间件
单个中间件的最简 demo
const Koa = require("koa");const app = new Koa();app.use(() => { //可以推断出Koa类有一个use函数,并且接收一个函数参数 console.log(1);});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});class Koa { constructor() { // 初始化函数 this.middleware = () => {} } use(cb) { // 保存函数 this.middleware = cb }, listen(...) {...}}可以假设 app.use 是一个注册器,注册了一个函数(中间件),当服务接收到请求后执行这个函数,而 http.createServer api 的参数接收一个函数来监听请求,正好满足我们的需求。
// 源码层const http = require("http");class Koa { constructor() { // 初始化函数 this.middleware = () => {}; } // 一个注册器 use(cb) { // 保存函数 this.middleware = cb; } listen(...args) { // http.createServer接收一个函数参数,用于接收请求 const server = http.createServer((req, res) => { // 接收到请求后执行use中注册的函数 this.middleware(); // 这一段是为了正常结束请求,暂时加上,可以先忽略 res.end("1"); }); return server.listen(...args); }}const Koa = require("./use.js");const app = new Koa();app.use(() => { console.log(1);});app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});多个中间件
const Koa = require("koa");const app = new Koa();app.use(function cb1(ctx, next) { console.log(1); next(); console.log(5);});
app .use(function cb2(ctx, next) { console.log(2); next(); console.log(4); }) .use(function cb3(ctx, next) { console.log(3); });
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});多次使用 app.use 注册中间件,中间件接收 ctx,next 两个参数,因为可以注册多个,需要用有一个地方存起来,比如用数组存储,可以推断出 use 内返回 this,next()会暂停执行当前中间件去执行下一个中间件,实际上就像 js 内的嵌套函数。
class Koa { constructor() { // 老代码 this.middleware = () => {} // 保存中间件数组 this.middleware = []; } // 注册器 use(cb) { // 保存中间件 this.middleware.push(cb); // 链式写法 return this; }}function cb1(next) { console.log(1); next(cb3); console.log(5);}
function cb2(next) { console.log(2); next(cb3); console.log(4);}function cb3(next) { console.log(3);}cb1(cb2);// 放控制台执行 -> 1 2 3 4 5// cb1的next -> cb2// cb2的next -> cb3// cb3的next -> () => {} // 兼容封装 use 函数
- 中间件注册的时机
- 中间件的注册发生在 node 服务启动的时候
- 执行是在请求进来时,所以请求进来的时候
- 我们已经用数组保存了全部的中间件
const middleware = [cb1, cb2, cb3];// 执行cb1的时候,我们可以获取到cb2,并传给cb1middleware[0](middleware[1]);const http = require("http");class Koa { constructor() { // 初始化中间件数组,因为可能是多个 this.middleware = []; } // 注册器 use(cb) { // 保存所有注册的中间件 this.middleware.push(cb); return this; } compose() { const dispatch = (i) => { // 从数组中取出中间件 const fn = this.middleware[i]; // 执行中间件,并传递执行下一个中间件的函数 // dispatch(i + 1)会立即执行下一个中间件,所以用一个函数包起来,何时执行交给用户自己选择 return fn(() => dispatch(i + 1)); }; // 执行第一个中间件 return dispatch(0); } callback() { return (req, res) => { // 接收请求后执行compose this.compose(); // 这一段是为了让Postman正常结束请求,暂时加上,可以先忽略 res.end("111"); }; } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); }}const Koa = require("./useSync.js");const app = new Koa();app.use(function cb1(next) { console.log(1); next(); console.log(5);});
app .use(function cb2(next) { console.log(2); next(); console.log(4); }) .use(function cb3(next) { console.log(3); });
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});middleware 是一个数组 middleware 内每一项都是一个函数,为了代码更健壮,最好在启动阶段就进行强制校验。
运行时机-启动时,也就是执行 callback 的时候 需要 compose 提供的功能-校验 + 执行中间件。利用闭包,执行时校验并返回一个执行中间件的函数,callback 执行时立即执行 compose,请求进来时执行中间件函数
const http = require("http");class Koa { constructor() { this.middleware = []; } use(cb) { this.middleware.push(cb); return this; } 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!"); } // 返回执行中间件的函数 return () => { const dispatch = (i) => { // 从数组中取出中间件 const fn = middleware[i]; // 执行中间件,并传递执行下一个中间件的函数 // 这里注意,dispatch(i + 1)会立即执行下一个中间件,所以用一个函数包起来,何时执行交给用户自己选择 return fn(() => dispatch(i + 1)); }; // 执行第一个中间件 return dispatch(0); }; } callback() { // 启动时校验 const fn = this.compose(this.middleware); return (req, res) => { // 请求进来时执行 fn(); res.end("111"); }; } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); }}const Koa = require("./useSyncValidator.js");const app = new Koa();app.use(111);
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});边界
Koa 的边界情况-
- Koa 中规定每个中间件只能执行一次 next
- 最后一个中间件也存在 next,执行 next 会报错。因为 i >= middleware.length,用 middleware[i]获取到的是 undefined
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!') } return () => { const dispatch = (i) => { const fn = middleware[i] return fn(() => dispatch(i + 1)) } return dispatch(0) }}每一个 next 都是一个 dispatch(i + 1),也就是说每次执行 next 的时候 i 都是相同的。 在 dispatch 外再维护一个索引,dispatch 执行的时候 index = i,再执行一次 dispatch 的时候判断 i 是不是小于等于 index,就可以判断当前 next 执行次数。
修改 compose 函数
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!') } return () => { let index = -1 const dispatch = (i) => { if (i <= index) { return console.error(new Error('next() called multiple times')) } index = i const fn = middleware[i] return fn(() => dispatch(i + 1)) } return dispatch(0) }}当最后一个 next 的时候,默认返回一个空函数就行
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!') } return () => { let index = -1 const dispatch = (i) => { if (i <= index) { return console.error(new Error('next() called multiple times')) } index = i // 添加了这一段 if (i >= middleware.length) return () => {} const fn = middleware[i] return fn(() => dispatch(i + 1)) } return dispatch(0) }}const Koa = require("./useSync.js");const app = new Koa();app.use(function cb1(next) { next(); next(); console.log(1);});
app.use(function cb2(next) { next();});
app.use(function cb3(next) { console.log(3); next();});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});异步
const Koa = require("./useSync.js");const app = new Koa();app.use(async (next) => { console.log(1); await next(); console.log(4);});
app.use(async (next) => { console.log(2); await timeout(); console.log(3);});
function timeout() { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 2000); });}底层原理就是基于 Promise 实现的自执行的 Generator 函数,async 最后会返回一个 Promise,await 等于 yield。
const Koa = require("koa");const app = new Koa();app.use((ctx, next) => { console.log(1); next().then(() => { console.log(3); });});
app.use((ctx, next) => { console.log(2);});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});最后一个中间件是一个普通函数,但是上一个中间件却用了 next().then(),也就是认为最后一个中间件是一个 Promise。 在 Koa 是被允许的,也就是 Koa 中兼容了普通函数和 Promise。
Promise.resolve
// 函数是Promise直接返回,不是就包一层PromisePromise.resolve = function (fn) { if (fn instanceof Promise) { return fn; } else { return new Promise((resolve) => { resolve(fn); }); }};所以再次修改 compose 函数
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!') } return () => { let index = -1 const dispatch = (i) => { if (i <= index) { return console.error(new Error('next() called multiple times')) } index = i if (i >= middleware.length) return () => {} const fn = middleware[i] // 修改了这 return Promise.resolve(fn(() => dispatch(i + 1))) } return dispatch(0) }}const Koa = require("./useAsync.js");const app = new Koa();app.use((next) => { console.log(1); next().then(() => { console.log(3); });});
app.use((next) => { return next().then(() => { console.log(2); });});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!') } return () => { let index = -1 const dispatch = (i) => { if (i <= index) { return console.error(new Error('next() called multiple times')) } index = i // 就这里 if (i >= middleware.length) return Promise.resolve() const fn = middleware[i] return Promise.resolve(fn(() => dispatch(i + 1))) } return dispatch(0) }}错误情况的兼容
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!') } return () => { let index = -1 const dispatch = (i) => { // Promise.reject if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i if (i === middleware.length) return Promise.resolve() const fn = middleware[i] // try catch try{ return Promise.resolve(fn(() => dispatch(i + 1))) } catch(err) { return Promise.reject(err) } } return dispatch(0) }}ctx 封装
基础结构
官方对 ctx 的定义,简单来说就是封装了 node 的 response 和 request,只简单的封装一下 ctx,request,response 和 body 首先,ctx 是一个对象,request 和 response 是 ctx 的一个属性并且也是一个对象。
const ctx = {};
module.exports = ctx;然后上述对 Context 的描述里说到,每个请求都会创建一个新的 Context,并在中间件中引用,也就是说每次 server 接收到请求后(callback 内返回的函数),都会根据 req 和 res 创建一个 Context。
ctx.request-根据 node 的 request 封装而来 ctx.response-根据 node 的 response 封装而来 ctx.req-node 的 request 对象 ctx.res-node 的 response 对象
中间件第一个参数为 Context
const http = require("http");const context = require("./src/context");const request = require("./src/request");const response = require("./src/response");class Koa { constructor() { this.middleware = []; // 初始化ctx等,引用类型,避免引用 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } use(cb) { this.middleware.push(cb); } 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!"); } // 接收ctx return (ctx) => { let index = -1; const dispatch = (i) => { if (i <= index) return Promise.reject(new Error("next() called multiple times")); index = i; if (i === middleware.length) return Promise.resolve(); const fn = middleware[i]; try { // 增加第一个参数为ctx return Promise.resolve(fn(ctx, () => dispatch(i + 1))); } catch (err) { return Promise.reject(err); } }; return dispatch(0); }; } 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)); // req,res为node原生request和response // 给request和response也赋值req,res是为了利用this获取到原生req,res,然后做二次封装 context.req = request.req = response.req = req; context.res = request.res = response.res = res; return context; } callback() { const fn = this.compose(this.middleware); return (req, res) => { // 每个请求都创建一个新的context const ctx = this.createContext(req, res); // 传入ctx fn(ctx); res.end("111"); }; } listen(...args) { const server = http.createServer(this.callback()); return server.listen(...args); }}const Koa = require("./ctx.js");const app = new Koa();app.use((ctx, next) => { next();});
app.use((ctx) => { console.log(ctx);});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});输出的 ctx 大概如下结构
{ request: { req: <node req>, res: <node res> }, response: { req: <node req>, res: <node res> }, req: <node req>, res: <node res>}response & request
已经给 request 和 response 赋值了,所以可以对 req 和 res 做一层有用的封装
module.exports = { // req: <node req>, // res: <node res> get header() { return this.req.headers }, set header(val) { this.req.headers = val }, get url() { return this.req.url }, set url(val) { this.req.url = val } ... api内的方法}module.exports = { // req: <node req>, // res: <node res> get header() { const { res } = this; return typeof res.getHeaders === "function" ? res.getHeaders() : res._headers || {}; // Node < 7.7 }, set header(val) { console.log(val, 222); }, get body() { return this._body; }, // 这里只是随便赋了个值,源码内做了很多判断为了适应不同的数据 set body(val) { this._body = val; }, //...};const Koa = require("./ctx.js");const app = new Koa();app.use((ctx, next) => { next();});
app.use((ctx) => { console.log(ctx.request.header, 2); console.log(ctx.request.url, 3);});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});ctx 别名
这里就对 request 的 header 和 url 做一次实现,其它都同理。 源码内利用 delegates 包做了一层代理,其实也可以用 proxy 等,实现一下 request 的代理。
const delegate = require("delegates");const ctx = {};
// 将ctx.request的header和url属性代理到ctx下delegate(ctx, "request").access("header").access("url");
// 代理response.bodydelegate(ctx, "response").access("body");
module.exports = ctx;const Koa = require("./ctx.js");const app = new Koa();app.use((ctx, next) => { next();});
app.use((ctx) => { console.log(ctx.request.url, 1); // 可以直接访问 console.log(ctx.url, 2);});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});ctx.body
Koa 中给 ctx.body 赋值后,请求结束会识别 ctx.body 的类型然后返回对应的数据
const Koa = require("koa");const app = new Koa();app.use((ctx, next) => { console.log(1); next(); console.log(3);});
app.use((ctx, next) => { console.log(2); ctx.body = "<html>111</html>";});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});// node koa.js -> 启动成功8889// Postman请求0.0.0.0:8889 -> 1 2 3只需要走完全部中间件的时候,判断 body 类型,然后使用 res.end 结束请求即可
class Koa { callback() { const fn = this.compose(this.middleware); return (req, res) => { const ctx = this.createContext(req, res); // 中间件全部走完后执行 fn(ctx).then(() => respond(ctx)); }; }}
function respond(ctx) { const res = ctx.res; let body = ctx.body; // 判断body类型,自动设置Content-Type if (typeof body === "string") { res.setHeader( "Content-Type", /^s*</.test(body) ? "text/html" : "text/plain" ); } if (typeof body === "object" && ctx.body !== null) { res.setHeader("Content-Type", "application/json"); body = JSON.stringify(body); } // 结束请求并返回body res.end(body);}const Koa = require("./ctx.js");const app = new Koa();app.use((ctx, next) => { next();});
app.use((ctx) => { ctx.body = { msg: "成功啦", };});
app.listen(8889, "0.0.0.0", () => { console.log(`启动成功8889`);});