1. 简介
Koa是基于Node.js的web开发框架(有人说是框架的框架),特点是轻量可扩展性强并且使用async处理异步让代码更加清晰且错误处理更加容易。
优点
- 拥抱async、await完美解决回掉地域
- 使用中间件完成对http请求处理,代码逻辑清晰
- 包装context对象代理req、res使开发更加简洁
- 错误处理简单
缺点
- koa本身只包含http服务,企业级应用需自己扩展
- koa社区较小
功能概览
2. 启动服务
原生node
const http = require("http");
const server = http.createServer((req, res) => {
res.end("hello world");
});
server.listen(3000, () => {
console.log("server is running in localhost:3000");
});
KOA
const koa = require("koa");
const app = new koa();
app.use(async (ctx, next) => {
ctx.body = "hello world";
});
app.listen(3000);
application.js
// 继承自Emitter,可方便使用发布订阅
class Application extends Emitter {
constructor(options) {
super();
options = options || {};
// 中间件初始化
this.middleware = [];
// 三个重要对象
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 1. 处理所有中间件并返回一个嵌套函数
// 2. 基于req和res封装ctx对象
// 3. 将ctx作为参数传递给中间件所组合成的一个嵌套函数
// 4. 嵌套函数执行完向res中写入数据
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount("error")) this.on("error", this.onerror);
const handleRequest = (req, res) => {
// 创建context对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// listen方法封装http.createServer方法
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
3. context
Context 对象,表示一次会话的上下文(req和res),每一次请求都会创建一个context,为了方便开发,context上很多方法都是对request和response的简单映射,
application.js
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
// request、response为Koa扩展的对象
const response = context.response = Object.create(this.response);
// res、req为NodeJS原生对象,为了后面使用原生res、req的属性、方法
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 = {};
// 构建完成的context上下文对象
return context;
}
获取request、response上的属性、方法
ctx.request.header
为了使用方便,koa将request和response的方法、属性代理到ctx上
const delegate = require('delegates');
delegate(proto, 'response')
.method('attachment') // 代理方法
.access('status') // 代理getter和setter
.getter('writable'); // 代理getter
...
4. 中间件
洋葱模型
原生node
const http = require("http");
const server = http.createServer((req, res) => {
console.log(req.url);
const { url } = req;
res.setHeader("Content-type", "text/html");
if (url === "/home") {
res.end("I am home");
} else {
res.end("I am other");
}
});
server.listen(3000, () => {
console.log("server is running in localhost:3000");
});
问题:业务逻辑复杂后,代码不清晰,
KOA
const koa = require("koa");
const app = new koa();
const mid1 = async (ctx, next) => {
ctx.type = "text/html;";
await next();
ctx.body = ctx.body + " 中间件1";
};
const mid2 = async (ctx, next) => {
ctx.body = "hello";
await next();
ctx.body = ctx.body + " 中间件2";
};
app.use(mid1);
app.use(mid2);
app.listen(3000);
//输出 hello 中间件2 中间件1
- 运行到await next()的时候就会暂停当前程序,进入下一个中间件
application.js
use(fn) {
// 将中间件添加到this.middleware中
this.middleware.push(fn);
return this;
}
koa-compose
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) <span style="color:#2228f8">return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
koa-compose返回的是一个Promise,Promise中取出第一个中间件并传入context和第一个next函数来执行。
第二个next函数里也是返回的是一个Promise,Promise中取出第二个中间件传入context和第二个next函数来执行。
第三个…
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。 这样就把所有中间件串联起来了。
koa中间件非常方便的实现后置处理逻辑
5. 错误处理
默认错误处理
application.js
// koa 会挂载一个默认的错误处理
if (!this.listenerCount("error")) this.on("error", this.onerror);
全局错误
application.js
one rror(err) {
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 === err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
}
application.js
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const one rror = (err) => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, one rror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
每次http请求错误交给ctx.onerror处理
context.js
onerror(err) {
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
this.app.emit('error', err, this);
if (headerSent) {
return;
}
const { res } = this;
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
this.set(err.headers);
this.type = 'text';
let statusCode = err.status || err.statusCode;
if ('ENOENT' === err.code) statusCode = 404;
if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
this.status = err.status = statusCode;
this.length = Buffer.byteLength(msg);
res.end(msg);
}