中间件与 Redux 与 Koa

compose 函数

/**
 * 接受一组函数,返回一个函数,该函数从右向左执行传入的函数,函数执行的结果作为下一个函数的参数,返回最后一个函数执行结果
 * reduce的数组如果空数组,且reduce参数没有初始值,会报错
 * reduce的数组如果只有一个元素,且reduce参数没有初始值,会直接返回数组中的唯一元素
 * @param  {...any} fns 一组函数
 */
const compose = (...fns) =>
  fns.reduce(
    (prev, cur) =>
      (...x) =>
        prev(cur(...x))
  )

简单的中间件实现

  • compose 的不足:只能内层执行完再执行外层,无法实现洋葱模型似的中间件

  • 中间件接受一个 next 函数,返回一个函数 A。

    1. A 可在 next 执行前做一些处理(修改参数等)

    2. A 可在 next 执行后做一些处理(修改结果等)

  • applyMiddleware 接收初始函数,和中间件数组,通过类似 compose 的方式,返回一个加强后的函数

const middleware_1 =
  next =>
  (...a) => {
    console.log('middleware_1 start', ...a)
    const res = next(...a)
    console.log('middleware_1 end', res)
    return res
  }

const middleware_2 =
  next =>
  (...a) => {
    console.log('middleware_2 start', ...a)
    const res = next(...a)
    console.log('middleware_2 end', res)
    return res
  }

const doSth = (...args) => {
  console.log('doSth', ...args)
  return args
}

// 执行applyMiddleware时,立即传入基础函数
const applyMiddleware = (func, middlewares) =>
  middlewares.reduce((prev, cur) => cur(prev), func)

// 延迟传入基础函数
const applyMiddleware_2 = middlewares =>
  middlewares.reduce((prev, cur) => func => cur(prev(func)))

const last = applyMiddleware(doSth, [middleware_1, middleware_2])
const last_2 = applyMiddleware_2([middleware_1, middleware_2])(doSth)

console.log('last end', last('params'))
console.log('last_2 end', last_2('params_2'))

redux 中的中间件

  • 从下面代码看到,redux 的中间件还要多包一层(传入{getState,dispatch}),通过 const chain = middlewares.map(middleware => middleware(middlewareAPI))转化成我们上面的中间件形式

  • 然后 redux 在组合中间件时,采用了延迟传入基础函数的方式compose(...chain)(store.dispatch)

  1. 中间件形式:({getState,dispatch})=>next=>action=>{...}

  2. 通过 applyMiddleware 和 compose 组合来加强 dispatch

function logger({ getState }) {
  return next => action => {
    console.log('will dispatch', action)
    // Call the next dispatch method in the middleware chain.
    const returnValue = next(action)
    console.log('state after dispatch', getState())
    // This will likely be the action itself, unless
    // a middleware further in chain changed it.
    return returnValue
  }
}

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),
      }
      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose(...chain)(store.dispatch)

      return {
        ...store,
        dispatch,
      }
    }
}

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

Koa 中的中间件

  • Koa 使用了koa-compose组合中间件

  • koa 中的中间件组合:返回一个函数,该函数从第一个中间件开始执行,执行中间件时传入下一个中间件的执行器(dispatch)。

// 中间件形式
app.use(async (ctx, next) => {
  await next()
  const rt = ctx.response.get('X-Response-Time')
  console.log(`${ctx.method} ${ctx.url} - ${rt}`)
})

// 组合
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) {
    // 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)
      }
    }
  }
}

为 fetch 添加中间件

  • 保留 fetch 的原生语法,按需添加中间件,逻辑清晰

// fetch usage: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

// fetch可以接收一个参数或两个参数 fetch(url/Request,[options])
// 此处只考虑fetch(url,options) 的情况

// https://github.com/sindresorhus/is-plain-obj
function isPlainObject(value) {
  if (Object.prototype.toString.call(value) !== '[object Object]') {
    return false
  }

  const prototype = Object.getPrototypeOf(value)
  return prototype === null || prototype === Object.prototype
}

const { fetch } = window
function fetchApi(url, options) {
  return fetch(url, options)
}

const applyMiddleware = (func, middlewares) =>
  middlewares.reduce((prev, cur) => cur(prev), func)

/**
 * 1. 添加'Content-Type': 'application/json'
 * 2. 将body stringify
 * @param {*} next
 * @returns
 */
const reqToJson = next => (url, options) => {
  let { body, headers } = options
  if (isPlainObject(body)) {
    body = JSON.stringify(body)
  } else {
    throw new Error('options.body is not plainObject')
  }

  if (!headers) {
    headers = {}
  }
  if (!headers['Content-Type']) {
    headers = {
      ...headers,
      'Content-Type': 'application/json',
    }
  } else {
    throw new Error(
      'headers with Content-Type is already set, and it\'s not "application/json"'
    )
  }

  return next(url, { ...options, headers, body })
}

/**
 * 为url添加前缀
 * @param {*} baseUrl
 * @returns
 */
const addPrefix = baseUrl => next => (url, options) => {
  if (typeof baseUrl !== 'string') {
    throw new Error('baseUrl should be string')
  }
  return next(`${baseUrl}${url}`, options)
}

const myFetch = applyMiddleware(fetchApi, [
  reqToJson,
  addPrefix('http://localhost:3001'),
])

export default myFetch

最后更新于