解析express, koa的中间件实现原理,顺便拓展axios和redux

Express

有关于Express,官方是这样说的:

Express 是一个路由和中间件 Web 框架,其自身只具有最低程度的功能: Express 应用程序基本上是一系列中间件函数调用。 中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示。

中间件函数可以执行以下任务:

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求/响应循环。
  • 调用堆栈中的下一个中间件函数。
  • 如果当前中间件函数没有结束请求/响应循环,那么它必须调用 next(),以将控制权传递 - 给下一个中间件函数。否则,请求将保持挂起状态。

Express 应用程序可以使用以下类型的中间件:

  • 应用层中间件
  • 路由器层中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

1. 应用层中间件

使用 app.use() 和 app.METHOD(HTTP方法: get|put|post) 函数将应用层中间件绑定到应用程序对象的实例。

要跳过路由器中间件堆栈中剩余的中间件函数,请调用 next('route') 将控制权传递给下一个路由。 注:next('route') 仅在使用 app.METHOD() 或 router.METHOD() 函数装入的中间件函数中有效

此示例显示一个中间件子堆栈,用于处理针对 /user/:id 路径的 GET 请求。

app.get('/user/:id', function (req, res, next) {
  // if the user ID is 0, skip to the next route
  if (req.params.id == 0) next('route');
  // otherwise pass the control to the next middleware function in this stack
  else next(); //
}, function (req, res, next) {
  // render a regular page
  res.render('regular');
});

// handler for the /user/:id path, which renders a special page
app.get('/user/:id', function (req, res, next) {
  res.render('special');
});

2. 路由器层中间件

路由器层中间件的工作方式与应用层中间件基本相同,差异之处在于它绑定到 express.Router() 的实例。

var router = express.Router();

使用 router.use() 和 router.METHOD() 函数装入路由器层中间件。 以下示例代码使用路由器层中间件复制以上为应用层中间件显示的中间件系统:

var app = express();
var router = express.Router();

// a middleware function with no mount path. This code is executed for every request to the router
router.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path
router.use('/user/:id', function(req, res, next) {
  console.log('Request URL:', req.originalUrl);
  next();
}, function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

// a middleware sub-stack that handles GET requests to the /user/:id path
router.get('/user/:id', function (req, res, next) {
  // if the user ID is 0, skip to the next router
  if (req.params.id == 0) next('route');
  // otherwise pass control to the next middleware function in this stack
  else next(); //
}, function (req, res, next) {
  // render a regular page
  res.render('regular');
});

// handler for the /user/:id path, which renders a special page
router.get('/user/:id', function (req, res, next) {
  console.log(req.params.id);
  res.render('special');
});

// mount the router on the app
app.use('/', router);

3. 错误处理中间件

错误处理中间件函数的定义方式与其他中间件函数基本相同,差别在于错误处理函数有四个自变量而不是三个,专门具有特征符 (err, req, res, next):

错误处理中间件始终采用四个自变量。 必须提供四个自变量,以将函数标识为错误处理中间件函数。 即使无需使用 next 对象,也必须指定该对象以保持特征符的有效性。 否则,next 对象将被解释为常规中间件,从而无法处理错误。

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

4. 内置中间件

自 V4.x 起,Express 不再依赖于 Connect。除 express.static 外,先前 Express 随附的所有中间件函数现在以单独模块的形式提供。请查看中间件函数的列表。

Express 中唯一内置的中间件函数是 express.static。此函数基于 serve-static,负责提供 Express 应用程序的静态资源。

express.static(root, [options]) root 自变量指定从其中提供静态资源的根目录。 options(可选) 可具有以下属性

属性 描述 类型 缺省值
dotfiles 是否对外输出文件名以点(.)开头的文件。有效值包括“allow”、“deny”和“ignore” 字符串 “ignore”
etag 启用或禁用 etag 生成 布尔 true
extensions 用于设置后备文件扩展名。 数组 []
index 发送目录索引文件。设置为 false 可禁用建立目录索引。 混合 “index.html”
lastModified 将 Last-Modified 的头设置为操作系统上该文件的上次修改日期。有效值包括 true 或 false。 布尔 true
maxAge 设置 Cache-Control 头的 max-age 属性(以毫秒或者 ms 格式中的字符串为单位) 数字 0
redirect 当路径名是目录时重定向到结尾的“/”。 布尔 true
setHeaders 用于设置随文件一起提供的 HTTP 头的函数。 函数 -

5. 第三方中间件

使用第三方中间件向 Express 应用程序添加功能。

安装具有所需功能的 Node.js 模块,然后在应用层或路由器层的应用程序中将其加装入。

以下是常用的第三方中间件:

以下示例演示如何安装和装入 cookie 解析中间件函数 cookie-parser。

var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');

// load the cookie-parsing middleware
app.use(cookieParser());

终:实现原理

express注册的中间件最后会被处理成一层一层的回调函数。 首先,在express中,有一个layer对象用于包装中间件

// express/lib/router/layer.js 版本4.17.1
module.exports = Layer;

function Layer(path, options, fn) {
  // ...
  // layer的handle方法即为注册的中间件
  this.handle = fn;
  // ...
}

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

再来看一下express负责路由相关代码中use方法的实现## 二级标题middlewareA(ctx, () => middlewareB(ctx, () => middlewareC(ctx, ...)))

// express/lib/router/index.js
var Layer = require('./layer');
// ...
var proto = module.exports = function(options) {
  // ...
  function router(req, res, next) {
    // ...
  }
  // ...
  // 存放layer对象的数组
  router.stack = [];
  return router;
};

// 注册中间件
proto.use = function use(fn) {
  // ...
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: false,
    end: false
  }, fn);
  // ...
  this.stack.push(layer);
}

// 当有请求命中改路由时实际执行的方法
proto.handle = function handle(req, res, out) {
  var self = this;
  var idx = 0;
  var stack = self.stack;
  // ...
  function next(err) {
    while (idx < stack.length) {
      // 取出中间件,并将中间件数组索引加1
      var layer = stack[idx++];
      // 执行中间件
      layer.handle_request(req, res, next)
    }
  }
  next()
}

express中间件的注册可以通过app.use(fn)进行全局注册,也可以通过app.use(path, fn)或者app.method(path, fn)的方式进行局部注册,但最终都会走到router对象的use方法。 从上面的代码可以看出,在编写express中间件时的第三个参数next实际上是通过包装的下一个中间件。由于在包装函数内直接调用下一个中间件,没有针对异步的处理逻辑,且包装函数本身是一个普通的同步函数,自然无法支持用async/await等方式处理处理异步,这就是express中间件不支持异步的根本原因

koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。 Koa 类似于你可能遇到过的许多其他中间件系统,例如 Ruby 的 Rack ,Connect 等,然而,一个关键的设计点是在其低级中间件层中提供高级“语法糖”。 这提高了互操作性,稳健性,并使书写中间件更加愉快。

这包括诸如内容协商,缓存清理,代理支持和重定向等常见任务的方法。 尽管提供了相当多的有用的方法 Koa 仍保持了一个很小的体积,因为没有捆绑中间件。

理解koa的中间件有一个非常经典的洋葱圈模型 image.png

koa的中间件最后会组成嵌套的高阶函数,类似于

middlewareA(ctx, () => middlewareB(ctx, () => middlewareC(ctx, ...)))

实现方式

// koa/lib/application.js
const Emitter = require('events');
const compose = require('koa-compose');
// ...

module.exports = class Application extends Emitter {
  constructor(options) {
    // ...
    // 存放中间件的数组
    this.middleware = [];
  }
  // ...
  // 将中间件函数传给use方法
  use(fn) {
  // ...
    this.middleware.push(fn);
    return this;
  }

  callback() {
    // 处理中间件
    const fn = compose(this.middleware);
    // ...
    const handleRequest = (req, res) => {
      // 创建ctx对象
      const ctx = this.createContext(req, res);
      // 将compose函数的返回传递给handleRequest
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  // fnMiddleware即是通过compose函数处理的返回结果
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  listen(...args) {
    debug('listen');
    // 当http服务创建成功后调用回调函数
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

koa在内部通过koa-compose处理,再来看看koa-compose做了什么

// koa-compose/index.js 版本4.1.0

module.exports = 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
   */

  // 在koa handleRequest中调用这个返回的函数时,next为空
  return function (context, next) {
    // 记录当前middleware的下标
    let index = -1
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 取出中间件
      let fn = middleware[i]
      // 下面两个if语句保证最后一个中间件调用next不会报错
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      
      try {
        return Promise.resolve(
          fn(
            context,
            // 通过bind方法返回一个函数,实际上就是下一个中间件
            // 也就是写中间件时的第二个参数、通常取名为next
            // 只要没有取到middleware数组的最后一个元素,dispatch都会递归下去
            dispatch.bind(null, i + 1)
          )
        );
      } catch (err) {
        return Promise.reject(err)
      }
    }
    // 开始执行第一个中间件
    return dispatch(0)
  }
}

值得注意的是,koa注册的中间件最后都会被promise.resolve包装一层从而被转换为promsie对象。进而可以使用 await next() 的写法等待下一个中间件执行完毕。 编写中间件时,第二个参数 next 实际上就是下一个中间件,如果已经是最后一个中间件,next执行返回Promise.resolve(),依然可以正常调用。

axios拦截器

1. 注册

const axios = require('axios')

axios.interceptors.request.use((config) => {
  console.log('请求拦截器')
  // 在发送请求前处理配置,并返回处理后的配置
  return config
}, (error) => {
  // 发生错误时
  return Promise.reject(error)
})

axios.interceptors.response.use((response) => {
  console.log('响应拦截器')
  // 对响应数据做处理,并返回处理后的数据
  return response
}, (error) => {
  // 发生错误时
  return Promise.reject(error)
})

2. 实现原理

axios.interceptors.request/response.use方法接受两个函数类型参数,分别处理正常情况和错误情况。axios 把这两个函数作为 promise.then 的参数,在运行时把所有的拦截器组合成一个promise调用链依次执行 axios发送请求整体的流程是:

1、config配置经过请求拦截器处理 2、发送请求 3、获取响应response 4、response通过响应拦截器处理 5、将结果返回用户

// axios/lib/core/Axios.js
var InterceptorManager = require('./InterceptorManager');

//...

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 这里即是请求和响应的拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
// axios/lib/core/InterceptorManager.js
function InterceptorManager() {
  // 放置拦截器们的数组
  this.handlers = [];
}

// ...

// 这里即是注册中间件所用的use方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// ...

// 在拦截器的原型上定义forEach方法,之后会用到
// 这里可以简单理解为提供一个方法遍历this.handlers
InterceptorManager.prototype.forEach = function forEach(fn) {
  // ...
};
// axios/lib/core/Axios.js
// ...

// dispatchRequest即为axios真正执行发送请求的方法
var dispatchRequest = require('./dispatchRequest');

// ...

// 定义axios发送请求的方法
Axios.prototype.request = function request(config) {
  // ...
	
  // 一开始chain就有两个方法,对应promise.then的两个参数
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 通过unshift把请求拦截器插到数组头部
  // 所以请求拦截器实际执行顺序和注册顺序相反
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 把响应拦截器插到数组尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    // chain在插入时总是处理正常流程和错误流程的函数成对的插入,所以这里连续使用shift
    // 通过while循环构造promise.then的调用链
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
}

同上面代码可以看出,所有的拦截器最后都被放在一个长长的promise.then调用链中执行,上一个拦截器处理完的配置/数据会传递给下一个promise.then,这种机制也支持在拦截器中使用async/await

redux

注册

import {
  createStore,
  applyMiddleware,
} from 'redux'

function reducer(state, action) {
  let newState = { ...state }

  // ...

  return newState
}

export function logger({ getState, dispatch }) {
  // next 代表下一个中间件包装过后的 dispatch 方法,action 表示当前接收到的动作
  return (next) => async (action) => {
    console.log('logger before change', action)
    // 调用下一个中间件包装的 dispatch
    let val = await next(action)
    console.log('logger after change', getState(), val)
    return val
  }
}

export function debug({ getState, dispatch}) {
  return (next) => async (action) => {
    console.log('debug before change', action)
    let val = await next(action)
    console.log('debug after change', getState(), val)
    return val
  }
}

const initialState = {
  // ...
}

export default createStore(
  reducer,
  initialState,
  applyMiddleware(logger, debug),
)

实现原理

redux中间件的处理逻辑类似于koa的洋葱圈模型,其中包含了各种高阶函数和各种柯里化,有一点不好理解,我们可以先尝试理解这样一种函数:它是一种高阶聚合函数,接受一个函数数组为参数,将后加入数组的函数的执行结果作为参数传递给先加入数组的函数

function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

// 简单理解
// compose(fn1, fn2, fn3)(...args) = > fn1(fn2(fn3(...args)))

// 示例
const a = []
a.push(function fn1 (val) { return val })
a.push(function fn2 (val) { return val * 2 })
a.push(function fn3 (val) { return val * 3 })

var x = compose(...a)
x(2) // => a 12

如果能理解compose函数,那接着往下看,我们通过 compose(...a) 生成函数x,即(...a) = > fn1(fn2(fn3(...a))),当函数x执行时,fn3、fn2、fn1依次执行,这和三个函数添加到数组中的顺序相反,如果applyMiddleware方法按照参数顺序将中间件填入数组中,那实际执行时越靠后的中间件反而会先执行,这和实际情况不符,而且compose函数也无法处理异步中间件。 这时需要先留意一下redux中间件的编写方式

function logger({ getState, dispatch }) {
  return (next) => async (action) => {
    console.log('logger before change', action)
    // 调用下一个中间件包装的 dispatch
    let val = await next(action)
    console.log('logger after change', getState(), val)
    return val
  }
}

中间件函数第一次执行,返回一个函数A函数A执行,返回一个函数B函数B的函数体才是中间件真正的代码。好家伙,这和套娃有什么区别… 再来看applyMiddleware的逻辑

// redux/src/compose.js 版本4.0.5
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)))
}

// redux/src/applyMiddleware.js 版本4.0.5
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    // 通过这一步数组chain里的元素就是中间件函数中 (next) => async (action) => { ... } 部分
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
	
	// todo即形为(...args) => a(b(c(...args)))的聚合函数
	// b(c(...args))就是a的next参数,c(...args)就是b的next参数,...args就是c的next参数
	const const todo = compose(...chain)
	
	// (...args) => a(b(c(...args)))执行时,函数c/b/a从后往前执行
	// c/b/a执行结果就是中间件函数中async (action) => { .. } 部分
	// store.diapatch将会成为最后一个中间件的next参数
	const dispatch = todo(store.dispatch)

	// 如果这里打印dispatch,会发现它就是传递给applyMiddleware的第一个中间件
	// 它的next参数就是下一个中间件

    return {
      ...store,
      dispatch
    }
  }
}

协助理解

如果上面的解析看不太懂的话,可以尝试运行下面的例子帮助理解

function a() {
  return (next) => {
    return (action) => {
      console.log('a before', action)
      const res = next(action)
      console.log('a after', res)
    }
  }
}

function b() {
  return (next) => {
    return (action) => {
      console.log('b before', action)
      const res = next(action)
      console.log('b after', res)
      return res
    }
  }
}

function c() {
  return (next) => {
    return (action) => {
      console.log('c before', action)
      const res = next(action)
      console.log('c after', res)
      return res
    }
  }
}

function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

function applyMiddleware(...funcs) {
  const middlewares = new Array(funcs.length)
  funcs.forEach((func, index) => {
    middlewares[index] = func()
  })
  return middlewares
}

const chain = applyMiddleware(a, b, c)
console.log(chain)

const todo = compose(...chain)
console.log(todo)

const dispatch = todo((action) => action)
console.log(dispatch)

dispatch({
  type: 'SET_LOG',
})

// 输出结果
// a before {type: "SET_LOG"}
// b before {type: "SET_LOG"}
// c before {type: "SET_LOG"}
// c after {type: "SET_LOG"}
// b after {type: "SET_LOG"}
// a after {type: "SET_LOG"}

参考连接:axios拦截器/koa中间件/express中间件/redux中间件的原理 参考连接:图解Redux中middleware的洋葱模型

WRITTEN BY

lidong

鄂ICP备20003892号Copyright © 2017-2023 leedong.cn

ABOUT ME

Hello,这里是「我的心情永不立冬」
一个想到什么就做什么的个人站点,所有内容纯主观、有偏见