Node中间件的简析及应用

1,020 阅读18分钟

Node 中间件的简析及应用

前言

如果你有 express ,koaredux 的使用经验,就会发现他们都有 中间件(middlewares) 的概念,中间件是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作

先来个🌰

import express from 'express';
const app = express();

// Middleware 1
app.use((req, res, next) => {
  console.log('第一层 - 开始');
  next();
  console.log('第一层 - 结束');
});

// Middleware 2
app.use((req, res, next) => {
  console.log('第二层 - 开始');
  next();
  console.log('第二层 - 结束');
});

app.listen(3001, () => {
  console.log('server is running on port 3001');
});
import Koa from 'koa';
const app = new Koa();

// Middleware 1
app.use(async (ctx, next) => {
  console.log('第一层 - 开始');
  await next();
  console.log('第一层 - 结束');
});

// Middleware 2
app.use(async (ctx, next) => {
  console.log('第二层 - 开始');
  await next();
  console.log('第二层 - 结束');
});

app.listen(3002, () => {
	console.log('Koa app listening on 3002')
});

上面两段程序运行结果是否一样?打印内容是什么?

server is running on port 3001/3002
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束

两段程序同时加上和减去 async/await 时结果如何?

同上

两段程序在 async 内部增加异步操作时结果如何?

import express from 'express';
const app = express();

function delay(ms: number) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

app.use(async (req, res, next) => {
  console.log('第一层 - 开始');
  // 情况一 await delay(1000);
  await next();
  console.log('第一层 - 结束');
});

app.use(async (req, res, next) => {
  console.log('第二层 - 开始');
  // 情况二 await delay(1000);
  await next();
  console.log('第二层 - 结束');
});

app.listen(3001, () => {
  console.log('server is running on port 3001');
});

情况一运行结果

# Express 情况一
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束

情况二运行结果

# Express 情况二
第一层 - 开始
第二层 - 开始
第一层 - 结束
第二层 - 结束
import Koa from 'koa';
const app = new Koa();

function delay(ms: number) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

app.use(async (ctx, next) => {
  console.log('第一层 - 开始');
  // 情况一 await delay(1000);
  await next();
  console.log('第一层 - 结束');
});

app.use(async (ctx, next) => {
  console.log('第二层 - 开始');
  // 情况二 await delay(1000);
  await next();
  console.log('第二层 - 结束');
});

app.listen(3002, () => {
  console.log('server is running on port 3002');
});

情况一运行结果

# Koa 情况一
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束

情况二运行结果

# Koa 情况二
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束

Express 和 Koa 中间件模型

  1. express 中间件是一个接一个的顺序执行,一种线性的逻辑,在同一个线程上完成所有的 HTTP 请求。
    express 中我们是通过 res.send 来向客户端响应数据的(习惯于将响应写在最后一个中间件中),不能在多个中间件里面 res.send 。(res.send之后及时继续调用 next,也已经和返回给客户端的结果无关了)
    优点:线性逻辑,通过中间件形式把业务逻辑细分、简化,一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。
    缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。

  2. koa 洋葱模型
    koa 中我们是通过 ctx.body,可以在多个中间件里面修改它,所有中间件都执行完了之后,才会响应给客户端。
    优点:首先,借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。其次,koaexpress 中内置的 router、view 等功能都移除了,使得框架本身更轻量。
    缺点:相对于 express 社区较小,实现路由等更多功能需要配合其他插件

实际上,express 的中间件也可以形成“洋葱圈”模型,在 next 调用后写的代码同样会执行到,不过express中一般不会这么做,因为 expressresponse一般在最后一个中间件,那么其它中间件 next 后的代码已经影响不到最终响应结果了。

// Koa 实现(放在中间件的那个位置?)
async function responseTime(ctx, next) {
  const started = Date.now();
  await next();
  const ellapsedTime = (Date.now() - started) + 'ms';
  ctx.set('X-ResponseTime', ellapsedTime);
}

app.use(responseTime);

Express 中怎么实现?

eg. 把开始时间挂载到 req 上,在中间件的最后一个计算时间间隔并返回结果(一个中间件实现不了)…

如果只是后端记录耗时,可以通过 Express 的事件机制,监听 finish 和 close 事件

function responseTime() {
  return function (req: Request & any, res: Response & any, next: NextFunction) {
    req._startTime = new Date().valueOf(); // 获取时间 t1

    const callResponseTime = function () {
      const now = new Date().valueOf(); // 获取时间 t2
      const ellapsedTime = now - req._startTime;
      console.log('finish time', ellapsedTime);
			// ...
    };

    res.once('finish', callResponseTime);
    res.once('close', callResponseTime);
    return next();
  };
}

// 不需要发送给请求响应时可以用一个中间件实现
app.use(responseTime());

Express 和 Koa 错误处理

Express 错误处理

在 Express 里面是使用的一个接收四个参数的中间件来处理的。这个中间件作为是最后一个中间件,其他中间件运行的时候,如果报错,将错误使用 next 传递给错误出来,上边这个中间件就能捕获到,进行处理。

猜一下下面同步和异步的错误捕获情况?

// 1. 同步
app.use((req, res, next) => {
  const a = c; // c 没有定义
  next();
});

// 2. 异步
app.use((req, res, next) => {
  try {
    setTimeout(() => {
      const a = c; // c 没有定义
      next();
    }, 0);
  } catch(e) {
    console.log('异步错误,能 catch 到么??')
  } 
});

// 3. 异步 + async/await
app.use(async (req, res, next) => {
  try {
    await (() => new Promise((resolve, reject) => {
      http.get('http://www.example.com//123', res => {
        reject('假设错误了');
				next();
      }).on('error', (e) => {
        throw new Error(e);
      })
    }))();
  } catch(e) {
    console.log('异步错误,能 catch 到么??', e)
  }  
});

// Express 错误捕获
app.use((err, req, res, next) => {
  const errLog = `
    +---------------+-------------------------+
    错误名称:${err.name},\n
    错误信息:${err.message},\n
    错误时间:${new Date()},\n
    错误堆栈:${err.stack},\n
    +---------------+-------------------------+
  `;
  fs.appendFile(path.join(__dirname, "error.log"), errLog, ()=>{
    res.writeHead(500, {'Content-Type': 'text/html;charset=utf-8'});
    res.end(`500 服务器内部错误`);
  });
});

// Node 错误捕获
process.on("uncaughtException", (err) => {
  console.log("uncaughtException message is::", err);
})

结论及分析

  • 同步的时候,不会触发 uncaughtException,而进入了错误处理的中间件。
    同步逻辑错误获取的底层逻辑: Express 内部对同步发生的错误进行了拦截,所以不会传到负责兜底的 node 事件 uncaughtException,如果发生了错误,则直接绕过其它中间件,进入错误处理中间件,即使没有错误处理中间件做兜底,也不会进入 uncaughtException,会直报 500错误。

  • 异步的时候,不会触发错误处理中间件, 而会触发 uncaughtException
    异步逻辑错误获取的底层逻辑:还是因为 Express 的中间件执行是同步顺序执行的。 所以如果有异步的,那么错误处理中间件实际是兜不住的,所以,Express 对这种中间件中的异步处理错误无能为力。
    除了错误处理中间件没有触发,我们当中的try catch 也没有触发

  • 异步 +  async await,我们不仅 try catch 可以获取到,uncaughtException 也可以获取到。
    async/await 是使用生成器、promise 和协程实现的, **wait 操作符还存储返回事件循环之前的执行上下文**,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。
    所以说,async await能够回到最外层的上下文, 那就可以用 try catch 了。
    await/async 是 Generator 语法糖

Koa 错误处理

上面提过洋葱模型,特点是最开始的中间件,在最后才执行完毕,所以,在 Koa 中,可以把错误处理中间件放到中间件逻辑最前面

const catchError = async(ctx, next) => {
  try{
    await next();
  } catch(error) {
		ctx.status = 400
    ctx.body = `Uh-oh: ${err.message}`
    console.log('Error handler:', err.message)
  }
}

app.use(catchError)

Express 和 Koa 的实现源码简析

Express 的实现分析

// express.js
var proto = require('./application');
var mixin = require('merge-descriptors');

exports = module.exports = createApplication;

function createApplication() {
  // app 同时是一个方法,作为 http.createServer 的处理函数
  var app = function(req, res, next) { 
    app.handle(req, res, next)
  }
  
  mixin(app, proto, false);
  return app
}

就是一个 createApplication方法用于创建 express实例,要注意返回值 app既是实例对象,上面挂载了很多方法,同时它本身也是一个方法,作为 http.createServer的处理函数。

// application.js
var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}

app.listen = function listen() {
  var server = http.createServer(this)
  return server.listen.apply(server, arguments)
}

app.listen 调用 http 模块创建一个服务服务,可以看到这里 var server = http.createServer(this) 其中 thisapp 本身,然后真正的处理程序是 app.handle

中间件处理

express 本质上就是一个中间件管理器,当进入到 app.handle 的时候就是对中间件进行执行的时候。

两个函数:app.use 和 app.handle。

  • app.use 添加中间件
app.use = function(fn) {
	this.stack.push(fn)
}

app.use全局维护一个stack数组用来存储所有中间件。
express 的真正实现当然不会这么简单,它内置实现了路由功能(router),app.use 方法实际调用的是 router 实例的 use 方法。

  • app.handle 尾递归调用中间件处理 req 和 res
// router/index.js
app.handle = function(req, res, callback) {
	var stack = this.stack;
	var idx = 0;

	function next(err) {
		if (idx >= stack.length) {
		  callback('err') 
		  return;
		}
		var mid;
		while(idx < stack.length) {
		  layer = stack[idx++];
			route = layer.route;
			// 递归调用执行
		  layer.handle_request(req, res, next)
		}
	}

	next();
}

// router/layer.js
Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

“尾递归调用”next 方法不断的取出stack中的中间件函数进行调用,同时把next 本身传递给中间件作为第三个参数,每个中间件约定的固定形式为 (req, res, next) => {},这样每个中间件函数中只要调用 next 方法即可传递调用下一个中间件。
之所以说是”尾递归“是因为递归函数的最后一条语句是调用函数本身,”尾递归“相对于普通”递归“的好处在于节省内存空间,不会形成深度嵌套的函数调用栈。原理分析可以阅读下阮一峰老师的尾调用优化

尾调用节省内存的原因归纳

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

Koa 的实现分析

相比较 express而言,koa的整体设计和代码实现显得更高级、更精炼。代码基于ES6实现,支持generator(async await), 没有内置的路由实现和任何内置中间件,context的设计也很是巧妙。

一共只有4个文件:

  • application.js 入口文件,koa 应用实例的类;
  • context.js ctx 实例,代理了很多requestresponse的属性和方法,作为全局对象传递;
  • request.js koa 对原生 req 对象的封装;
  • response.js koa 对原生 res 对象的封装。

中间件机制是 Koa 的核心

Koa 的中间件使用很简单:使用 app.use 去给 Koa 注册一个中间件即可。

use方法就是做了一件事,维护得到 middleware中间件数组

// application.js
const http = require('http');
const Emitter = require('events');
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')

module.exports = class Koa extends Emitter {
  constructor() {
    super();
    this.middleware = []
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }

  use(fn) {
    this.middleware.push(fn);
    return this;
  }

  listen(...arg) {
		// 同样的在 listen 方法中创建 web 服务
    const server = http.createServer(this.callback);
    return server.listen(...arg);
  }

  callback() {
		// 调用 koa-compose 核心方法
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

  handleRequest(ctx, fnMiddleware) {
    const onerror = err => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
  }
};

koa-compose 函数

将中间件函数队列组合成一个函数并且可以异步顺序执行

compose 1.0 版本:在没有 async 的情况下,compose的实现其实相当复杂,利用了generator + co来进行异步管理。

compose 2.0 版本:基于 Promise 实现

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

  return function(context, next) {
		// 这里 next 指的是洋葱模型的中心函数
    // context 是一个配置对象,保存着一些配置,也可以利用 context 将一些参数往下一个中间传递

    // last called middleware #
    let index = -1 // 记录执行的中间件的索引

		// 取出第一个中间件函数执行,然后通过第一个中间件递归调用下一个中间件
    return dispatch(0)
		
    function dispatch(i) {
			// 这里是保证同个中间件中一个 next 不被调用多次调用 
			// 第一次 i 为 0,index 为 -1,可以继续走下去,index 被赋值为 0
      // 当 next 函数被调用两次的时候,i 会小于 index,然后抛出错误
			// eg. dispatch(0) => dispatch(1) + dispatch(1),执行第二个 dispatch(1) 时 index 为 1,1 <= 1 成立
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
			// 取出数组里的 fn1, fn2, fn3...
      let fn = middleware[i]

			// 当 middleware 为空或者到了洋葱模型的中心(最后一个中间件)时将 next 赋值给 fn
      if (i === middleware.length) fn = next 
			// 如果中间件为空,即直接 resolve     
      if (!fn) return Promise.resolve()

      try {
        // bind 函数是返回一个新的函数。 
				// i + 1 是为了 let fn = middleware[i] 取 middleware 中的下一个函数。
				// 也就是 next 是下一个中间件里的函数。
				// 所以使用中间件时需要「await next()」 
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

简化成下面这样就可能更好理解了:

Promise.resolve( // 第一个中间件
  function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(1)
    // await next 上的代码 (中间件1)
    Promise.resolve( // 第二个中间件
      function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(2)
        // await next 上的代码 (中间件2)
        Promise.resolve( // 第三个中间件
          function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(3)
            // await next 上的代码 (中间件3)
            Promise.resolve();
            // await next 下的代码 (中间件3)
          }
        );
        // await next 下的代码 (中间件2)
      }
    );
    // await next 下的代码 (中间件2)
  }
);

express中的next 很类似,只不过他是 promise 形式的,因为要支持异步:每个中间件是一个**async (ctx, next) => {}**(Express 为**(req, res, next) => {}**)。 执行后返回的是一个promise,第二个参数 next的值为 dispatch.bind(null, i + 1) ,用于传递中间件的执行,一个个中间件向里执行,直到最后一个中间件执行完,resolve 掉,它前一个中间件接着执行 await next() 后的代码,然后 resolve 掉,在不断向前直到第一个中间件 resolve掉,最终使得最外层的promise resolve掉。

这里和 express 很不同的一点就是 koa 的响应的处理并不在中间件中,而是在中间件执行完返回的 promise resolve 后:return fnMiddleware(ctx).then(handleResponse).catch(onerror),通过 handleResponse 最后对响应做处理,中间件会设置 ctx.body,handleResponse 也会主要处理 ctx.body ,所以 koa 的”洋葱圈“模型才会成立,await next() 后的代码也会影响到最后的响应。respond 源码如下:

// const handleResponse = () => **respond**(ctx)
// return fnMiddleware(ctx).then(handleResponse).catch(...)
function respond (ctx) {
	const res = ctx.res
  let body = ctx.body
  const code = ctx.status

  // status body
  if (body == null) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code)
    } else {
      body = ctx.message || String(code)
    }
    return res.end(body)
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body)
  if (typeof body === 'string') return res.end(body)
  if (body instanceof Stream) return body.pipe(res)

  // body: json
  body = JSON.stringify(body)
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body)
  }
  res.end(body)
}

koa: 中间件执行完后通过 handleResponse(respond) 中的 ctx.res.end(…) 返回相应结果。

express:  每个中间件都可以拿到  res 执行 res.end(…) 返回相应结果。

Redux 中的 middleware

Redux 的基本流程:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。

Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。

怎么才能 Reducer 在异步操作结束后自动执行呢(异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样;异步完成后,触发第二个 Action 去改变 store )?

这就要用到新的工具:中间件(middleware)

Redux middleware 解决的问题与 Express 或 Koa middleware 不同,但在概念上是相似的。它在 dispatch action 的时候和 action 到达 reducer 那一刻之间提供了三方的逻辑拓展点( 主要增强 store.dispatch 的能力 。可以使用 Redux middleware 进行日志记录、故障监控上报、与异步 API 通信、路由等。

以添加日志功能举例,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造:

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action);
  console.log('next state', store.getState());
}

从上图中得出结论,middleware 通过 next(action) 一层层处理和传递 action 直到 redux 原生的 dispatch。而如果某个 middleware 使用 store.dispatch(action) 来分发 action,就相当于重新来一遍。

在 middleware 中使用 dispatch 的场景一般是接受一个定向 action,这个 action 并不希望到达原生的分发 action,往往用在一步请求的需求里,如 redux-thunk,就是直接接受 dispatch。

如果一直简单粗暴调用 store.dispatch(action),就会形成无限循环。

中间件执行顺序

应用了如下的中间件: [A, B, C],

整个执行 action 的过程为 A -> B -> C -> dispatch -> C -> B -> A

const mid1 = ({getState, dispatch}) => next => action => {
  console.log(`middleware1 before next action `)
  next(action)
  console.log(`middleware1 after next action `)
}
const mid2 = ({getState, dispatch}) => next => action => {
  console.log(`middleware2 before next action `)
  next(action)
  console.log(`middleware2 after next action `)
}
const mid3 = ({getState, dispatch}) => next => action => {
  console.log(`middleware3 before next action `)
  next(action)
  console.log(`middleware3 after next action `)
}

const sagaMiddleware = createSagaMiddleware()
// 实际的执行是次序是 store.dispatch -> Mid3 -> Mid2 -> Mid1
const store = createStore(reducer, applyMiddleware(mid1, mid2, mid3))

中间件实现原理:compose

export default function compose(...funcs) {
  // 没传任何参数,则默认返回一个空的函数
  if (funcs.length === 0) {
    return (arg) => arg
  }
  // 只传一个那就直接返回这一个咯
  if (funcs.length === 1) {
    return funcs[0]
  }
  // 将所有函数组合,返回一个函数
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

eg. compose(f, g, h) 返回 () => f(g(h(..args)))

现在再理解增强 store.dispatch 的能力:

dispatch = compose(...chain)(store.dispatch) ⇒ dispatch = f1(f2(f3(store.dispatch))))

原生的 store.dispatch传入最后一个中间件,返回一个新的 dispatch,再向外传递到前一个中间件,直至返回最终的 dispatch,当覆写后的dispatch 调用时,每个中间件的执行又是从外向内的”洋葱圈“模型。

Nestjs 中的“洋葱模型”

在 Nestjs 中在 service 的基础上,按处理的层次补充了中间件(middleware)、异常处理(Exception filters)、管道(Pipes)、守卫(Guards),以及拦截器(interceptors)在请求到打真正的处理函数之间进行了层层的处理。

  • Pipes 一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。
  • Guards 守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在 middleware 中间件中来做请求的接受与否的处理,与 middleware 相比,Guards 可以获得更加详细的关于请求的执行上下文信息。
    通常 Guards 守卫层,位于 middleware 之后,请求正式被处理函数处理之前。
  • Interceptors 拦截器可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。
    I
    nterceptors 拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。**
  • Exception filters 异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。
    我们可以自定义一个异常过滤器,并且在这个异常过滤器中可以指定需要捕获哪些异常,并且对于这些异常应该返回什么结果等,举例一个自定义过滤器用于捕获 HttpException 异常的例子。

在 Nestjs 中的中间件完全跟 Express 的中间件一摸一样。不仅如此,我们还可以直接使用 Express 中的中间件,比如在我的应用中需要处理 cors 跨域:

import * as cors from 'cors';

async function bootstrap() {
  const app = await NestFactory.create(/* 创建app的业务逻辑*/);

  app.use(cors({
    origin:'http://localhost:8080',
    credentials:true
  }));

  await app.listen(3000)
}

bootstrap();

初此之外,跟 Nestjs 的中间件也完全保留了 Express 中的中间件的特点:

  • 在中间件中接受 response 和 request 作为参数,并且可以修改请求对象 request 和结果返回对象 response。
  • 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接 res.send 等。
  • 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过 next 方法,将中间件传递给下一个中间件处理。

在 Nestjs 中,中间件跟 Express 中完全一样,除了可以复用 Express 中间件外,在 Nestjs 中针对某一个特定的路由来使用中间件也十分的方便:

class ApplicationModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

上面就是对于特定的路由 url 为 /cats 的时候,使用 LoggerMiddleware 中间件。

问答

  1. Express 和 Koa 的中间件处理请求时,都可以在 next 方法前面或者后面异步操作?
import express from 'express';
const app = express();

app.use(async (req, res, next) => {
  console.log('第一层 - 开始');
  await next();
  console.log('第一层 - 结束');
});

app.use(async (req, res, next) => {
  console.log('第二层 - 开始');
  await delay(1000); // 异步操作
  await next();
  console.log('第二层 - 结束');
});

app.listen(3001, () => {
  console.log('server is running on port 3001');
});
import Koa from 'koa';
const app = new Koa();

app.use(async (ctx, next) => {
  console.log('第一层 - 开始');
  await next();
  console.log('第一层 - 结束');
});

app.use(async (ctx, next) => {
  console.log('第二层 - 开始');
  await delay(1000); // 异步操作
  await next();
  console.log('第二层 - 结束');
});

app.listen(3002, () => {
	console.log('Koa app listening on 3002')
});

Koa 可以,Express 不可以

  1. 下面的错误能被 catch 到吗?
try{
  setTimeout(() => {
    const a = c; // c 未定义
  }, 100)
} catch(e) {
  console.log('能获取到错误么', e);
}

不能
最外层的 try catch 是在一个task中,我们定义它为我们 js 文件的同步主任务,从上到下执行到这里了,然后会把里面的 setTimeout 推到一个任务队列中, 这个队列是存储在内存中的,由V8来管理。
然后主 task 就继续向下执行,一直到结束。当该 setTimeout 时间到了,且没有其它的 task 执行了, 那么,就将这个 setTimeout 的代码推入执行栈开始执行。 当执行到错误代码的时候,也就是这个 const a = c,因为 c 未定义,所以就会报错。
但问题的本质是,这个错误跟最外层的 try catch 并不在一个执行栈中,当里面执行的时候,外边的这个 task 早已执行完, 他们的上下文已经完全不同了。 所以,会直接报错,甚至程序崩溃。
不能
原因其实与上面的 setTimeout 是一样的,执行栈上下文已经不同了。

async/await 是使用生成器、promise 和协程实现的, wait 操作符还存储返回事件循环之前的执行上下文,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。
所以说,能够回到最外层的上下文, 那就可以用 try catch 了。

try {
  new Promise((resolve, reject) => {
    reject('promise error');
  })
} catch(e) {
  console.log('异步错误,能 catch 到么', e);
}

不能

原因其实与上面的 setTimeout 是一样的,执行栈上下文已经不同了。

const asyncFn = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('asyncFn执行时出现错误了')
    }, 100);
  })
}

const executedFn = async () => {
  try {
    await asyncFn();
  } catch(e) {
    console.log('异步错误,能 catch 到么', e);
  }
}

async/await 是使用生成器、promise 和协程实现的, wait 操作符还存储返回事件循环之前的执行上下文,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。

所以说,能够回到最外层的上下文, 那就可以用 try catch 了。