动态导入:我错过了什么吗?

Dynamic Imports: Am I missing something?

我有一个使用 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 会智能地接受我在这里尝试做的事情,并将这个沉重的包分成它自己的块,仅在需要时才下载,但生成的包是相同的——一个main.js 是 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/,
        },
      },
    },
  },

那好吧,看!你有 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,
});
  • 加载器:要动态导入的组件
  • 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>
    );
}

所以当你打包 yow 代码时,我们会得到类似的东西:

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

来自docs:

By adding [magic] comments to the import, we can do things such as name our chunk or select different modes. For a full list of these magic comments see the code below followed by an explanation of what these comments do.

// Single target

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

// Multiple possible targets

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. Follow installation instructions, then you can run yarn build to see routes being split by their name