前所未有的快速!使用 Suspense/lazy,Webpack 4,Router 和 Redux 对 React App 进行代码拆分(Code-splitting)

10年服务1亿前端开发工程师

世界在不断变化,数字世界同样如此。 对于前端来说,我们的世界发展非常快。几周前 Facebook 团队为我们提供了一个全新的 React 版本,带来了很多新的东西。

今天我们将深入探讨 React v16.6.0 的新功能Suspenselazy ,并讨论最有趣的部分:如何将它们与 React 状态管理库 Redux 绑定。 💪😁

在本文中,我们将使用已经创建的App,您可以从 GitHub repo https://github.com/BiosBoy/React-Redux-Suspense-Lazy-Memo-test 克隆

那是什么 – Suspense 和 lazy ?

简而言之,Suspense – 是一种功能,允许您延迟渲染应用程序树的一部分,直到满足某些条件(例如,终端或资源数据加载完成)。 关于 lazy – 它是动态导入的包装器,它来自最新的 React 版本。 因此,使用示例如下:

import React, {lazy, Suspense} from 'react';
const DynamicComponent = lazy(() => import('./someComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DynamicComponent />
    </Suspense>
  );
}

这里我们有 someComponent 模块,必须异步加载。 因此,为了实现这一点,我们使用由 Suspense 包装的React lazy 功能。 这两个是一对甜蜜的情侣,必须始终在一起,因为第一个用于响应异步模块加载,第二个用于响应用户在屏幕上为用户提供一些微调器或加载信息,直到异步模块加载完成。 它更漂亮,简单实用。

为什么我们需要 Suspense / Lazy ?

从我们获得 动态导入(Dynamic Imports) 功能的那一刻到现在已有一年多了。 对于所有开发人员而言,这是一个巨大的飞跃,他们面临的问题是如何使应用程序更加轻量化,并且如何只向客户交付他们交互真正所需要那部分代码。

它引入了类似于 import 形式的新函数,适用于各种用例。 下面的函数返回一个所请求模块的 模块命名空间对象 的promise ,该对象是在获取、实例化或求值所有模块的依赖关系以及模块本身之后创建的。

以下是如何在原生的 JavaScript 中动态导入和使用某些模块:

<script type="module">
  const moduleDynamic = './someModule.js';
  import(moduleDynamic)
    .then((module) => {
      module.doSomething();
    });
</script>

至于React开发,今天我们有很多针对 React 动态导入的自定义解决方案/库,旨在使我们的代码拆分工作轻松有趣。 在我看来,最受欢迎的是一个 React-Loadable 包,它为我们提供了一个友好的 API ,我们来看一下:

import React from 'react';
import Loadable from 'react-loadable';

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

class App extends React.Component {
   render() {
      return <LoadableComponent/>;
   }
}

这很简单,不是吗? 对于我们所有人来说,这是一个非常好的方法,直到有了 React 原生支持的代码拆分,开箱即用。 因此,今天我认为 React-Loadable 将失去它的受欢迎程度,因为 Suspense/Lazy 为常规的 React 开发提供了更多的灵活性和定可定制性。

通过 React 新引入的 schedulerconcurrent 等特性相结合,我们可以选择何时以及如何向用户提供交互元素:

  • 组件加载完成之前。
  • 组件加载完成后。
  • Onload 组件阶段。

不管怎样,这都需要自己的文章和示例,所以这里我们就不深入了。相反,有了以上所有信息,我们终于可以开始创建 React(Suspense/Lazy)-Redux-Router 应用程序了。

😎请记住,我们将使用已创建的示例作为测试,您可以克隆,并且在本文中使用它:https://github.com/BiosBoy/React-Redux-Suspense-Lazy-Memo-test

使用 Webpack 4 启动代码拆分(Code-splitting)

因此,第一步是创建正确的 Webpack 配置,使我们能够基于 App 的巨大 Modules(模块) /Routes(路由)创建一个应用程序包(在我们的例子中,最后一个是最重要的)。

我不会在这里复制一长串使用过的 Webpack 代码配置,相反,我只想向您提供主要部分,这是为我们将要创建的应用程序块而做出的响应,并对其进行描述。 在这里,我还将为您提供有关此配置的 Github gist 的链接,您可以在其中找到它的实现方式和工作方式……

在这里您可以找到准备工作中的整个 Webpack 4 配置:https://gist.github.com/BiosBoy/8b45ef3fec246813ecb05ce1ae11bfde

// ...

const optimization = {
  optimization: {
    splitChunks: {
      chunks: ‘all’,
      minChunks: 2
    },
    // ...some other rules
  }
  // ... some other modules
}

// ...

如上所示,splitChunks 规则响应了 bundle / chunk 的创建。 这是配置 App bundles(打包)的一个要点。 它可以进行大量的定制,但是对于我们来说,当前的配置是详尽的。如果 app 中包含 2 个以上的动态模块/组件(在我们的例子中它们是路由),它将给我们一个创建块的机会。

有关 splitChunks 定制的更多信息,您可以在官方 Webpack 页面上找到:https://webpack.js.org/plugins/split-chunks-plugin/

使用 React-Router 创建应用程序路由

在第二步,我们需要为我们的 App 创建正确的路由结构。 以下是我们将在React ^ 16功能的 Suspenselazy 帮助下实现我们上面讨论的所有内容。

在本文中,我们将使用一个非常简单的计数器 App。 它将包括几个部分:

--| components: 
  |-- <Header />
  |-- <Body /> 
  |-- <Footer />
--| container: 
  |-- <AppContainer />
--| layout:
  |-- <AppLayout />
--| routes:
  |-- <HelloWorld />
  |-- <StartCoding />
--| controller:
  |-- store.js
  |-- actions.js
  |-- reducers.js
  |-- initialState.js
  |--| middleware:
     |-- reduxLogger.js
     |-- rootReducer.js

Body 组件是一个 HOC(高阶组件),可以接受 2 个 动态组件<HelloWorld /><StartCoding />

重要! 我不会停留在 React 生态系统和组件导入的工作原理上(我想如果您正在阅读本文,这意味着您已经熟悉它了)。

我不会停止 React 生态系统和组件导入的工作方式(我想如果你正在阅读这篇文章,那就意味着你已经熟悉它了)。 我想向您展示的一点是,我们可以轻松地使用 Suspense/lazy,Webpack 4,Router 和 Redux 对 React App 进行代码拆分。

让我们开始工作,并建立我们的应用程序路由的 entry point(入口点) :

// ./container/index.js

// ... some Components and dependencies imports

const AppContainer = ({ store, history }) => {
  return (
    <AppLayout>
      <Suspense fallback={<LoadingPlaceholder />}>
        <Switch location={location}>
          <Route 
            exact 
            path='/'
            render={
              () => <AsyncComponent componentName='HelloWorld' />
            }
          />
          <Route 
            path='/next' 
            render={
              () => <AsyncComponent componentName='StartCoding' />
            } 
          />
       </Switch>
      </Suspense>
    </AppLayout>
  );
};

// ...

这里我们看到我们的主要的路由结构。 它由 react-router-dom v.4 API 组成:

  • <Switcher /> 用于在 url 更改时替换组件。
  • <Route /> 基于当前 url 的渲染组件。

对我们来说最有趣是

  • <Suspense /> 包装器,它提供了一个回退API,可以在动态组件最终加载之前向用户显示一些占位符。 这意味着,每当特定导航的 bundle(包) 未被用户路由加载时,Suspense 将回退以显示占位符。 这是简单而美观的 API,不是吗?:)

一旦 bundle(包) 被加载后,Suspense 将通过 <AppLayout /> App Layout 组件中的 Render Pattern抛出动态组件。

但这还不是全部。悬念只是动态加载的回退包装。我们不能忘记在应用程序中动态加载HelloWorld并开始对组件进行编码。我们可以使用React lazy wrapper实现这一点。在这里

但并非全部。 Suspense 只是动态加载的回退包装器。 我们不要忘记在 App 中动态加载 HelloWorldStartCoding 组件。 我们可以使用React lazy 包装器来实现它。 这里就是:

// ./routes/index.js

// ... some Components and dependencies imports

const HelloWorld = lazy(() => import(/* webpackChunkName: "HelloWorld" */ './HelloWorld'));

const StartCoding = lazy(() => import(/* webpackChunkName: "StartCoding" */ './StartCoding'));

const Components = {
  HelloWorld,
  StartCoding
};

const AsyncComponent = props => {
  const { componentName } = props;

  const Component = Components[componentName];

  return <Component {...props} />;
};

// ...

P.S. /* webpackChunkName: ‘COMPONENT_NAME’*/ 允许我们在应用部署阶段设置 bundle(包) 名称,而不是常规编号。

就这样。现在我们可以将所有这些结合在一起并放入主要布局组件:

class AppLayout extends Component {
  render() {
    const { children } = this.props;
    
    return (
      <div className={styles.appWrapper}>
        <Header />
        {children} // here is our dynamic component will be
        <Footer />
      </div>
    );
  }
}

使用异步 Reducers 创建 Redux 存储

第三步非常重要,在这里我们必须以某种方式使我们的 Redux 存储与动态组件兼容,并为每个存储提供自己的 reducers 存储。 今天这已经不是问题了。我将在这里展示一个非常流行的方法,基于 reducers 注入。下面是它的工作原理:

1)我们需要创建一个基本的 Redux 存储:

// ./controller/store.js
// ... some Components and dependencies imports
const rootStore = () => {
  const middleware = [routerMiddleware(history), logger];
  const store = createStore(
    makeRootReducer(),
    initialState,
    compose(
      applyMiddleware(...middleware),
      ...enhancers
    )
  );
  store.asyncReducers = {};
  return store;
};
// ...

我知道代码很多:),但只有一个字符串对我们来说非常重要 – store.asyncReducers 。 Redux 存储中的这个注入对象将响应导入动态组件 reducer。

2)创建 App 的根reducer:

// ./controller/widdleware/rootReducer.js

// ... some dependencies imports

const makeRootReducer = asyncReducers => {
   return combineReducers({
     ...asyncReducers,
     common,
   });
};

// ...

我们上面的内容 – 函数 makeRootReducer()redux 包的 combineReducers API 的常规包装器。 它可以接收任何数量的新 reducers(从动态组件获取),并在主存储区(main single store)将它们组合在一起。 但还没有结束。 我们需要一些 API 在 App 中使用这种方法。

因此,为了让它工作,我们可以使用 makeRootReducer 同一个文件中编写一个名为 injectReducer() 的小实用函数:

// ./controller/widdleware/rootReducer.js

// ... some dependencies imports

const makeRootReducer = asyncReducers => {
  // ...
};

export const injectReducer = (store, { key, reducer }) => {
  if (Object.hasOwnProperty.call(store.asyncReducers, key)) return;
   
  store.asyncReducers[key] = reducer;
  store.replaceReducer(makeRootReducer(store.asyncReducers));
};

// ...

injectReducer 函数将检查动态加载的组件是否已经在存储中,如果 reducer 不存在,它将立即中断或添加它。

差不多就这些了!我们只需要在代码上做一些改进,我们的应用程序就会活跃起来!:)

将 Redux 存储与 React 动态组件集成

最后一步是对 Redux 存储进行集成,主要全局的 Reducer 和 在 HelloWorldStartCoding 路由的特定 Reducers 之间。

在我上面提供的 App repo 中,您可以找到如何在组件路由和全局 App store之间实现业务逻辑。但是,我向您保证,在Redux 网站的例子中,没有什么有趣或创新的东西是找不到的😉

让我们继续,使我们的路线在整个 Redux 存储内进行 reducer 注入。 为了做到这一点,首先,我们需要升级当前路由的代码,并插入我们之前在组件加载之后创建的 injectReducer 函数:

// ./routes/index.js
const AsyncComponent = props => {
  //...
  import(`./${componentName}/controller/reducer`)
     .then(({ default: reducer }) => {
       injectReducer(rootStore, { key: componentName, reducer });
     })
  //... 
};
// ...

注意弄清楚我们上面做了什么:

在这里,我们看到了常规的JS动态导入用法: import(./${componentName}/controller/reducer)

…我们使用它来根据我们在渲染期间从 <AppLayout /> 组件接收到的 componentName prop 导入当前的组件 reducer 。

一旦模块 reducer 加载后,我们在全局 Redux rootStore 存储中注入它,并包含 key prop 作为标记,用于检查该 reducer 是否已在存储中提供:

injectReducer(rootStore, { key: componentName, reducer });

因此,通过这种细微的集成,我们找到了一种简单的方法,可以在一个地方保存全局和特定路由的 reducer ,他们可以从整个 App 环境中访问。

使用 Redux 存储集成React-Router

我们需要实现的最后一件事就是让我们的 Redux 存储 响应位置/路由变化。 我们需要做的就是 – 在 redux <Prodiver />connected-react-router <ConnectedRouter /> 软件包 HOC 中包装<AppContainer /> 后代:

// ./container/index.js

// ... some Components and dependencies imports

const AppContainer = ({ store, history }) => {
  return (
    <AppLayout>
      <Prodiver store={store}>
        <ConnectedRouter history={history}>
         
          //... dependencies Routes/Components

        </ConnectedRouter>
      </Provider>      
    </AppLayout>
  );
};

// ...

此外,我们需要记住在全局 Redux 存储中添加带有 history 对象的 connected-react-router reducer:

// ./controller/widdleware/rootReducer.js

// ... some dependencies imports

const makeRootReducer = asyncReducers => {
   return combineReducers({
      ...asyncReducers,
      common,
      // routing
      router: connectRouter(history)
   });
};

// ...

如果您想知道如何获取 history 对象,connectRouter reducer并将它们组合在一起,您可以在这篇文档的 App repo 中找到该问题的答案。

概括

在这里你明白了! 我们有一个简单但很酷的 React-Redux 应用程序,它基于非常简单的 Webpack 4 配置和Suspense/lazy 动态组件加载逻辑来进行代码拆分。

您现在可以尝试实现新的路由,Reducers 以及 Async Redux-Saga集成! 它具有与本文中描述的相同的模式! 一定要让你的应用程序像你所看到的这样!

当然,如果我出错了或者有什么东西可以改进或简化,请告诉我。 PR 和提交 issues 都非常受欢迎! 这里有一些有用的链接:

英文原文:https://medium.com/@svyat770/fast-as-never-before-code-splitting-with-react-suspense-lazy-router-redux-webpack-4-d55a95970d11

赞(1) 打赏
未经允许不得转载:WEB前端开发 » 前所未有的快速!使用 Suspense/lazy,Webpack 4,Router 和 Redux 对 React App 进行代码拆分(Code-splitting)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

前端开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏