# 中间件与 Redux 与 Koa

* [参考](https://redux.js.org/understanding/history-and-design/middleware)

## compose 函数

```js
/**
 * 接受一组函数，返回一个函数，该函数从右向左执行传入的函数，函数执行的结果作为下一个函数的参数，返回最后一个函数执行结果
 * 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 的方式，返回一个加强后的函数

```js
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

```js
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](https://github.com/koajs/compose/blob/master/index.js)组合中间件
* koa 中的中间件组合：返回一个函数，该函数从第一个中间件开始执行，执行中间件时传入下一个中间件的执行器（dispatch）。

```js
// 中间件形式
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 的原生语法，按需添加中间件，逻辑清晰

```js
// 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
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://linqiang-miao.gitbook.io/github/zhong-jian-jian-yu-redux.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
