koa
koa 是 Node.js 的一个 web 开发框架,它是由 Express 原班人马打造的,致力于成为一个更小、更富裕表现力、更强壮的 Web 框架。应用 koa 编写 web 利用,通过组合不同的 generator,能够罢黜反复繁琐的回调函数嵌套,并极大地晋升错误处理的效率。koa 不在内核办法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 利用变得得心应手。
与 Express 区别
这里简略讲下 koa 与 Express 的次要区别:
- Express 封装、内置了很多中间件,比方 connect 和 router,而 koa 则比拟轻量,开发者能够依据本身需要订制框架;
- Express 是基于 callback 来解决中间件的,而 koa 则是基于 async/await;
- 在异步执行中间件时,Express 并非严格依照 洋葱模型 执行中间件,而 koa 则是严格遵循的(体现在二者在中间件为异步函数的时候解决会有不同)。
所以,须要先介绍下洋葱模型。
洋葱模型
洋葱咱们都晓得,一层包裹着一层,层层递进,然而当初不是看其平面的构造,而是须要将洋葱切开来,从切开的立体来看,如图所示:
能够看到,要从洋葱中心点穿过来,就必须先一层层向内穿入洋葱表皮进入中心点,而后再从中心点一层层向外穿出表皮。
这里有个特点:进入时穿入了多少层表皮,进来时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,合乎咱们所说的 栈列表 , 先进后出 的准则。
无论是 Express 还是 koa,都是基于中间件来实现的。中间件次要用于申请拦挡和批改申请或响应后果的。而中间件(能够了解为一个类或者函数模块)的执行形式就须要根据洋葱模型。
洋葱的表皮咱们能够思考为中间件:
从外向内的过程是一个关键词 next();如果没有调用 next(),则不会调用下一个中间件;
而从外向外则是每个中间件执行结束后,进入原来的上一层中间件,始终到最外一层。
也就是说,对于异步中间件,koa 与 Express 在某种状况代码的执行程序会有差别。
异步差别
同样的逻辑,先来 Express:
const express = require('express')
const app = express()
app.use(async (req, res, next) => {const start = Date.now();
console.log(1)
await next();
console.log(2)
})
app.use(async (req, res, next) => {console.log('3')
await next()
await new Promise((resolve) =>
setTimeout(() => {console.log(`wait 1000 ms end`);
resolve()},
1000
)
);
console.log('4')
})
app.use((req, res, next) => {console.log(5);
res.send('hello express')
})
app.listen(3001)
console.log('server listening at port 3001')
失常而言,咱们冀望返回后果程序是:
1
3
5
wait 1000 ms end
4
2
但事实上后果是:
1
3
5
2
wait 1000 ms end
4
同样逻辑的 Koa 代码:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {console.log(1)
await next();
console.log(2)
});
app.use(async (ctx, next) => {console.log(3)
await next();
await new Promise((resolve) =>
setTimeout(() => {console.log(`wait 1000 ms end`);
resolve()},
1000
)
);
console.log(4)
});
// response
app.use(async ctx => {console.log(5)
ctx.body = 'Hello Koa';
});
app.listen(3000);
console.log('app start : http://localhost:3000')
响应差别
大部分状况下,koa 与 Express 的响应是没有区别的,只是写法稍有不同,前者须要 ctx.body = xxx, 而后者须要用 res.send 或 res.json 等办法。
但以下这种状况,就是 Express 不能做到的。
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {ctx.body = 'Hello Koa';});
app.listen(3000);
console.log('app start : http://localhost:3000')
上述代码次要是想给所有的接口增加一个响应头,这个响应头代表着这个接口函数的执行工夫。
在 Express 中,你可能会这样写:
const express = require('express')
const app = express()
app.use(async (req, res, next) => {const start = Date.now();
await next();
const ms = Date.now() - start;
res.header('X-Response-Time', `${ms}ms`);
})
app.use((req, res, next) => {res.send('hello express')
})
app.listen(3001)
console.log('server listening at port 3001')
但申请时会报错:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
因为 res.send 曾经意味着发送响应了,这时你还想再设置响应头,是不容许的。
也就是说,Express 应用 res.send 等办法,会间接进行响应,而 koa 会等所有中间件都实现后,才会响应。
中间件次要解决逻辑
koa 的中间件解决逻辑非常简单,次要放在 koa-compose 中:
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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {if (i <= index) 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, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err)
}
}
}
}
每个中间件调用的 next()其实就是这个:
dispatch.bind(null, i + 1)
还是利用闭包和递归的性质,一个个执行,并且每次执行都是返回 promise。
贴下 koa 中间件的执行流程吧:
oak
对标 nodejs 的 koa 框架,Deno 有开发者参考它开发出一个 oak 框架,用法简直截然不同,学习老本很低,举荐应用。
代码如下:
import {Application} from "https://deno.land/x/oak/mod.ts";
const app = new Application();
app.use(async (ctx, next) => {const start = Date.now();
console.log(1)
await next();
console.log(2)
const ms = Date.now() - start;
ctx.response.headers.set('X-Response-Time', `${ms}ms`);
});
app.use(async (ctx, next) => {const start = Date.now();
console.log(3)
await next();
await new Promise((resolve) =>
setTimeout(() => {console.log(`wait 1000 ms end`);
resolve('wait')
},
1000
)
);
console.log(4)
const ms = Date.now() - start;
console.log(`${ctx.request.method} ${ctx.request.url} - ${ms}`);
});
app.use((ctx) => {console.log(5)
ctx.response.body = "Hello Deno!";
});
console.log('app start : http://localhost:3002')
await app.listen({port: 3002});
中间件解决逻辑
它的中间件解决逻辑在这里:
/** Compose multiple middleware functions into a single middleware function. */
export function compose<
S extends State = Record<string, any>,
T extends Context = Context<S>,
>(middleware: Middleware<S, T>[],
): (context: T, next?: () => Promise<unknown>) => Promise<unknown> {
return function composedMiddleware(
context: T,
next?: () => Promise<unknown>,): Promise<unknown> {
let index = -1;
async function dispatch(i: number): Promise<void> {if (i <= index) {throw new Error("next() called multiple times.");
}
index = i;
let fn: Middleware<S, T> | undefined = middleware[i];
if (i === middleware.length) {fn = next;}
if (!fn) {return;}
await fn(context, dispatch.bind(null, i + 1));
}
return dispatch(0);
};
}
看的进去与 koa 的简直截然不同。
简版 oak
上面,咱们写个简版的 oak/koa,实现上述的样例性能。
实现前,先看下 Deno 的 http 服务代码:
// Start listening on port 8080 of localhost.
const server = Deno.listen({port: 8080});
console.log(`HTTP webserver running. Access it at: http://localhost:8080/`);
// Connections to the server will be yielded up as an async iterable.
for await (const conn of server) {
// In order to not be blocking, we need to handle each connection individually
// without awaiting the function
serveHttp(conn);
}
async function serveHttp(conn: Deno.Conn) {
// This "upgrades" a network connection into an HTTP connection.
const httpConn = Deno.serveHttp(conn);
// Each request sent over the HTTP connection will be yielded as an async
// iterator from the HTTP connection.
for await (const requestEvent of httpConn) {
// The native HTTP server uses the web standard `Request` and `Response`
// objects.
const body = `Your user-agent is:\n\n${requestEvent.request.headers.get("user-agent",) ?? "Unknown"}`;
// The requestEvent's .respondWith() method is how we send the response back to the client.
requestEvent.respondWith(
new Response(body, {status: 200,}),
);
}
}
通过上述代码,就能启动一个 http://localhost:8080 的服务。
所以,咱们的代码也很简略:
class Application {middlewares: Middleware[] = [];
use(callback: Middleware) {this.middlewares.push(callback);
}
async listen(config: {port: number;}) {
const middlewares = this.middlewares;
const server = Deno.listen(config);
console.log(`HTTP webserver running. Access it at: http://localhost:${config.port}/`);
// Connections to the server will be yielded up as an async iterable.
for await (const conn of server) {
// In order to not be blocking, we need to handle each connection individually
// without awaiting the function
serveHttp(conn);
}
async function serveHttp(conn: Deno.Conn) {
// This "upgrades" a network connection into an HTTP connection.
const httpConn = Deno.serveHttp(conn);
// Each request sent over the HTTP connection will be yielded as an async
// iterator from the HTTP connection.
for await (const requestEvent of httpConn) {
const ctx: Context = {
request: requestEvent.request,
response: {
body: '',
status: 200,
headers: {_headers: {},
set(key: string, value: string | number) {(this._headers as any)[key] = value;
},
get(key: string) {return (this._headers as any)[key]
}
}
}
};
console.log(requestEvent.request.url);
await compose(middlewares)(ctx);
const body = ctx.response.body;
requestEvent.respondWith(
new Response(body, {
status: ctx.response.status,
headers: ctx.response.headers._headers
}),
);
}
}
}
}
这样,一个简略的应用中间件来解决音讯的性能就实现了。至于怎么实现路由,就交给大家了。
总结
本文介绍了 Node.js 的两大支流 web 框架 koa 与 Express 的区别和 koa 的中间件解决逻辑,能够看出 koa 的设计思维是十分精妙的。继而引出 Deno 与之类似的 oak 框架,旨在通过比照二者的应用差别,让大家对 Deno 有个简要的意识。下一篇将重点安利 Deno,带你从 Node 走进 Deno。
本文参考:
- 浅谈 Nodejs 框架里的“洋葱模型”
- 再也不怕面试官问你 express 和 koa 的区别了