【问题标题】:Dynamic Imports: Am I missing something?动态导入:我错过了什么吗?
【发布时间】:2020-08-27 18:05:51
【问题描述】:

我有一个使用 Webpack 作为捆绑器的 React 项目,我将我的捆绑包分成两个块——主代码库 main.js 和供应商捆绑包 vendor.js

构建这些包后,main.js 最终为 45kb,vendor.js 为 651kb。

一个特定的供应商库为 225kb,似乎是供应商导入中最严重的违规者。

我在文件顶部的页面组件中导入这个库:

import React from 'react';
import { ModuleA, ModuleB } from 'heavyPackage'; // 225kb import

...

const Page = ({ setThing }) => {

...

};

为了尝试将这个繁重的导入加载到单独的包中,我尝试使用动态导入来导入这些模块。

Page 组件中,直到调用特定函数时才真正使用模块,因此我尝试在该范围内而不是在文件顶部导入模块:

import React from 'react';

...

const Page = ({ setThing }) => {

  ...

  const handleSignIn = async () => {
    const scopedPackage = await import('heavyPackage');
    const { moduleA, moduleB } = scopedPackage;

    // use moduleA & moduleB normally here
  };

};

出于某种原因,我认为 Webpack 会智能地了解我在这里尝试做的事情,并将这个沉重的包分成自己的块,仅在需要时下载,但生成的包是相同的——@987654328 @ 是 45kb,vendor.js 是 651kb。我的思路是否正确,可能我的 Webpack 配置已关闭,或者我是否以错误的方式考虑动态导入?

edit 我已将 Webpack 配置为使用 splitChunks 拆分包。这是我的配置方式:

  optimization: {
    chunkIds: "named",
    splitChunks: {
      cacheGroups: {
        commons: {
          chunks: "initial",
          maxInitialRequests: 5,
          minChunks: 2,
          minSize: 0,
        },
        vendor: {
          chunks: "initial",
          enforce: true,
          name: "vendor",
          priority: 10,
          test: /node_modules/,
        },
      },
    },
  },

【问题讨论】:

  • 首先,好的思维方式。现在让我们看看为什么它仍然在开始时带来它。你能调试你的代码并在heavyPackage代码的第一行下一个断点吗?也许其他人也在您不知情的情况下导入了它(也许第三部分库取决于它)。可能发生的另一件事是您将 webpack 优化规则配置为允许 webpack 生成的最大块是两个。这不太常见。我建议找到第一个带来这个惰性包的导入链 - 如果它来自您的动态导入,我会感到惊讶。
  • 嗨@RazRonen - 感谢您的洞察力。我在我的 Webpack 配置中使用 splitChunks 来拆分我的包。我已更新问题以包含此splitChunks 设置。我需要进行更多研究,但我认为我的 splitChunks 配置阻止了它按预期工作?
  • 有这方面的消息吗?所有的答案都涉及一个动态加载的组件,但你的问题是关于一个沉重的包

标签: javascript reactjs webpack dynamic-import


【解决方案1】:

@Ernesto 的回答提供了一种代码拆分方法,即使用 react-loadablebabel-dynamic-import 插件,但是,如果您的 Webpack 版本是 v4+(并且有 custom Webpack config set to SplitChunks by all),那么您只需要使用魔法cmets 和一个自定义的 React 组件。

来自docs

通过将 [magic] cmets 添加到导入中,我们可以做一些事情,例如命名我们的块或选择不同的模式。有关这些魔法 cmets 的完整列表,请参阅下面的代码,然后解释这些 cmets 的作用。

// 单个目标

import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  'module'
);

// 多个可能的目标

import(
  /* webpackInclude: /\.json$/ */
  /* webpackExclude: /\.noimport\.json$/ */
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackPrefetch: true */
  /* webpackPreload: true */
  `./locale/${language}`
);

因此,您可以像这样创建一个可重用的LazyLoad 组件:

import React, { Component } from "react";
import PropTypes from "prop-types";

class LazyLoad extends Component {
  state = {
    Component: null,
    err: "",
  };

  componentDidMount = () => this.importFile();

  componentWillUnmount = () => (this.cancelImport = true);

  cancelImport = false;

  importFile = async () => {
    try {
      const { default: file } = await import(
        /* webpackChunkName: "[request]" */
        /* webpackMode: "lazy" */
        `pages/${this.props.file}/index.js`
      );

      if (!this.cancelImport) this.setState({ Component: file });
    } catch (err) {
      if (!this.cancelImport) this.setState({ err: err.toString() });
      console.error(err.toString());
    }
  };

  render = () => {
    const { Component, err } = this.state;

    return Component ? (
      <Component {...this.props} />
    ) : err ? (
      <p style={{ color: "red" }}>{err}</p>
    ) : null;
  };
}

LazyLoad.propTypes = {
  file: PropTypes.string.isRequired,
};

export default file => props => <LazyLoad {...props} file={file} />;

然后在您的路由中,使用LazyLoad 并将您的pages 目录中的文件名传递给它(例如pages/"Home"/index.js):

import React from "react";
import { Route, Switch } from "react-router-dom";
import LazyLoad from "../components/LazyLoad";

const Routes = () => (
  <Switch>
    <Route exact path="/" component={LazyLoad("Home")} />
    <Route component={LazyLoad("NotFound")} />
  </Switch>
);

export default Routes;

注意,React.LazyReact-Loadable 是自定义 Webpack 配置或不支持动态导入的 Webpack 版本的替代方案。


可以在here 找到工作演示。按照installation 的说明,然后你可以运行yarn build 来查看路由被他们的name 分割。

【讨论】:

  • 嗨,马特——感谢您的出色回答。我还有很多东西要学,但你的回答肯定有帮助。很多例子都集中在路由级别的动态导入上。在一些示例中,将供应商包导入到延迟加载的文件中,这或多或少是我想要做的。
  • 由于某种原因,我无法让这些示例正常工作,尽管它帮助我更多地表达了所需的场景,即:用户导航到站点 --> 只有主包已下载并在网络选项卡中看到 --> 用户单击按钮(或某些此类操作)--> 供应商块已下载并可以在网络选项卡中看到。
  • 我在 Lazy Loading 上发现了这个 Webpack 页面,它描述了一个非常相似的场景,但即使在他们的示例中,看起来他们也将 lodash 导入移动到主包而不是块中:@ 987654326@.
  • 在您的示例中,我看到这些块被拆分了,但是当我检查时,看起来它们都被加载到页面加载时的网络选项卡中(我也一直在胡闹代码我试过了,如果有问题请告诉我——可能是用户错误)。
  • 在生产中,我只看到在登陆主页时从服务器请求这些文件:screenshot。然后在登陆未找到页面时:screenshot。似乎按预期工作。也就是说,如果 vendors-main 包含未被所有路由共享的块,则可以将它们拆分为更小的块。
【解决方案2】:

好吧,看!你有带有splitChunks 属性的webpack 配置,还需要在webpackoutput 对象的一侧添加一个chunkFilename 属性。

如果我们以 CRA 生成的为例

      // The build folder.
      path: isEnvProduction ? paths.appBuild : undefined,
      // Add /* filename */ comments to generated require()s in the output.
      pathinfo: isEnvDevelopment,
      // There will be one main bundle, and one file per asynchronous chunk.
      // In development, it does not produce real files.
      filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
      // TODO: remove this when upgrading to webpack 5
      futureEmitAssets: true,

      // THIS IS THE ONE I TALK ABOUT
      chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
      // webpack uses `publicPath` to determine where the app is being served from.
      // It requires a trailing slash, or the file assets will get an incorrect path.
      // We inferred the "public path" (such as / or /my-project) from homepage.
      publicPath: paths.publicUrlOrPath,
      // Point sourcemap entries to original disk location (format as URL on Windows)
      devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path
              .relative(paths.appSrc, info.absoluteResourcePath)
              .replace(/\\/g, '/')
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
      // Prevents conflicts when multiple webpack runtimes (from different apps)
      // are used on the same page.
      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
      // this defaults to 'window', but by setting it to 'this' then
      // module chunks which are built will work in web workers as well.
      globalObject: 'this',
    },

一旦你在你的 webpack 上安装了它。接下来是安装npm i -D @babel/plugin-syntax-dynamic-import 并将其添加到您的 babel.config.js 中

module.exports = api =>
...
return {
  presets: [
   .....
 ],
 plugins: [
....
"@babel/plugin-syntax-dynamic-import",
....
 ]
}

那么最后一件事npm install react-loadable 创建一个名为:containers 的文件夹。里面放所有的容器

在 index.js 里面做一些类似的事情:

可加载对象有两个属性

export const List = Loadable({
    loader: () => import(/* webpackChunkName: "lists" */ "./list-constainer"),
    loading: Loading,
});
  • loader:要动态导入的组件
  • loadinh:在加载动态组件之前要显示的组件。

最后在你的路由器上设置每个可加载的路由。

...
import { Lists, List, User } from "../../containers";
...
export function App (): React.ReactElement {
    return (
        <Layout>
            <BrowserRouter>
                <SideNav>
                    <nav>SideNav</nav>
                </SideNav>
                <Main>
                    <Header>
                        <div>Header</div>
                        <div>son 2</div>
                    </Header>
                    <Switch>
                        <Route exact path={ROUTE_LISTS} component={Lists} />
                        <Route path={ROUTE_LISTS_ID_USERS} component={List} />
                        <Route path={ROUTE_LISTS_ID_USERS_ID} component={User} />
                        <Redirect from="*" to={ROUTE_LISTS} />
                    </Switch>
                </Main>
            </BrowserRouter>
        </Layout>
    );
}

所以当你捆绑你的代码时,我们会得到一些类似的东西:

【讨论】:

  • 感谢@Ernesto 的回答。为什么你使用 react-loadable 而不是 React Lazy?
  • 哦,我来自老学校,我的朋友。我没用过 React-Lazy
猜你喜欢
  • 2021-11-23
  • 2023-03-12
  • 2012-12-20
  • 2017-10-25
  • 1970-01-01
  • 2021-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多