Koa 原理实现
零、Koa 源码目录结构
.
├── History.md
├── LICENSE
├── Readme.md
├── dist
│ └── koa.mjs
├── lib
│ ├── application.js # 最核心的模块
│ ├── context.js # 上下文对象
│ ├── request.js # Koa 自己实现的请求对象
│ └── response.js # Koa 自己实现的响应对象
└── package.json
一、基本结构
使用:
const Koa = require('./koa')
const app = new Koa()
app.listen(3000)
application.js
module.exports = class Application {
listen(...args) {
const server = http.createServer((req, res) => {
res.end('My Koa')
})
return server.listen(...args)
}
}
二、实现中间件功能
-
Koa 会把所有中间件组合成一个大的 Promise
-
当这个 Promise 执行完毕之后,会采用当前的 ctx.body 进行结果响应
-
next 前面必须有 await 或者 return next,否则执行顺序达不到预期
-
如果都是同步执行,加不加 await 都无所谓
-
我不知道后续是否有异步逻辑,所以建议写的时候都加上 await next
收集中间件
使用方式:
const Koa = require('./koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.body = 'foo'
})
app.use((ctx, next) => {
ctx.body = 'Koa'
})
app.listen(3000)
application.js
const http = require("http");
module.exports = class Application {
constructor() {
this.middleware = [];
}
use(fn) {
if (typeof fn !== "function") {
throw new TypeError("middleware must be a function!");
}
this.middleware.push(fn);
}
listen(...args) {
const server = http.createServer((req, res) => {
res.end("My Koa");
});
return server.listen(...args);
}
};
const http = require("http");
class Application {
constructor() {
this.middleware = []; // 保存用户添加的中间件函数
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
use(fn) {
this.middleware.push(fn);
}
// 异步递归遍历调用中间件处理函数
compose(middleware) {
return function () {
const dispatch = (index) => {
if (index >= middleware.length) return Promise.resolve();
const fn = middleware[index];
return Promise.resolve(
// TODO: 上下文对象
fn({}, () => dispatch(index + 1)) // 这是 next 函数
);
};
// 返回第 1 个中间件处理函数
return dispatch(0);
};
}
callback() {
const fnMiddleware = this.compose(this.middleware);
const handleRequest = (req, res) => {
fnMiddleware()
.then(() => {
console.log("end");
res.end("My Koa");
})
.catch((err) => {
res.end(err.message);
});
};
return handleRequest;
}
}
module.exports = Application;
测试中间件执行流程
/**
* - Koa 中间件功能
* + use 的时候收集起来
* + 请求进来的时候调用
*/
const Koa = require("./koa");
const app = new Koa();
const one = (ctx, next) => {
console.log(">> one");
next();
console.log("<< one");
};
const two = (ctx, next) => {
console.log(">> two");
next();
console.log("<< two");
};
const three = (ctx, next) => {
console.log(">> three");
next();
console.log("<< three");
};
app.use(one);
app.use(two);
app.use(three);
// console.log(app.middleware)
app.listen(3000);
三、处理上下文对象
初始化上下文对象
context 上下文对象的使用方式:
/**
* Koa Context
*/
const Koa = require("./koa");
const app = new Koa();
app.use(async (ctx, next) => {
// Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。
// 每个请求都将创建一个 Context,并在中间件中作为参数引用
console.log(ctx); // Context 对象
console.log(ctx.req.url);
console.log(ctx.req.method);
console.log(ctx.request.req.url);
console.log(ctx.request.req.method);
console.log(ctx.req); // Node 的 request 对象
console.log(ctx.res); // Node 的 response 对象
console.log(ctx.req.url);
console.log(ctx.request); // Koa 中封装的请求对象
console.log(ctx.request.header); // 获取请求头对象
console.log(ctx.request.method); // 获取请求方法
console.log(ctx.request.url); // 获取请求路径
console.log(ctx.request.path); // 获取不包含查询字符串的请求路径
console.log(ctx.request.query); // 获取请求路径中的查询字符串
// Request 别名
// 完整列表参见:https://koa.bootcss.com/#request-
console.log(ctx.header);
console.log(ctx.method);
console.log(ctx.url);
console.log(ctx.path);
console.log(ctx.query);
// Koa 中封装的响应对象
console.log(ctx.response);
ctx.response.status = 200;
ctx.response.message = "Success";
ctx.response.type = "plain";
ctx.response.body = "Hello Koa";
// Response 别名
ctx.status = 200;
ctx.message = "Success";
ctx.type = "plain";
ctx.body = "Hello Koa";
});
app.listen(3000);
context.js
const context = {
}
module.exports = context
request.js
const url = require("url");
const request = {
get method() {
return this.req.method;
},
get header() {
return this.req.headers;
},
get url() {
return this.req.url;
},
get path() {
return url.parse(this.req.url).pathname;
},
get query() {
return url.parse(this.req.url, true).query;
},
};
module.exports = request;
response.js
const response = {
set status(value) {
this.res.statusCode = value;
},
};
module.exports = response;
application.js
const http = require("http");
const context = require("./context");
const request = require("./request");
const response = require("./response");
class Application {
constructor() {
this.middleware = []; // 保存用户添加的中间件函数
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
use(fn) {
this.middleware.push(fn);
}
// 异步递归遍历调用中间件处理函数
compose(middleware) {
return function (context) {
const dispatch = (index) => {
if (index >= middleware.length) return Promise.resolve();
const fn = middleware[index];
return Promise.resolve(
// TODO: 上下文对象
fn(context, () => dispatch(index + 1)) // 这是 next 函数
);
};
// 返回第 1 个中间件处理函数
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));
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;
}
callback() {
const fnMiddleware = this.compose(this.middleware);
const handleRequest = (req, res) => {
// 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染
const context = this.createContext(req, res);
fnMiddleware(context)
.then(() => {
res.end("My Koa");
})
.catch((err) => {
res.end(err.message);
});
};
return handleRequest;
}
}
module.exports = Application;
处理上下文对象中的别名
context.js
const context = {
// get method () {
// return this.request.method
// },
// get url () {
// return this.request.url
// }
};
defineProperty("request", "method");
defineProperty("request", "url");
function defineProperty(target, name) {
context.__defineGetter__(name, function () {
return this[target][name];
});
// Object.defineProperty(context, name, {
// get () {
// return this[target][name]
// }
// })
}
module.exports = context;
四、处理 ctx.body
保存 body 数据
response.js
const response = {
set status(value) {
this.res.statusCode = value;
},
_body: "", // 真正用来存数据的
get body() {
return this._body;
},
set body(value) {
this._body = value;
},
};
module.exports = response;
发送 body 数据
context.js
const context = {
// get method () {
// return this.request.method
// },
// get url () {
// return this.request.url
// }
};
defineProperty("request", "method");
defineProperty("request", "url");
defineProperty("response", "body");
function defineProperty(target, name) {
// context.__defineGetter__(name, function () {
// return this[target][name]
// })
Object.defineProperty(context, name, {
get() {
return this[target][name];
},
set(value) {
this[target][name] = value;
},
});
}
module.exports = context;
application.js
callback() {
const fnMiddleware = this.compose(this.middleware);
const handleRequest = (req, res) => {
// 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染
const context = this.createContext(req, res);
fnMiddleware(context)
.then(() => {
res.end(context.body)
// res.end('My Koa')
})
.catch((err) => {
res.end(err.message);
});
};
return handleRequest;
}
更灵活的 body 数据
callback() {
const fnMiddleware = this.compose(this.middleware);
const handleRequest = (req, res) => {
// 每个请求都会创建一个独立的 Context 上下文对象,它们之间不会互相污染
const context = this.createContext(req, res);
fnMiddleware(context)
.then(() => {
respond(context);
// res.end(context.body)
// res.end('My Koa')
})
.catch((err) => {
res.end(err.message);
});
};
return handleRequest;
}
function respond(ctx) {
const body = ctx.body;
const res = ctx.res;
if (body === null) {
res.statusCode = 204;
return res.end();
}
if (typeof body === "string") return res.end(body);
if (Buffer.isBuffer(body)) return res.end(body);
if (body instanceof Stream) return body.pipe(ctx.res);
if (typeof body === "number") return res.end(body + "");
if (typeof body === "object") {
const jsonStr = JSON.stringify(body);
return res.end(jsonStr);
}
}