koa
- 作用:将原始http模块进行封装,简化流程
- 相比express的优势:基于promise,而不是基于回调,可以在实例.onerror做异常统一处理
拆解
- 模块导入的是一个类,需要new出一个实例
- 实例是一个封装过的http服务实例,可以listen
- 实例上有use方法,核心api
- 那可以多次new,所以要保证数据的私有性
use
- 传入一个函数
- 函数接收两个参数,ctx与next
- use的特性:可以多次调用,函数并不会马上执行,而是等前一个next调用后才执行
- 内部是将参数封装成promise,等待外部resolve就执行end
- 所以传递函数写成async与await最好,后面会阐述原因
ctx
- 这个koa将http默认的req与res集成进来,并且内部封装了response与request,且在ctx上自定义了一些方法
- req、res、response、request互相引用
body
- 常用的一个操作:通过ctx.body进行写入内容
- ctx.body可以写多次,以最后一次为准,koa会自己返回
- 也可以写成流的形式,buffer、string也可以,json会在返回前自动stringify一层
- 可以写多次,证明了一件事情,它并不是直接end的,因为多次end会报错
const Koa = require("koa");
const app = new Koa();
app.use(function (ctx, next) {
ctx.body = "ok";
ctx.body = "ok1";
});
app.listen(3000, () => {
console.log("3000");
});
next
- 下一个实例
- 递归操作,这个就是koa洋葱模型的由来
[1[3[5,6]4]2]一层包一层
app.use(function (ctx, next) {
console.log(1);
ctx.body = "ok";
console.log(2);
});
app.use(function (ctx, next) {
console.log(3);
ctx.body = "ok1";
console.log(4);
});
app.use(function (ctx, next) {
console.log(5);
ctx.body = "ok2";
console.log(6);
});
app.use(function (ctx, next) {
console.log(1);
ctx.body = "ok";
next();
console.log(2);
});
app.use(function (ctx, next) {
console.log(3);
ctx.body = "ok1";
next();
console.log(4);
});
app.use(function (ctx, next) {
console.log(5);
ctx.body = "ok2";
console.log(6);
});
- 解释:next代表的是它下一个函数,所以可以按照下面例子理解
function 1(ctx, next) {
console.log(1);
ctx.body = "ok";
function 2(ctx, next) {
console.log(3);
ctx.body = "ok1";
function 3(ctx, next) {
console.log(5);
ctx.body = "ok2";
console.log(6);
}
console.log(4);
}
console.log(2);
}
app.use(function (ctx, next) {
console.log(1);
ctx.body = "ok";
next();
console.log(2);
});
app.use(function (ctx, next) {
console.log(3);
setTimeout(() => {
ctx.body = "ok1";
next();
}, 1000);
console.log(4);
});
app.use(function (ctx, next) {
console.log(5);
ctx.body = "ok2";
console.log(6);
});
- 解释:跟我们正常的开发流程一样,函数自上而下执行,先走同步,再走异步
- 第一次走了同步,然后后面异步了,但
并没有等待,同步代码执行完,直接返回了,所以返回值为ok
- 所以koa推荐使用async与await操作,在上一个await回来之前,进入等待状态
const sleep = (time, fn) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
fn();
resolve();
}, time);
});
};
app.use(async function (ctx, next) {
console.log(3);
await sleep(1000, () => {
ctx.body = "ok1";
next();
});
console.log(4);
});
- 正确做法
- 就让它一步一步执行,最后一次加上next,如果没有就不执行,不影响的,这样也可以防止后续其他操作忘记在前面加next导致的程序卡壳,还得排查原因,麻烦
- 而且不确定他是不是异步代码,防止意外,直接await
app.use(async function (ctx, next) {
console.log(1);
ctx.body = "ok";
await next();
console.log(2);
});
app.use(async function (ctx, next) {
console.log(3);
await sleep(1000, async () => {
ctx.body = "ok1";
await next();
});
console.log(4);
});
app.use(async function (ctx, next) {
console.log(5);
ctx.body = "ok2";
await next();
console.log(6);
});
源码分析
- package.json中可以看到main指向lib下的applications.js
- 进入lib文件夹下可以看到context、request、response、applications这四个js文件,这就是核心代码了
- applications返回实例,内部引用context、request、response这三个状态,并且通过Object.create多次处理,防止多个实例与多次请求引用地址之间互相干扰,采用__proto_-的方式连接
- 监听操作采用旧版方法:
__defineGetter__、与__defineSetter__
- 异常操作:用户可能传递的是非promise函数,所以会直接报错,那么使用try包一层
const Koa = require("koa");
const app = new Koa();
app.use(async function (ctx, next) {
throw Error(11);
console.log(ctx.body);
ctx.body = "1";
ctx.body = "2";
});
app.on("error", (err) => {
console.log("--------", err);
});
app.listen(3000, () => {
console.log("3000");
});
app.use(async function (ctx, next) {
next();
next();
});
app.on("error", (err) => {
console.log("--------", err);
});
app.listen(3000, () => {
console.log("3000");
});
application.js
const context = require("./context");
const request = require("./request");
const response = require("./response");
const EventEmitter = require("events");
const Stream = require("stream");
const http = require("http");
class Application extends EventEmitter {
constructor() {
super();
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
this.middleware = [];
}
use(fn) {
this.middleware.push(fn);
}
createContext(req, res) {
const ctx = Object.create(this.context);
const request = Object.create(this.request);
const response = Object.create(this.response);
ctx.req = req;
ctx.res = res;
ctx.request = request;
ctx.response = response;
ctx.request.req = ctx.response.req = req;
ctx.request.res = ctx.response.res = res;
ctx.app = this;
ctx.context = ctx;
return ctx;
}
compose(ctx) {
let middleware = this.middleware;
let p = -1;
const dispatch = (i) => {
if (i <= p) {
throw Error("next() called multiple times");
}
p = i;
if (i === middleware.length) return Promise.resolve();
let fn = middleware[i];
try {
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
};
return dispatch(0);
}
handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
res.statusCode = 404;
this.compose(ctx)
.then(() => {
if (typeof ctx.body === "string" || Buffer.isBuffer(ctx.body)) {
res.statusCode = 200;
res.end(ctx.body);
} else if(ctx.body instanceof Stream) {
ctx.body.pipe(res);
} else {
res.end("Not Found");
}
})
.catch((err) => {
this.emit("error", err);
res.statusCode = 500;
res.end("Internal Server Error");
});
};
listen(...args) {
const server = http.createServer(this.handleRequest);
server.listen(...args);
}
}
module.exports = Application;
context.js
const context = {};
function defineGetter(target, key) {
context.__defineGetter__(key, function () {
return this[target][key];
});
}
function defineSetter(target, key) {
context.__defineSetter__(key, function (value) {
this[target][key] = value;
});
}
defineGetter("request", "path");
defineGetter("request", "query");
defineGetter("request", "header");
defineGetter("request", "headers");
defineGetter("response", "body");
defineSetter("response", "body");
module.exports = context;
request.js
const url = require("url");
const request = {
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 body(value) {
this._body = value;
},
get body() {
return this._body;
},
};
module.exports = response;
中间件
- 中间件就是将我们的代码进行解耦,通过next方法连接,本质还是对use的应用
- 中间件推荐封装成一个函数,return出一个promise
封装一个获取post请求体的中间件
- post的请求要通过事件监听去拿到,那么多个post就要处理多次,我们就可以将它统一处理
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
if (ctx.path === "/login" && ctx.method === "GET") {
ctx.type = "text/html;charset=utf-8;";
ctx.body = `
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="username">
<input type="password" name="password">
<button>提交</button>
</form>
`;
} else {
return next();
}
});
app.use(async (ctx, next) => {
if (ctx.path === "/login" && ctx.method === "POST") {
let arr = [];
ctx.body = await new Promise((resolve, reject) => {
ctx.req.on("data", (chunk) => {
arr.push(chunk);
});
ctx.req.on("end", () => {
resolve(Buffer.concat(arr).toString());
});
ctx.req.on("error", (err) => {
reject(err);
});
});
}
});
app.listen(3000, () => {
console.log("server start 3000");
});
const bodyparser = (params) => {
return async (ctx, next) => {
if (ctx.method === "POST") {
let arr = [];
ctx.request.body = await new Promise((resolve, reject) => {
ctx.req.on("data", (chunk) => {
arr.push(chunk);
});
ctx.req.on("end", () => {
resolve(Buffer.concat(arr).toString());
});
ctx.req.on("error", (err) => {
reject(err);
});
});
}
return next();
};
};
app.use(bodyparser());
app.use(async (ctx, next) => {
if (ctx.path === "/login" && ctx.method === "POST") {
ctx.body = ctx.request.body;
}
});
const uuid = require("uuid");
Buffer.prototype.split = function (bodunary) {
let arr = [];
let offset = 0;
let curPosition = 0;
while (-1 !== (curPosition = this.indexOf(bodunary, offset))) {
arr.push(this.slice(offset, curPosition));
offset = curPosition + bodunary.length;
}
arr.push(this.slice(offset));
return arr;
};
const bodyparser = (params) => {
return async (ctx, next) => {
if (ctx.method === "POST") {
let arr = [];
ctx.request.body = await new Promise((resolve, reject) => {
ctx.req.on("data", (chunk) => {
arr.push(chunk);
});
ctx.req.on("end", () => {
let body = Buffer.concat(arr);
const contentType = ctx.get("Content-Type");
if (contentType === "application/x-www-form-urlencoded") {
resolve(querystring.parse(body.toString()));
} else if (contentType === "application/json") {
resolve(body.toJSON());
} else {
if (contentType.includes("multipart/form-data")) {
const boundary = "--" + contentType.split("=")[1];
let lines = body.split(boundary).slice(1, -1);
body = {};
lines.forEach((lineBuffer) => {
let [head, content] = lineBuffer.split("\r\n\r\n");
let key = head.toString().match(/name="(.+?)"/)[1];
if (head.includes("filename")) {
let filename = uuid.v4();
let content = lineBuffer.slice(head.length + 4, -2);
fs.writeFile(path.join(params, filename), content);
body[key] = {
originName: head.toString().match(/filename="(.+?)"/)[1],
filename: path.join(params, filename),
size: content.length,
};
} else {
body[key] = content.toString().replace("\r\n", "");
}
});
resolve(body);
}
}
});
ctx.req.on("error", (err) => {
reject(err);
});
});
}
return next();
};
};
app.use(bodyparser(path.join(__dirname, "upload")));
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="username">
<input type="password" name="password">
<button>提交</button>
</form>
修改为form-data格式
<form action="/login" method="post" enctype="multipart/form-data">
<input type="text" name="username">
<input type="password" name="password">
<input type="file" name="file" id="">
<button>提交</button>
</form>
文件上传
<form action="/login" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="">
<button>提交</button>
</form>
路由中间件
@koa/router第三方库
- 它可以让路由简化,不用去单独判断请求类型了
const Router = require("@koa/router");
const router = new Router();
app.use(router.routes());
router.post("/login", async (ctx, next) => {
ctx.body = ctx.request.body;
});
实现一下
class Router {
constructor() {
this.stack = [];
}
compose(stack, ctx, next) {
function dispath(i) {
if (i === stack.length) return next();
let { callback } = stack[i];
return Promise.resolve(callback(ctx, dispath(i + 1)));
}
return dispath(0);
}
routes() {
return async (ctx, next) => {
let requestPath = ctx.path;
let requestMethod = ctx.method.toLowerCase();
const stack = this.stack.filter((layer) => {
return (layer.path = requestPath && layer.method === requestMethod);
});
return this.compose(stack, ctx, next);
};
}
}
class Layer {
constructor(path, method, callback) {
this.path = path;
this.method = method;
this.callback = callback;
}
}
["get", "post", "delete"].forEach((method) => {
Router.prototype[method] = function (path, callback) {
this.stack.push(new Layer(path, method, callback));
};
});
静态资源托管
koa-static第三方库
- 传一个地址,将对应目录文件托管
- 可使用多次
const server = require("koa-static");
app.use(server(path.join(__dirname, "/upload")));