“都 1202 年了怎么还有人在用 Redux”——这大概很多人看到这篇文章的第一反应。首先先代表一下,这篇文章并不讨论是否是应该使用 Redux,这是一个比较大的话题,应该单独水一篇。并且社区已经存在许许多多的讨论了,你总能从几篇高赞的文章中找到一些优缺点的对比图,而后结合你项目的场景最终做出决定。咱们来随便举几个团队使用 Redux 的缘由。首先是易懂,Redux 被人吐槽不少的多是写法繁琐,可是在繁琐写法的背后就没有那么多黑科技了,很是容易排查问题。另外,Redux 本质是对逻辑处理方式提出了标准范式,而且搭配得给到了一组实践规范,有助于保持项目代码书写风格与组织方式的一致性,这点在多人合做开发的项目里面尤其重要。其余的优势就不在此赘述啦。javascript
这时候就有同窗可能要问了,你讲 Redux,那和 hooks 又有啥子关系呢。众所周知,在 React 团队推出 Hooks 这个概念后不久,Redux 也更新了对应的 API 来支持。Hooks 的本质是对逻辑的封装以及逻辑与 UI 代码的解耦。有了 Hooks 的加持可以让咱们的 Redux React 项目更加简洁、易懂、扩展性更强。并且 Hooks API 在 Redux 的最佳实践建议中目前是 Level 2 的强烈推荐使用级别。他拥有更简洁的表达方式,更干净的 React 节点数,更友好的 typescript 支持。java
具体 Redux 相关的 API 怎么用,这里不作介绍,能够直接跳转官方文档进行了解。下面咱们会从一个应用场景来具体讲一讲,他们是怎么帮助咱们更好地组织代码的。其中的部分工程级别代码来自于 react-boilerplate 的项目模版,它在动态加载问题上提供了很多帮助。react
在开发大型 React 应用的时候,动态懒加载代码永远是咱们项目架构中的必选项。代码的拆分、动态引用等,工程化工具都已经帮咱们完成了。咱们更须要关注的是,动态引入与解除挂载等操做时额外要作什么,以及这个工做如何尽可能少的暴露给项目开发者。前面说过了,Hooks 最强大的能力在于逻辑的封装,这里固然也就要借助他的力量了。typescript
这里咱们以 Reducer 做为例子来说,其余中间件,例如 Saga 等均可以类推,若是须要能够后续再把相应的代码一并贴出来。咱们把整个封装分为三层:核心实现、可组合封装、对开发者暴露封装。下面咱们按顺序一一讲解。(具体实现中我都会默认带上包含 connected router 的实现,方便须要抄代码的能够直接用)redux
这里的代码实现的是如何为一个 store 挂载与解除挂载拆分后的各个 Reducer 的逻辑。数组
// 本段代码彻底来自于 react-boilerplate 项目 import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; import invariant from 'invariant'; import { isEmpty, isFunction, isString } from 'lodash'; import history from '@/utils/history'; import checkStore from './checkStore'; // 作类型安全检测的,不用关心 function createReducer(injectedReducers = {}) { return history => combineReducers({ router: connectRouter(history), ...injectedReducers, }); } export function injectReducerFactory(store, isValid) { return function injectReducer(key, reducer) { if (!isValid) checkStore(store); invariant( isString(key) && !isEmpty(key) && isFunction(reducer), '(src/utils...) injectReducer: Expected `reducer` to be a reducer function', ); if ( Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer ) return; store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign store.replaceReducer(createReducer(store.injectedReducers)(history)); }; } export default function getInjectors(store) { checkStore(store); return { injectReducer: injectReducerFactory(store, true), }; }
这段有个点比较特殊,须要讲一下。你可能会发现,这里面根本没有解除挂载的部分。这是由于 reducer 比较特殊,他并不会产生反作用,而且由于目前提供的方法是经过整个替换的方式去挂载新的 Reducer,因此并无什么必要去单独作解除挂载。在处理其余中间件的挂载时,特别是那些存在反作用的(例如 redux-saga),咱们须要对应地实现一个解除挂载的 eject 方法。安全
OK,那么如今咱们已经能够经过 getInjectors 方法为整个项目提供一个 injectReducer 注入 Reducer 的能力了(同时可能包含 eject 方法)。下一步就是怎么调度这个能力。react-router
这里,咱们但愿经过一个自定义的 hooks,能够容许开发者为一个组件声明某一个 命名空间 的 reducer 与其生命周期一致地进行挂载与解除挂载。开发者只须要传入 reducer 的命名空间与 reducer 实现,并将这个 hooks 放到相应的组件逻辑中便可。架构
import React from 'react'; import { ReactReduxContext } from 'react-redux'; // 这是咱们在上一步实现的 injector 工厂,经过他来产出一个与固定 store 绑定的 injectReducer 函数 import getInjectors from './reducerInjectors'; const useInjectReducer = ({ key, reducer }) => { // 须要从 Redux 的 context 中获取到当前应用的全局 store 实例 const context = React.useContext(ReactReduxContext); // 为了模拟 constructor 的运行时机 const initFlagRef = React.useRef(false); if (!initFlagRef.current) { initFlagRef.current = true; getInjectors(context.store).injectReducer(key, reducer); } // 若是须要加入 eject 的逻辑,则可使用这样的写法。相似于为当前组件增长一个 willUnmount 的生命周期逻辑。 // React.useEffect(() => (() => { // const injectors = getInjectors(context.store); // injectors.ejectReducer(key); // }), []); }; export { useInjectReducer };
useInjectReducer 这个 Hooks 帮助咱们处理了什么时候去挂载,怎么挂载等问题,咱们最终只须要告诉他 挂载什么 就能够了。经过这层封装,能够发现咱们进一步收敛了关注点。到这一步为止,咱们都是提供了一个项目级别的公共方法。在下一步中,咱们会提供一个统一的写法,在具体的开发过程当中去使用,进一步作封装收敛。函数
在进入下一步以前,咱们先简单解释一下上面的逻辑。逻辑经过注释分为了三段(第三段在 reducer 场景下没用到),第一段咱们经过当前组件所处的 redux 上下文,拿到了 store 的引用,第二段与第三段咱们分别让组件在 初始化 和 销毁前 执行挂载与解除挂载的操做。经过一个 initFlagRef 为 functional 的组件模拟构造器的生命周期(若是有更好的实现方案欢迎指教),由于若是在挂载以后再 inject 的话,会在第一次渲染时取不到对应 store 的内容。
在完成公用方法的封装以后,咱们下一步考虑的就是如何用更简单的方式,为咱们的模块挂载 store 。按照下面的方式,开发者不用关心任何东西,只需一句话就能够完成挂载,也不用提供额外的参数。若是同时有 reducer、saga 或其余中间件内容,也能够一块儿打包搞定。
import { useInjectReducer, // useInjectSaga, } from '@/utils/store'; import actions from './actions'; import constants from './constants'; import reducer from './reducer'; // import saga from './saga'; const namespace = constants.namespace; const useSubStore = () => { useInjectReducer({ key: namespace, reducer }); // useInjectSaga({ key: namespace, saga }); }; export { namespace, actions, constants, useSubStore, };
实际使用范例:
import React from 'react'; import { useSubStore, } from './store'; export default function Page() { useSubStore(); return <div />; };
具体的数据和逻辑咱们也能够封装成几个 Hooks ,例如咱们须要提供一个数组数据简单操做,咱们只关心 添加 和 数量,就能够封装一个 Hooks,这样实际使用方只须要关心 添加 和 数量 这两个要素,不用关心 redux 的具体实现方式了。
import { useMemo, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { actions, constants, namespace, } from './store'; export function useItemList() { const dispatch = useDispatch(); const list = useSelector(state => state[namespace].itemList); // 这只是范例! const count = useMemo(() => list.length, [list]); const add = useCallback((item) => dispatch(actions.addItem(item)), []); return [count, add]; }
下面咱们修改一下使用的地方:
import React from 'react'; import { useSubStore, } from './store'; import { useItemList } from './useItemList'; export default function Page() { useSubStore(); const [count, add] = useItemList(); return <div onClick={() => add({})}>{count}</div>; };
经过这样一种拆分方式,store 的定义,store 的使用逻辑,业务侧三者都只关注本身必须关注的部分,任何一方改动均可以尽可能少地引发变动。
那咱们进一步思考一下,之前咱们可能一个页面对应一个 store。经过 Hooks 进行拆分后,咱们更方便从功能层面去拆分 store,store 的逻辑也会更为清晰。与 store 的交互被封装成了 Hooks 以后也能够很快在多个展现层被使用。这在复杂 B 端工做台场景下会展示出很大的价值。案例会有点长,之后有时间能够再补上。
看完上面的例子,相信聪明的读者已经知道我想表达的问题了。经过结合 Redux + Hooks,标准化了定义代码,对逻辑、调用、定义三者必定程度上进行了解耦。经过简化的 API,减小了逻辑的理解成本,减小了后续维护的复杂度,必定程度上还能够达到复用。不论是相较于过去的 Redux 接入方案,仍是相较于单纯使用 Hooks,都有着其独特的优点。特别适用于逻辑相对复杂的工做台场景。(并且我很喜欢 Saga 的设计思路,能用起来就很爽)。
OK,收。此次以一个简单的例子,稍稍展现了一下在 Hooks 大环境下 Redux 与其产生的化学反应。主要想展现的是依赖 Hooks 的逻辑可封装能力的一种设计思路,Redux 黑的同窗们不要过多纠结与这个选型,萝卜青菜各有所爱。
但愿这个系列能继续写下去 :)
做者:ES2049 / armslave00
文章可随意转载,但请保留此原文连接。很是欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com 。