基于 React 和 Redux 的 API 集成解决方案

日期:2020-05-07编辑作者:Web前端

时间: 2019-09-08阅读: 141标签: api

看到标题,也许您会觉得奇怪,redux跟Koa以及Express并不是同一类别的框架,干嘛要拿来做类比。尽管,例如Express以及koa等这类的middleware是指可以被嵌入在框架接收请求到产生响应过程之中的代码。而redux的middleware是提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。我觉得,不管是什么框架,一种思想才是最重要的。相同的概念,可能实现的思路与方法不同,同样值得我们去深究,去学习。

在前端开发的过程中,我们可能会花不少的时间去集成 API、与 API 联调、或者解决 API 变动带来的问题。如果你也希望减轻这部分负担,提高团队的开发效率,那么这篇文章一定会对你有所帮助。

面对多样的业务场景,前后端都需要一种插件机制,可以随意组合。middleware即是这种可以自由组合,自由插拔的插件机制。所以我们就来横向比较一下middleware在redux,koa以及express的运行机制,本文主要通过以下三点来进行比较:

文章中使用到的技术栈主要有:

  1. 异步编程模式
  2. middleware的使用
  3. middleware的执行原理及实现

React 全家桶TypeScriptRxJS

异步编程模式

理解异步编程方式式理解几种middleware执行原理的前提。

框架 异步方式
Express callback
Koa1 generator/yield+co
Koa2 Async/Await
Redux redux-thunk,redux-saga,redux-promise等
  • express: 由于是在ES6之前出现的,所以中间件的基础原理还是callback方式
  • koa: koa1得益于generator特性并通过co框架加入了自动流程管理。(co会把所有generator的返回封装成为Promise对象)koa2则使用了Async/Await的形式(仅仅知识genertator函数的语法糖)
  • redux: 论redux的异步变成,更准确的应该是异步的action的方式。解决异步action的方式有多种,比如:redux-thunk,redux-saga,redux-promise等

本文并不会对js里的异步编程作详细,推荐阮一峰老师写的《深入掌握 ECMAScript 6 异步编程》系列文章。

文章中会讲述集成 API 时遇到的一些复杂场景,并给出对应解决方案。通过自己写的小工具,自动生成 API 集成的代码,极大提升团队开发效率。

middleware的使用

众所周知,Koa是Express框架原班人马基于ES6新特性重新开发的敏捷开发框架。Express主要是基于Connect中间件框架,其自身封装了大量的功能,比如路由、请求等。而Koa是基于co(koa2基于async/await)中间件框架,框架自身并没集成太多功能,大部分功能需要用户自行require中间件去解决。我们首先来比较一下Koa与Express的写法:

//Express
var express = require('express')
var app = express()

app.get('/',(req,res)=>{
    res.send('Hello Express!')
})
app.listen(3000)

//Koa
var koa = require('koa')
var app = koa()
var route = require('koa-route')

app.use(route.get('/',async (ctx) => {
    ctx.body = 'Hello Koa'
}))

app.listen(3000)

redux使用包含自定义功能的middleware来扩展。Middleware可以让你包装store的dispatch方法来达到你想要的目的。同时,middleware还拥有"compose"这一关键特性。(这个下文会讲)多个middleware可以被组合到一起使用,形成middleware链。其中,每个middleware都不需要关心链中它前后的middleware 的任何信息。

例如:

const logger = ()=>{
    // ...
}
const crashReporter = ()=>{
    // ...
}
const thunk = () =>{
    // ...
}

let store = createStore(
  App,
  applyMiddleware(
    crashReporter,
    thunk,
    logger
  )
)

本文的所有代码都在这个仓库:request。自动生成代码的工具在这里:ts-codegen。

middleware的执行原理及实现

  1. 统一处理 HTTP 请求1.1 为什么要这样做?
express的middleware执行原理及实现

Express更像是中间件顺序执行,称之为线性模型

↓
---------------
| middleware1 |
---------------
       ↓
---------------
| ... ... ... |
---------------
       ↓
---------------
| middlewareN |
---------------
       ↓

其实express middleware的原理很简单,express内部维护一个函数数组,这个函数数组表示在发出响应之前要执行的所有函数,也就是中间件数组,每一次use以后,传进来的中间件就会推入到数组中,执行完毕后调用next方法执行函数的下一个函数,如果没用调用,调用就会终止。
下面我们实现一个简单的Express中间件功能

function express() {
    var funcs = [] // 中间件存储的数组
    var app = function (req, res) {
        var i = 0  
        // 定义next()
        function next() {
            var task = funcs[i++]  // 取出中间件数组里的下一个中间件函数
            if (!task) {    // 如果中间件不存在,return
                return
            }
            task(req, res, next);   // 否则,执行下一个中间件
        }
        next()
    }
    // use方法则是将中间件函数推入到中间件数组中
    app.use = function (task) {
        funcs.push(task);
    }
    return app    // 返回实例
}

我们可以直接通过 fetch 或者 XMLHttpRequest 发起 HTTP 请求。但是,如果在每个调用 API 的地方都采用这种方式,可能会产生大量模板代码,而且很难应对一些业务场景:

koa的middleware执行原理及实现

Koa会把多个中间件推入栈中,与express不同,koa的中间件是所谓的洋葱型模型。

image

koa的中间件的实现主要依靠的是koa-compose。首先我们来看下koa-compose的使用,koa-compose模块可以将多个中间件合成为一个:

const Koa = require('koa')
const compose = require('koa-compose')
const app = new Koa()

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`)
  next()
}

const main = ctx => {
  ctx.response.body = 'Hello Koa'
};

const middlewares = compose([logger, main])

app.use(middlewares)
app.listen(3000)

下面我们来分析一下koa-compose的源码

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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'))
      // 当前执行第 i 个中间件
      index = i
      let fn = middleware[i]
      // 所有的中间件执行完毕
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 执行当前的中间件
        // 这里的fn也就是app.use(fn)中的fn
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa的中间件支持普通函数,返回一个Promise的函数,以及async函数。

如何为所有的请求添加 loading 动画?如何统一显示请求失败之后的错误信息?如何实现 API 去重?如何通过 Google Analytics 追踪请求?

redux的middleware执行原理及实现

"It provides a third-party extension point between dispatching an action,and the moment it reaches the reducer"
这是redux作者的描述。

正因为redux单一数据源的特点,数据从顶层流动,middleware就好比管道去辅助这些数据的流向,不同的管道具有不同的特点与功能。

每个middleware函数接受Store的dispatch和getState函数作为命名参数,并返回一个函数。该函数会被传入被称为next的下一个middleware的 dispatch方法,并返回一个接收action的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个middleware会接受真实的store的dispatch方法作为next参数,并借此结束调用链。

下面以redux-thunk为例,来介绍下如何写一个redux middleware,下面是redux-thunk的源码

redux-thunk帮助你统一了异步和同步action的调用方式,把异步过程放在action级别解决

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

一共只有11行,action本身是一个object,带有type和arguments。上述将dispatch和getState传入action,next()和action()是redux提供的方法。接着做判断,如果action是一个function,就返回action(dispatch, getState,extraArgument),否则返回next(action)。

然后将他们引用到Redux Store中

import { createStore, combineReducers, applyMiddleware } from 'redux'

let todoApp = combineReducers(reducers)
let store = createStore(
  todoApp,
  // applyMiddleware() 告诉 createStore() 如何处理中间件
  applyMiddleware(thunk)
)

首先我们从源码出发

import compose from './compose'

export default function applyMiddleware(...middleware){
    return (next) => (reducer,initialState) => {
        let store = next(reducer, initialState)
        let disptach = store.dispatch
        let chain = []

        var middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        } 
        chain = middlewares.map(middlre => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    }
}

下面我们从以下几个点介绍redux的middleware机制。

  1. 函数式编程思想
    redux middlreware的思想是使用匿名单参数函数来实现多参数函数的方法。

ES6实现一个curring函数

function curring(fn){
    return function curried(...args){
        return args.length >= fn.length ? fn.call(this,...args):(...rest)=>{
            return curried.call(this,...args,...rest)
        }
    }
}

好处:

  • 易串联
  • 共享store
  1. 分发store

创建一个普通的store

let newStore = applyMiddleware(mid1,mid2,...)(createStore)(reducer,null)

由于

var middlewareAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        } 
chain = middlewares.map(middlre => middleware(middlewareAPI))

因为闭包,每个匿名函数都可以访问相同的store,即middlewareAPI。

  1. compose
    compose 的源码就是一个函数 compose :
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }
  // 获取最后一个函数
    const last = funcs[funcs.length - 1];
    // 获取除最后一个以外的函数[0,length-1)
    const rest = funcs.slice(0, -1)
   // 通过函数 curry 化
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

这里的compose跟上文中的koa-compose有些类似。属于函数式编程中的组合,它将chain中的所有匿名函数[f1,f2,f3,...,fn]组成一个新的函数,即新的dispatch,假设n = 3:
dispatch = f1(f2(f3(store.dispatch)))
这时调用新dispatch,每一个middleware就依次执行了。

  1. 在middleware中调用dispatch

image

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

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

下面我们来总结一下三者的区别

express

  1. 中间件为一个方法,接受 req,res,next三个参数。
  2. 中间可以执行任何方法包括异步方法。
  3. 最后一定要通过res.end或者next来通知结束这个中间件方法。
  4. 如果没有执行res.end或者next访问会一直卡着不动直到超时。
  5. 并且在这之后的中间件也会没法执行到。

koa

  1. 中间件为一个方法或者其它,接受ctx,next两个参数。
  2. 方法中可以执行任何同步方法。可以使用返回一个Promise来做异步。
  3. 中间件通过方法结束时的返回来判断是否进入下一个中间件。
  4. 返回一个Promise对象koa会等待异步通知完成。then中可以返回next()来跳转到下一个中间件。
  5. 如果Promise没有异步通知也会卡住。

Redux

  1. 中间件为一个方法,接受store参数。
  2. 中间可以执行任何方法包括异步方法。
  3. 中间件通过组合串联middlware。
  4. 通过next(action)处理和传递action直到redux原生的dispatch,或者使用store.dispatch(actio)来分发action。
  5. 如果一只简单粗暴调用store.dispatch(action),就会形成无限循环。

因此,为了减少模板代码并应对各种复杂业务场景,我们需要对 HTTP 请求进行统一处理。

参考文档


1.2 如何设计和实现?

通过 redux,我们可以将 API 请求 「action 化」。换句话说,就是将 API 请求转化成 redux 中的 action。通常来说,一个 API 请求会转化为三个不同的 action: request action、request start action、request success/fail action。分别用于发起 API 请求,记录请求开始、请求成功响应和请求失败的状态。然后,针对不同的业务场景,我们可以实现不同的 middleware 去处理这些 action。

1.2.1 Request Action

redux 的 dispatch 是一个同步方法,默认只用于分发 action (普通对象)。但通过 middleware,我们可以 dispatch 任何东西,比如 function (redux-thunk) 和 observable,只要确保它们被拦截即可。

要实现异步的 HTTP 请求,我们需要一种特殊的 action,本文称之为request action。request action 会携带请求参数的信息,以便之后发起 HTTP 请求时使用。与其他 action 不同的是,它需要一个request属性作为标识。其定义如下:

interface IRequestActionT = any { type: T meta: { request: true // 标记 request action }; payload: AxiosRequestConfig; // 请求参数}

redux 的 action 一直饱受诟病的一点,就是会产生大量模板代码而且纯字符串的 type 也很容易写错。所以官方不推荐我们直接使用 action 对象,而是通过action creator函数来生成相应的 action。比如社区推出的 redux-actions,就能够帮助我们很好地创建 action creator。参考它的实现,我们可以实现一个函数createRequestActionCreator,用于创建如下定义的 action creator:

interface IRequestActionCreatorTReq, TResp = any, TMeta = any { (args: TReq, extraMeta?: TMeta): IRequestAction; TReq: TReq; // 请求参数的类型 TResp: TResp; // 请求响应的类型 $name: string; // request action creator 函数的名字 toString: () = string; start: { toString: () = string; }; success: { toString: () = string; }; fail: { toString: () = string; };}

在上面的代码中,TReq 和 TResp 分别表示请求参数的类型请求响应的类型。它们保存在 request action creator 函数的原型上。这样,通过 request action creator,我们就能迅速知道一个 API 请求参数的类型和响应数据的类型。

constuser:typeofgetUser.TResp = { name:"Lee", age:10};

对于 API 请求来说,请求开始、请求成功和请求失败这几个节点非常重要。因为每一个节点都有可能触发 UI 的改变。我们可以定义三种特定 type 的 action 来记录每个异步阶段。也就是我们上面提到的 request start action、request success action 和 request fail action,其定义如下:

interface IRequestStartActionT = any { type: T; // xxx_START meta: { prevAction: IRequestAction; // 保存其对应的 reqeust action };}interface IRequestSuccessActionT = any, TResp = any { type: T; // xxx_SUCCESS payload: AxiosResponseTResp; // 保存 API Response meta: { prevAction: IRequestAction; };}interface IRequestFailActionT = any { type: T; // xxx_FAIL error: true; payload: AxiosError; // 保存 Error meta: { prevAction: IRequestAction; };}

在上面的代码中,我们在 request action creator 的原型上绑定了toString方法,以及start、success和fail属性。因为 action type 是纯字符串,手写很容易出错,所以我们希望通过 request action creator 直接获取它们的 type,就像下面这样:

`${getData}` // "GET_DATA"`${getData.start}` // "GET_DATA_START"`${getData.success}` // "GET_DATA_SUCCESS"`${getData.fail}` // "GET_DATA_FAIL"

1.2.2 Request Middleware

接下来,我们需要创建一个 middleware 来统一处理 request action。middleware 的逻辑很简单,就是拦截所有的 request action,然后发起 HTTP 请求:

请求开始:dispatch xxx_STAT action,方便显示 loading请求成功:携带 API Response,dispatch xxx_SUCCESS action请求失败:携带 Error 信息,dispatch xxx_FAIL action

这里需要注意的是,request middleware 需要「吃掉」request action,也就是说不把这个 action 交给下游的 middleware 进行处理。一是因为逻辑已经在这个 middleware 处理完成了,下游的 middleware 无需处理这类 action。二是因为如果下游的 middleware 也 dispatch request action,会造成死循环,引发不必要的问题。

1.3 如何使用?

我们可以通过分发 request action 来触发请求的调用。然后在reducer 中去处理 request success action,将请求的响应数据存入 redux store。

但是,很多时候我们不仅要发起 API 请求,还要在请求成功请求失败的时候去执行一些逻辑。这些逻辑不会对 state 造成影响,因此不需要在 reducer 中去处理。比如:用户填写了一个表单,点击 submit 按钮时发起 API 请求,当 API 请求成功后执行页面跳转。这个问题用 Promise 很好解决,你只需要将逻辑放到它的 then 和 catch 中即可。然而,将请求 「action化」之后,我们不能像 Promise 一样,在调用请求的同时注册请求成功和失败的回调。

如何解决这个问题呢?我们可以实现一种类似 Promise 的调用方式,允许我们在分发 request action 的同时去注册请求成功和失败的回调。也就是我们即将介绍的 useRequest。

1.3.1 useRequest:基于 React Hooks 和 RXJS 调用请求

为了让发起请求、请求成功和请求失败这几个阶段不再割裂,我们设计了onSuccess和onFail回调。类似于 Promise 的 then 和 catch。希望能够像下面这样去触发 API 请求的调用:

// 伪代码useRequest(xxxActionCreator, { onSuccess: (requestSuccessAction) = { // do something when request success }, onFail: (requestFailAction) = { // do something when request fail },});

通过 RxJS 处理请求成功和失败的回调

Promise 和 callback 都像「泼出去的水」,正所谓「覆水难收」,一旦它们开始执行便无法取消。如果遇到需要「取消」的场景就会比较尴尬。虽然可以通过一些方法绕过这个问题,但始终觉得代码不够优雅。因此,我们引入了 RxJS,尝试用一种新的思路去探索并解决这个问题。

我们可以改造 redux 的dispatch方法,在每次 dispatch 一个 action 之前,再 dispatch 一个subject$(观察者)。接着,在 middleware 中创建一个rootSubject$(可观察对象),用于拦截 dispatch 过来的subject$,并让它成为rootSubject$的观察者。rootSubject$会把 dispatch 过来的 action 推送给它的所有观察者。因此,只需要观察请求成功和失败的 action,执行对应的 callback 即可。

利用 Rx 自身的特性,我们可以方便地控制复杂的异步流程,当然也包括取消。

实现 useRequest Hook

useRequest提供用于分发 request action 的函数,同时在请求成功或失败时,执行相应的回调函数。它的输入和输出大致如下:

interface IRequestCallbacksTResp { onSuccess?: (action: IRequestSuccessActionTResp) = void; onFail?: (action: IRequestFailAction) = void;}export enum RequestStage { START = "START", SUCCESS = "SUCCESS", FAILED = "FAIL",}const useRequest = T extends IRequestActionCreatorT["TReq"], T["TResp"]( actionCreator: T, options: IRequestCallbacksT["TResp"] = {}, deps: DependencyList = [],) = { // ... return [request, requestStage$] as [typeof request, BehaviorSubjectRequestStage];};

它接收actionCreator作为第一个参数,并返回一个request 函数,当你调用这个函数时,就可以分发相应的 request action从而发起 API 请求

同时它也会返回一个可观察对象requestStage$(可观察对象),用于推送当前请求所处的阶段。其中包括:请求开始、成功和失败三个阶段。这样,在发起请求之后,我们就能够轻松地追踪到它的状态。这在一些场景下非常有用,比如当请求开始时,在页面上显示 loading 动画,请求结束时关闭这个动画。

为什么返回可观察对象requestStage$而不是返回requestStage状态呢?如果返回状态,意味着在请求开始、请求成功和请求失败时都需要去 setState。但并不是每一个场景都需要这个状态。对于不需要这个状态的组件来说,就会造成一些浪费(re-render)。因此,我们返回一个可观察对象,当你需要用到这个状态时,去订阅它就好了。

options作为它的第二个参数,你可以通过它来指定onSuccess和onFail回调。onSuccess 会将 request success action 作为参数提供给你,你可以通过它拿到请求成功响应之后的数据。然后,你可以选择将数据存入 redux store,或是 local state,又或者你根本不在乎它的响应数据,只是为了在请求成功时去跳转页面。但无论如何,通过 useRequest,我们都能更加便捷地去实现需求。

const [getBooks] = useRequest(getBooksUsingGET, { success: (action) = { saveBooksToStore(action.payload.data); // 将 response 数据存入 redux store },});const onSubmit = (values: { name: string; price: number }) = { getBooks(values);};

复杂场景

useRequest封装了调用请求的逻辑,通过组合多个useRequest,可以应对很多复杂场景。

本文由www.129028.com金沙发布于Web前端,转载请注明出处:基于 React 和 Redux 的 API 集成解决方案

关键词:

nginx负载均衡如何实现www.129028.com金沙?

什么是nginx? Nginx是一个免费的,开源的,高性能的服务器和反向代理服务器软件,同时它也可以为IMAP和POP3服务器代...

详细>>

最少编码原则

这的确是大多数程序员,甚至是那些高级程序员都很容易混淆的一个重点。作为一名程序员,编写代码无疑是你职业...

详细>>

CSS中cursor 鼠标指针光标样式

值 前面url()是自定义鼠标的样式,图像的地址,后面的参数是 css 标准的 cursor样式,(IE下面可以不需要) 出现版本...

详细>>

AJAX:如何处理书签和后退按钮(1)

或者如果你不喜欢onclick: window.onload = initialize;function initialize() { // initialize the DHTML History // framework dhtmlHistory.initial...

详细>>