React 没有中间件还能用吗?


redux middleware 是 redux 的一个 advanced feature. 这个概念并不是很新奇,以为在 Koa 里面早已经实现过了. 对比与原生的redux middleware , koa 的 middleware 差不多相当于是爸爸级的 level 了. 这么说,是有依据的. 我们这里,来深入一下源码,具体看一下redux middleware 到底做了些啥. 我们首先来探讨一下基本的源码吧.

redux 的中间件具体流程为:

redux 的源码可以说是比较简单的。 首先,入口文件是 index.js 。我们来看一下,里面具体的内容:

// 从不同的文件里面导出相关方法
export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

这里,其实是 redux 的提供的所有方法了. createStore,combineReducers,bindActionCreators 这三个方法,与 middleware 关系不大,我这里就不赘述了. 这里,主要讲解一下 applyMiddleware 方法和 compose 方法. in fact, compose 是一个非常基础的方法, 用来以函数式的编程来组合中间件, 在 koa 中我们也同样遇见过这样的写法. applyMiddleware 也是用到这样的方法的. so, 我们来具体看看.

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

第一眼看的时候, 肯定超级S B。。。 md... 这写个啥... 看了一下官方给的注释就是:

// 简单 demo 就是
compose(f, g, h) is identical to doing (...args) => f(g(h(...args))).

合着就是个函数嵌套的写法。。。 关键坑爹的在于他的reduceRight方法, 写的真心6. 由于,return 两个函数时,只会返回第二个执行的结果:

function test(a,b){
    return a(),b();
}
console.log(test(a=>1,b=>2));
// 开玩笑的. 上面那种只是科普一下. 他真正的机制实际上是利用 reduceRight 的第二个参数来执行的

Array.reduceRight(fn,start); 
// 主要还是这里的start, 相当于就是 last(...args)

// 将上面代码翻译一下就是
rest.reduceRight(function(composed, f){return f(composed)}, last(...args));
//... 慢慢去体会吧...

所以, 一开始看的时候,在纠结 最后一个 composed 都没执行... 后来发现, 原来还有一层 last(...args). 不过实话说, 真心没有 koa 里面的 compose 函数写得好, 你直接先写一个 noop 函数不行吗!!!

// 俺 实际写了一个替换的compose. 感觉这个看的清楚一点
function compose(...funcs) {
    return function(...args) {
        var len = funcs.length,
         middle = funcs[--len](...args);
        while (len--) {
            middle = funcs[len].call(this, middle);
        }
        return middle;
    }
}
// 测试
console.log(compose(a => a, b => b, (c,b) => c+b)('a', 'b'));

这个是简单的compose 函数. 下面,我们来看一下重点,关于 redux-middleware 的核心方法, applyMiddleware.

applyMiddleware 中间件

由于这个中间件有点复杂, 对传入的函数有具体的要求. 我们先来看一下使用该方法的上下文。

offical website 找到一个 demo:

let store = createStore(
  todoApp,
  // applyMiddleware() tells createStore() how to handle middleware
  applyMiddleware(logger, crashReporter)
)

最终 applyMiddleware return 的结果,还需要返回到 createStore 里去的. 通过 createStore 传入方法时, 函数里面并未对 里面做什么处理.

function createStore(reducer, preloadedState, enhancer) {
  // 这里就是一些列条件判断, 如果你使用 middle 是上面的形式,那么就会直接将参数赋给 enhancer
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    // 实际上调用 applyMiddleware 方法的地方. 注意他传入的参数即可. z
    return enhancer(createStore)(reducer, preloadedState)
  }
}

ok, 我们了解了传入 applyMiddleware 的参数后, 继续看. 中间件的写法:

// 这里就看一下logger 就Ok
const logger = store => next => action => {
 // debug info
  console.group(action.type)
  console.info('dispatching', action)

  let result = next(action)

  // debug info
  console.log('next state', store.getState())
  console.groupEnd(action.type)

  return result
}
// 我们将 debug 信息去掉之后,可以得到一个精简版的 middleware

const logger = store => next => action => {
  // 传递前, 执行的代码
  let result = next(action)
  // 传递完, 执行的代码
  return result
}

看到这里,有种 koa 的感觉. next(action) 显示的将控制权交给下一个函数进行执行. 相当于就是 onion model.

这里, 放一下 applyMiddleware 的源码:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

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

    return {
      ...store,
      dispatch
    }
  }
}

看到这里, 我也是有点惊讶, 第一是他的中间件传递的复杂度巧妙的转化为箭头函数的写法, 第二是他显示的改变了真正的dispatch的内容。 最后实际调用整个流程,是直接根据applyMiddleware提供的方法来的:

// 注意这里是 applyMiddleware 提供的 dispatch 方法
store.dispatch(action)

如果按照上面的调用方式写的话,具体调用顺序就是:

applyMiddleware(logger, crashReporter)

其中,中间件最常用的就是用来检查登录状态的信息。

登录状态的检查

有时候,需要对指定的接口进行登录态的判断。我们可以看到中间件的写法:

const logger = store => next => action => {
  // 传递前, 执行的代码
  let result = next(action)
  // 传递完, 执行的代码
  return result
}

在需要登录的 action 里面手动再带上一个参数。比如:

# 通过额外传递一个 check 属性值,我们就可以实现登录态的检查
store.dispatch({
    type: "LOGINED-FETCH",
    check: true
})

# 中间件登录态检查
const Logined = store => next => action =>{
    let user = _.getCookie("skey");

    if(!action.check){
        return next(action);
    }

    if(!user){
        clearCookie();
        hisotry.push('/');
    }
}

// 然后,再通过 applyMiddleware 直接绑定即可:
applyMiddleware(Logined);

初始化状态 initialState

initialState 相当于设置初始化所有 reducer 的信息,它直接通过 createStore 方法,利用 Object.assign() 模式来将属性拷贝进入。其实不用 initialState 也是 OK 的,一般我们是直接在每个 Reducer 中直接设置好 state:

export const initialState = {
  currentTime: new Date().toString(),
}

export const reducer = (state = initialState, action) => {
  switch(action.type) {
    case types.FETCH_NEW_TIME:
      return { ...state, currentTime: action.payload}
    default:
      return state;
  }
}

有时候,为了方便,也可以通过 initialState 抽到一个事件当中去:

# 设置初始化 reducer 信息
export const initialState = {
  currentTime: currentTime.initialState,
  currentUser: currentUser.initialState,
}

# 然后通过 createStore 方法直接进行合并即可
import { rootReducer, initialState } from './reducers'
// ...
export const configureStore = () => {
  const store = createStore(
    rootReducer,
    initialState,
  );

  return store;
}

combineReducers 做了什么

combineReducers 主要和是 createStore API 结合在一起使用的。用来将多个 reducer 合并起来。他的最终结果是,针对不同 reducers 返回的结果,一般只能影响到本 reducers 里面的内容。

export default createStore(
    combineReducers({
        roomInfo,
        videoInfo
    })
)

这样,通过 roomInfo 和 videoInfo 两个 reducers 返回的结果只能作用在当前 state 下的作用域。例如:通过 roomInfo 返回的 state,只能通过 state.roomInfo 获得。

// 在 roomInfo.js 里
export function showSuperAdmin(isAnchor,userUin) {
    return {
        type: SHOW_SUPER,
        admin:{
            isAnchor,
            userUin,
            isShow:true,
            isSuper:true
        }
    };
}

// 在 index.js 中获取得到:
state.roomInfo.admin.xxx

这里,combineReducers 也是对 state 做了命名空间的划分。

applyMiddleware all procedure

applyMiddleware 整个执行过程:

对应于上文, 整个API的流程图为: 关键点在于applyMiddleware 和 中间件两个内容.

redux 中间件机制

middleware 有 3 个参数 store,action,next。例如:

const log =  store => next => action => {
    //...
}

他们分别代表着

关于 redux-middleware 还有一个比较流行的库, 即, redux-thunk . 该库灰常简单, 就一个函数.

redux-thunk

直接看源码算了:

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;

他和原来的中间件的写法有一个非常不同的地方,在于. 他写中间件的地方, 不在 createStore 里面, 而在 dispatch 里面.

// 初始化调用
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

// thunk 类型的中间件
function doSth(forPerson) {
  // 这里必须返回一个函数... 才能达到中间件的效果
  return function (dispatch) {
    return async().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
    );
  };
}
// 更简便的写法可以为:
let doSth = forPerson=>dispatch=> async().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
)

看源码,我们可以很容易发现, 在实际调用 dispatch 时, 不仅仅只有 dispatch 这一个参数,还有 getState,extraArgument 这两个参数。 so, what are they doing? getState 这个就不用说了, 就是用来获取当前 redux 的 state. 那 extraArgument 干啥嘞? 看源码很容易发现, 就是在初始化 thunk 时, 传入的参数. 其实, 也不会经常用到. 所以, 我们中间件实际可以写为:

let doSth = forPerson=>(dispatch,getState,extArgs)=> async().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize('The Sandwich Shop', forPerson, error))
)

对比与上面的 applyMiddle 来说, 使用 redux-thunk 可以实现 私有, 定制化的 中间件操作. 而,原来的 applyMiddleware 则 只能在初始阶段设置相关的中间件, 但 却可以实现 next 的执行域的分离. 所以, 两个都可以使用, 只是看你具体需求是啥.

react-redux 中间件

不过,在 react-redux 中,已经将两者都已经实现了,connect 方法和上面的一致,差别的是 dispatch。这里,react-redux 将子单元的中间件的写法应用到 dispatch 当中。即,一般使用 dispatch 返回 plain object(例如,type:xxx),这样,可以通过全局的 actions 捕获到而执行相同的方法。不过,dispatch 还可以接受一个函数格式如下:

// actions.js
function requestPosts(subreddit) {
    return { type: 'REQUEST_POSTS', subreddit }
}
function receivePosts(subreddit, json) {
    return { type: 'RECEIVE_POSTS', id }
}

export function fetchPosts(params) {
    return (dispatch,getState)=>{
        dispatch(requestPosts(subreddit))

        fetch(`http://www.subreddit.com/r/${subreddit}.json`)
            .then(response => response.json())
            .then(json =>
                dispatch(receivePosts(subreddit, json))
            )
    }
}
# 可以简写为
export let fetchPosts = params=>(dispatch,getState)=>{
        dispatch(requestPosts(subreddit))

        fetch(`http://www.subreddit.com/r/${subreddit}.json`)
            .then(response => response.json())
            .then(json =>
                dispatch(receivePosts(subreddit, json))
            )
}

// 在 index.js 中
dispatch(fetchPosts(roomID));

getState 这个就不用说了, 就是用来获取当前 redux 的 state. 这样,可以在通过 dispatch 触发指定 action 时,进行异步数据的拉取,不过最终它还是需要通过 dispatch 返回一个 plain object 来进行触发全局的 actions。 整个中间件的流程图为:

原文发布于微信公众号 - 前端小吉米(villainThr)

原文发表时间:2018-04-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java开发

Spring集成MyBatis 通用Mapper以及 pagehelper分页插件

这里送上楼主配置的Spring DataSource 和 TransactionManager 和MyBatis配置

822
来自专栏C/C++基础

网络字节序与主机字节序转换

在Linux网络编程中,经常碰到网络字节序与主机字节序的相互转换。说到网络字节序与主机字节序需要清晰了解以下几个概念。

603
来自专栏lulianqi

Stream 流操作

Stream 是所有流的抽象基类(不能被实例化,需要使用他的派生类FileStream/MemoryStream/BufferedStream)。流是字节序列的...

792
来自专栏菩提树下的杨过

rpc框架之 thrift 学习 2 - 基本概念

thrift的基本构架: ? 上图源自:http://jnb.ociweb.com/jnb/jnbJun2009.html 底层Underlying I/O以上...

2087
来自专栏喔家ArchiSelf

全栈Python 编程必备

Python作为一种编程语言,被称为“胶水语言”,更被拥趸们誉为“最美丽”的编程语言,从云端到客户端,再到物联网终端,无所不在,同时还是人工智能优选的编程语言。

774
来自专栏java学习

Java每日一练(2017/6/17)

Java基础 | 数据库 | Android | 学习视频 | 学习资料下载 最新通知 ●回复"每日一练"获取以前的题目! ●【新】Ajax知识点视频更新了!(...

2638
来自专栏数据之美

MapReduce 中的两表 join 几种方案简介

1. 概述 在传统数据库(如:MYSQL)中,JOIN操作是非常常见且非常耗时的。而在HADOOP中进行JOIN操作,同样常见且耗时,由于Hadoop的独...

1825
来自专栏文渊之博

关于数据存储类型的一点分析

简介     SQL Server每个表中各列的数据类型的有各种形式,产生的效果也各有不同,我们主要根据效率兼顾性能的情况下讨论下如何规定类型。     在SQ...

1676
来自专栏用户2442861的专栏

最常用的两种C++序列化方案的使用心得(protobuf和boost serialization)

http://blog.csdn.net/lanxuezaipiao/article/details/24845625

1042
来自专栏步履前行

Java 基础-LocalDate相关

那么在写具体的LocalDate前,我们先来看下为什么要在Java8中搞一套新的API呢,因为旧的Date类非常的难用,比如,其中的几个构造方法都被标注为@De...

501

扫码关注云+社区