动态导入:我错过了什么吗?
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 配置,你还需要在 webpack
的 output
对象的一侧添加一个 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-loadable
和 babel-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.Lazy
和 React-Loadable
是自定义 Webpack 配置或不支持动态导入的 Webpack 版本的替代方法。
可以找到工作演示 here. Follow installation instructions, then you can run yarn build
to see routes being split by their name。
我有一个使用 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 配置,你还需要在 webpack
的 output
对象的一侧添加一个 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-loadable
和 babel-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.Lazy
和 React-Loadable
是自定义 Webpack 配置或不支持动态导入的 Webpack 版本的替代方法。
可以找到工作演示 here. Follow installation instructions, then you can run yarn build
to see routes being split by their name。