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')

失常而言,咱们冀望返回后果程序是:

135wait 1000 ms end42

但事实上后果是:

1352wait 1000 ms end4

同样逻辑的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)});// responseapp.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-timeapp.use(async (ctx, next) => {    const start = Date.now();    await next();    const ms = Date.now() - start;    ctx.set('X-Response-Time', `${ms}ms`);});// responseapp.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的区别了