发布于 2026-01-06 3 阅读
0

如何拆分 Redux store 以进一步提升应用性能 redux-store-manager AWS 安全直播!

如何对 Redux store 进行代码拆分以进一步提升应用性能

redux-store-manager

AWS 安全直播!

如今,为了在用户访问我们的网站时实现最佳的应用程序加载速度,我们对网络上传输的每一个字节的代码都进行严格审查。

假设用户正在访问一个电商网站(基于 React 和 Redux)的首页。为了实现最佳交互时机,JavaScript 包中应该只包含渲染首页首屏部分所需的 UI 组件。我们不应该在用户访问产品列表或结账页面之前加载这些页面的代码。

要实现这一点,您可以:

  1. 延迟加载路由——按需打包每个路由的 UI 组件。
  2. 延迟加载页面首屏下方的组件。

那么 reducer 呢?
与组件不同,主 bundle 包含了所有的 reducer,而不仅仅是首页需要的那些。我们无法实现的原因如下: 

  1. 最佳实践是保持 redux 状态树扁平化——reducer 之间没有父子关系,从而避免代码分割点。
  2. 组件和 reducer 的模块依赖树并不相同。store.js -imports-> rootReducer.js -imports-> reducer.js(files)因此,即使存储的数据是由主组件还是按需组件使用,store 的依赖树也包含了应用程序的所有 reducer。
  3. 了解组件中使用的数据属于业务逻辑,或者至少不能进行静态分析—— mapStateToProps这是一个运行时函数。
  4. Redux store API 本身并不支持代码分割,所有 reducer 都必须在 store 创建之前就成为 rootReducer 的一部分。但是等等,在开发过程中,每当我更新 reducer 代码时,我的 store 都会通过webpack 的热模块替换 (HMR)进行更新。这是怎么回事?没错,为此我们需要重新创建 rootReducer 并使用store.replaceReducer API。这不像切换单个 reducer 或添加新 reducer 那么简单。

遇到不熟悉的概念吗?请参考以下链接和说明,以了解 Redux、模块和 Webpack 的基本概念。

  • Redux - 一个用于管理应用程序状态的简单库,核心概念与 React 结合使用
  • 模块 - 简介ES6 模块动态导入
  • 依赖关系树——如果moduleBA 在 B 中导入moduleA,那么 AmoduleB就是 B 的依赖项;moduleA如果moduleCB 在 C 中导入moduleB,那么最终的依赖关系树为:A -  moduleA -> moduleB -> moduleCB。像 webpack 这样的打包工具会遍历这个依赖关系树来打包代码库。
  • 代码分割——当父模块使用动态导入导入子模块时,Webpack 会将子模块及其依赖项打包到一个单独的构建文件中,该文件会在运行时客户端执行导入调用时加载。Webpack 会遍历代码库中的所有模块,并生成可供浏览器加载的包。代码分割

现在你已经熟悉了以上概念,让我们深入探讨一下。

让我们来看一下典型的 React-Redux 应用结构——

// rootReducer.js
export default combineReducers({
  home: homeReducer,
  productList: productListReducer
});

// store.js
export default createStore(rootReducer/* , initialState, enhancer */);

// Root.js
import store from './store';
import AppContainer from './AppContainer';

export default function Root() {
  return (
    <Provider store={store}>
      <AppContainer />
    </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

首先创建根Reducer和Redux store,然后将store导入到根组件中。这将生成如下所示的依赖关系树。

RootComponent.js
|_store.js
| |_rootReducer.js
|   |_homeReducer.js
|   |_productListReducer.js
|_AppContainer.js
  |_App.js
  |_HomePageContainer.js
  | |_HomePage.js
  |_ProductListPageContainer.js
    |_ProductListPage.js
Enter fullscreen mode Exit fullscreen mode

我们的目标是合并 store 和 AppContainer 的依赖关系树——
这样,当一个组件被代码分割时,webpack 会将该组件及其对应的 reducer 打包到按需执行的 chunk 中。让我们看看理想的依赖关系树大概是什么样子——

RootComponent.js
|_AppContainer.js
  |_App.js
  |_HomePageContainer.js
  | |_HomePage.js
  | |_homeReducer.js
  |_ProductListPageContainer.js
    |_ProductListPage.js
    |_productListReducer.js
Enter fullscreen mode Exit fullscreen mode

如果你仔细观察,你会发现依赖树中没有 store!

在上述依赖关系树中

  1. 假设ProductListPageContainer是动态导入的AppContainer。Webpack 现在会productListReducer在按需构建块中构建,而不是在主构建块中构建。
  2. 每个减震器现在都以容器的形式导入并在商店中注册。

有意思!现在容器不仅可以绑定数据和动作,还可以绑定 reducer。

现在让我们来想想如何实现这个目标!

Redux store 期望rootReducer第一个参数是a createStore。由于这个限制,我们需要两样东西——

  • 在创建之前,让容器绑定 reducer。rootReducer
  • 一个高阶实体,可以保存所有 reducer 的定义,这些定义在rootReducer它们被打包成一个 reducer 之前必须存在。

假设我们有一个名为storeManager 的高阶实体,它提供以下 API:

  • sm.registerReducers()
  • sm.createStore()
  • sm.refreshStore()

以下是重构后的代码及其依赖关系树storeManager-

// HomePageContainer.js
import storeManager from 'react-store-manager';
import homeReducer from './homeReducer';

storeManager.registerReducers({ home: homeReducer });

export default connect(/* mapStateToProps, mapDispatchToProps */)(HomePage);

// ProductListPageContainer.js
import storeManager from 'react-store-manager';
import productListReducer from './productListReducer';

storeManager.registerReducers({ productList: productListReducer });

export default connect(/* mapStateToProps, mapDispatchToProps */)(ProductListPage);


// AppContainer.js
import storeManager from 'react-store-manager';

const HomeRoute = Loadable({
  loader: import('./HomePageContainer'),
  loading: () => <div>Loading...</div>
});

const ProductListRoute = Loadable({
  loader: import('./ProductListPageContainer'),
  loading: () => <div>Loading...</div>
});

function AppContainer({login}) {
  return (
    <App login={login}>
      <Switch>
        <Route exact path="/" component={HomeRoute} />
        <Route exact path="/products" component={ProductListRoute} />
      </Switch>
    </App>
  );
}

export default connect(/* mapStateToProps, mapDispatchToProps */)(AppContainer);

// Root.js
import storeManager from 'react-store-manager';
import AppContainer from './AppContainer';

export default function Root() {
  return (
    <Provider store={storeManager.createStore(/* initialState, enhancer */)}>
      <AppContainer />
    </Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Reducer 只是注册了,而 Store 则是在 RootComponent 挂载时创建的。现在,它拥有了所需的依赖关系树。

RootComponent.js
|_AppContainer.js
  |_App.js
  |_HomePageContainer.js
  | |_HomePage.js
  | |_homeReducer.js
  |_ProductListPageContainer.js
    |_ProductListPage.js
    |_productListReducer.js
Enter fullscreen mode Exit fullscreen mode

如果ProductListPageContainer使用动态导入按需加载,productListReducer则也会被移动到按需加载块内。

好耶!任务完成?……差不多了

问题在于,当按需加载的数据块时, 
sm.registerReducers()其中的调用会在 storeManager 上注册 reducer,但不会刷新包含新rootReducer注册 reducer 的 redux store。因此,要更新 store 的 rootReducer,我们需要使用redux 的 store.replaceReducer API

因此,当父元素AppContainer.js动态加载子元素时ProductListPageContainer.js,它只需调用一个方法即可。这样,在开始访问数据或触发对数据点的操作之前,sm.refreshStore()该存储就具备了相应的权限productListReducerProductListPageContainerproductList

// AppContainer.js
import {withRefreshedStore} from 'react-store-manager';

const HomeRoute = Loadable({
  loader: withRefreshedStore(import('./HomePageContainer')),
  loading: () => <div>Loading...</div>
});

const ProductListRoute = Loadable({
  loader: withRefreshedStore(import('./ProductListPageContainer')),
  loading: () => <div>Loading...</div>
});

function AppContainer({login}) {
  return (
    <App login={login}>
      <Switch>
        <Route exact path="/" component={HomeRoute} />
        <Route exact path="/products" component={ProductListRoute} />
      </Switch>
    </App>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们已经了解了它如何storeManager帮助我们实现目标。让我们把它付诸实践吧!

import { createStore, combineReducers } from 'redux';

const reduceReducers = (reducers) => (state, action) =>
  reducers.reduce((result, reducer) => (
    reducer(result, action)
  ), state);

export const storeManager = {
  store: null,
  reducerMap: {},
  registerReducers(reducerMap) {
    Object.entries(reducerMap).forEach(([name, reducer]) => {
      if (!this.reducerMap[name]) this.reducerMap[name] = [];

      this.reducerMap[name].push(reducer);
    });
  },
  createRootReducer() {
    return (
      combineReducers(Object.keys(this.reducerMap).reduce((result, key) => Object.assign(result, {
        [key]: reduceReducers(this.reducerMap[key]),
      }), {}))
    );
  },
  createStore(...args) {
    this.store = createStore(this.createRootReducer(), ...args);

    return this.store;
  },
  refreshStore() {
    this.store.replaceReducer(this.createRootReducer());
  },
};

export const withRefreshedStore = (importPromise) => (
  importPromise
    .then((module) => {
      storeManager.refreshStore();
      return module;
    },
    (error) => {
      throw error;
    })
);

export default storeManager;
Enter fullscreen mode Exit fullscreen mode

您可以将上述代码片段作为模块添加到您的代码库中,或者使用下面列出的 npm 包 - 

GitHub 标志 sagiavinash / redux-store-manager

使用 redux-store-manager 以声明式的方式对 Redux store 进行代码分割,并使容器拥有完整的 Redux 流程。

redux-store-manager

使用 redux-store-manager 以声明式的方式对 Redux store 进行代码分割,并使容器拥有完整的 Redux 流程。

安装

yarn add redux-store-manager
Enter fullscreen mode Exit fullscreen mode

问题

  1. rootReducer 传统上是使用 combineReducers 手动创建的,这使得根据使用其数据的 widget 的加载方式(无论是在主包中还是在按需包中)来拆分 reducer 代码变得困难。
  2. Bundler 无法通过 tree shake 或死代码消除来删除 rootReducer,从而避免包含那些数据未被任何容器组件使用的 reducer。

解决方案

  1. 让那些将要使用 reducer 存储的数据并触发 action 的容器负责将 reducer 添加到 store 中。这使得容器通过链接拥有了整个 redux 流程。
    • 通过mapDispatchToProps将操作作为组件属性
    • 负责通过storeManager.registerReduers更新数据的Reducer
    • 通过mapStateToProps将数据作为组件属性传递
  2. 使用 Redux store 的 replaceReducer API,无论注册了哪些 reducer,当按需加载数据块时,store 都会刷新……

欢迎来到构建优化领域一个尚未开发的角落 :)

喜欢这个创意吗? - 请分享文章并给 Git 仓库点个星标 :)

文章来源:https://dev.to/websavi/how-to-code-split-redux-store-to-further-improve-your-apps-performance-4gg9