本文是有关如何使用动态导入的详细指南,动态导入可实现代码拆分和延迟加载。它还描述了如何使用错误边界来捕获错误。
是import()功能吗?
是的,但是没有。
import()目前处于TC39流程的第4阶段。它是JavaScript中类似函数的模块加载语法形式。
它在许多方面像一个函数一样起作用:
- 它由
()
调用。 - 它为请求的模块的模块名称空间对象返回一个promise,该promise是在获取,实例化和评估所有模块的依赖关系以及模块本身之后创建的。
但这不是一个函数:
- 这是一种语法形式,恰好使用括号,类似于super()。
- 它不继承自
Function.prototype
。因此,不能使用apply
或调用它call
。 - 它不继承自
Object.prototype
。因此,它不能用作变量。const a = import
是非法的。
另一个有趣的观点:import.meta
TC39的第4阶段提案,向JavaScript模块公开了特定于上下文的元数据。它包含有关模块的信息,例如模块的URL。import.meta
是带有null
原型的对象。但是,它是可扩展的,并且其属性是可写的,可配置的和可枚举的。
动态导入,代码拆分,延迟加载和错误边界是有趣的技术。你想知道更多吗?
什么是动态导入?
与静态导入模块相反,动态导入是一种设计模式,用于将对象的初始化推迟到需要时才进行初始化。动态导入可实现代码拆分和延迟加载。这可以大大提高应用程序的性能。
它是通过import()
以下方式完成的:
import("/path/to/import-module.js") // .js can be skipped
.then((module) => {
// do something with the module
});
MDN Web文档中描述了五个动态导入用例:
- 当静态导入极大地减慢了加载速度,并且您不太可能需要代码,或者以后再需要它时
- 如果静态导入会显着增加内存使用量,并且您不太可能需要代码
- 加载时不存在该模块时
- 需要动态构造导入说明符字符串时
- 当导入的模块具有副作用时,除非发生某些情况,否则可以避免这些副作用
静态导入示例
这是网页的用户界面。不管用户是否要选择任何内容,所有内容都是静态导入的。
选择菜单有两个选择:
- 该
Micro Frontends
话题 - 该
React Utilities
话题
Micro Frontends
选择主题后,它将显示两个文章链接。当您单击任何一个链接时,将显示相关的介绍:
React Utilities
选择主题之后,它将显示另外两个文章链接。单击两个链接中的任何一个时,将显示相关的介绍:
让我们创建这个例子。create-react-app
是启动React编码环境的便捷方法:
npx create-react-app my-app
cd my-app
npm start
我们在package.json:
增加了两个dependencies
"dependencies": {
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0"
}
-
react-router-dom
用于建造路由。 -
react-select
是实现下拉列表的一种绝妙方法。
更改src/App.css
为最小样式:
.App {
padding: 50px;
}
.select-category {
width: 300px;
}
在下文中src/index.js
,加BrowserRouter
在第10行和第12行:
mport React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
创建组件生成器:src/buildComponent.js
。
import React from "react";
export const buildComponent = ({title, paragraphs}) => (
<>
<h1>{title}</h1>
{paragraphs.map((item, i) => (
<p key={i}>{item}</p>
))}
</>
);
在中为Micro Frontends
主题创建路由信息src/microFrontendRoutes.js
。
import { buildComponent } from "./buildComponent";
const fiveStepsArticle = {
title: "5 Steps to Turn a Random React Application Into a Micro Front-End",
paragraphs: [
"What is a micro front-ends approach? The term micro front-ends first came " +
"up in the ThoughtWorks Technology Radar in November 2016. It extends the " +
"concepts of microservices to front-end development.",
"The approach is to split the browser-based code into micro front-ends by " +
"breaking down application features. By making smaller and feature-centered " +
"codebases, we achieve the software development goal of decoupling.",
"Although the codebases are decoupled, the user experiences are coherent. " +
"In addition, each codebase can be implemented, upgraded, updated, and " +
"deployed independently.",
],
};
const ecoSystemArticle = {
title: "Build Your Own Micro-Frontend Ecosystem",
paragraphs: [
"Micro-frontend architecture is a design approach. It modularizes a " +
"monolithic application into multiple independent smaller applications, " +
"which are called micro-frontends. Micro-frontends can also be spelled as " +
"micro front-ends, micro frontends, micro front ends, or microfrontends.",
"The goal of the micro-frontend approach is decoupling. It allows each " +
"micro-frontend to be independently implemented, tested, upgraded, updated, " +
"and deployed. A thin micro-frontend container launches multiple " +
"micro-frontends.",
],
};
const FiveStepsComponent = () => buildComponent(fiveStepsArticle);
const EcoSystemComponent = () => buildComponent(ecoSystemArticle);
export const currentRoutes = [
{ path: "/5steps", name: fiveStepsArticle.title, component: FiveStepsComponent },
{ path: "/3steps", name: ecoSystemArticle.title, component: EcoSystemComponent },
];
在中为React Utilities
主题创建路由信息src/reactUtilitiesRoutes.js
。
import { buildComponent } from "./buildComponent";
const useAsyncArticle = {
title: "The Power and Convenience of useAsync",
paragraphs: [
"How do you make async calls in React? Do you use axios, fetch, or even " +
"GraphQL?",
"In that case, you should be familiar with getting data for a successful " +
"call, and receiving an error for a failed call. Likely, you also need to " +
"track the loading status to show pending state.",
"Have you considered wrapping them with a custom Hook?",
"All of these have been accomplished by react-async, a utility belt for " +
"declarative promise resolution and data fetching. We are going to show you " +
"how easy it is to use this powerful react-async.",
],
};
const reactTableArticle = {
title: "An Introduction to React-Table",
paragraphs: [
"A table, also called a data grid, is an arrangement of data in rows and " +
"columns, or possibly in a more complex structure. It is an essential " +
"building block of a user interface. I’ve built tables using Java Swing, " +
"ExtJs, Angular, and React. I’ve also used a number of third party tables. " +
"As a UI developer, there’s no escape from table components.",
"Build vs. buy? It is always a choice between cost and control. When there " +
"is an open-source with a proven track record, the choice becomes a " +
"no-brainer.",
"I would recommend using React Table, which provides a utility belt for " +
"lightweight, fast, and extendable data grids. This project started in " +
"October, 2016, with hundreds of contributors and tens of thousands of " +
"stars. It presents a custom hook, useTable, which implements row sorting, " +
"filtering, searching, pagination, row selection, infinity scrolling, and " +
"many more features.",
],
};
const UseAsyncComponent = () => buildComponent(useAsyncArticle);
const ReactTableComponent = () => buildComponent(reactTableArticle);
export const currentRoutes = [
{
path: "/useAsync",
name: useAsyncArticle.title,
component: UseAsyncComponent,
},
{
path: "/reactTable",
name: reactTableArticle.title,
component: ReactTableComponent,
},
];
现在,让我们来看一下主要的变化src/App.js
:
import React, { useState, useMemo, useCallback } from "react";
import { NavLink, Route, Switch, useHistory } from "react-router-dom";
import Select from "react-select";
import { currentRoutes as mfaRoutes } from "./microFrontendRoutes";
import { currentRoutes as utilRoutes } from "./reactUtilitiesRoutes";
import "./App.css";
const App = () => {
const [topic, setTopic] = useState("");
const [routes, setRoutes] = useState([]);
const selectOptions = useMemo(
() => [
{
value: "mfa",
label: "Micro Frontends",
},
{
value: "util",
label: "React Utilities",
},
],
[]
);
const routeMapping = useMemo(
() => ({
mfa: mfaRoutes,
util: utilRoutes,
}),
[]
);
const history = useHistory();
const handleTopicChange = useCallback(
(selected) => {
setTopic(selected);
setRoutes(routeMapping[selected.value]);
history.push(`/`);
},
[history, routeMapping]
);
return (
<div className="App">
<h1>This is an example of static import</h1>
<p>Jennifer's articles are initially loaded to be used.</p>
<Select
className="select-category"
value={topic}
options={selectOptions}
onChange={handleTopicChange}
/>
<ul>
{routes.map(({ path, name }) => (
<li key={path}>
<NavLink to={path}>{name}</NavLink>
</li>
))}
</ul>
<Switch>
{routes.map(({ path, component }) => (
<Route key={path} path={path} component={component} />
))}
</Switch>
</div>
);
};
export default App;
在第4行和第5行,mfaRoutes
并且utilRoutes
是静态导入的。它们映射在第26-32行。
第9行定义了topic
状态,该状态由Select组件设置。24.选择分量由线路49定义- -选择选项由线12限定54当topic
被选择时,onChange
在管线53将调用handleTopicChange
(行36 - 43)。
第10行定义routes
状态。当handleTopicChange
被调用时,它设置所选择的topic
在线38和将所选择的routes
在线39routes
变化将引起在线路55链接重新渲染-在线63和61个路由改变- 67。
此示例中的所有内容都是静态导入。
转换为动态导入
这是网页的相同用户界面。Select组件下面的内容将被动态导入。
Micro Frontends
选择主题后,它将加载链接和相关介绍。
React Utilities
选择主题后,它将加载链接和相关介绍。
本示例使用与静态导入示例相同的代码库,并对中进行了一些更改src/App.js
。
import React, { useState, useMemo, useCallback } from "react";
import { NavLink, Route, Switch, useHistory } from "react-router-dom";
import Select from "react-select";
import "./App.css";
const App = () => {
const [topic, setTopic] = useState("");
const [routes, setRoutes] = useState([]);
const selectOptions = useMemo(
() => [
{
value: "mfa",
label: "Micro Frontends",
},
{
value: "util",
label: "React Utilities",
},
],
[]
);
const routeMapping = useMemo(
() => ({
mfa: "microFrontendRoutes",
util: "reactUtilitiesRoutes",
}),
[]
);
const history = useHistory();
const handleTopicChange = useCallback(
(selected) => {
setTopic(selected);
import("./" + routeMapping[selected.value]).then((module) => {
setRoutes(module.currentRoutes);
});
history.push(`/`);
},
[history, routeMapping]
);
return (
<div className="App">
<h1>This is an example of dynamic loading</h1>
<p>Jennifer's articles are not loaded until the category is selected.</p>
<Select
className="select-category"
value={topic}
options={selectOptions}
onChange={handleTopicChange}
/>
<ul>
{routes.map(({ path, name }) => (
<li key={path}>
<NavLink to={path}>{name}</NavLink>
</li>
))}
</ul>
<Switch>
{routes.map(({ path, component }) => (
<Route key={path} path={path} component={component} />
))}
</Switch>
</div>
);
};
export default App;
静态进口mfaRoutes
和utilRoute
被删除。routeMapping
第24-30行指向文件名,而不是静态导入。可以跳过文件扩展名。
关键区别在于handleTopicChange
(第34-43行)。它动态导入要设置的相关模块currentRoutes
。
这就是所有的变化。看起来很简单明了。
代码分割
如果您src/App.js
仔细阅读以上内容,您可能想知道为什么我们不愿意在第37行构造文件路径,而不是在routeMapping
中构建完整路径。
尝试一下。
您将遇到错误:"Cannot find module with dynamic import.。" 此错误来自创建响应应用程序使用的Webpack。Webpack在构建时执行静态分析。它可以很好地处理静态路径,但是很难从变量中推断出哪些文件需要放在单独的块中。
如果我们对进行类似的硬编码import("./microFrontendRoutes.js")
,它将使该文件成为单独的块。
如果我们编写类似的代码import("./" + routeMapping[selected.value])
,它将使./
目录中的每个文件成为单独的块。
如果我们编写类似的代码import(someVariable)
,则Webpack会引发错误。
什么是代码拆分?
它的功能是将代码分成多个束(块),然后可以按需或并行加载这些束。它可用于实现较小的捆绑包并控制资源负载的优先级。如果使用正确,则可以减少加载时间。
Webpack提供了三种执行代码拆分的常规方法:
- 入口点:使用入口配置手动拆分代码
- 防止重复:使用将
SplitChunksPlugin
重复数据删除和拆分 - 动态导入:通过内联
import()
拆分代码
对于我们的静态示例,npm run build
显示以下生成的包:
File sizes after gzip:
71.85 KB (+32.46 KB) build/static/js/2.489c17a1.chunk.js
2.24 KB (+1.6 KB) build/static/js/main.7c91b243.chunk.js
776 B build/static/js/runtime-main.8894ea17.js
312 B (-235 B) build/static/css/main.83b9e03d.chunk.css
在以上捆绑中:
-
main.[hash].chunk.js
:这是应用程序代码,包括App.js等。 -
[number].[hash].chunk.js
:这是供应商代码或拆分块。 -
runtime-main.[hash].js
:这是Webpack运行时逻辑的一小部分,用于加载和运行应用程序。 -
main.[hash].chunk.css
:这是CSS代码。
使用import("./microFrontendRoutes.js")
,我们可以看到生成了一个额外的块:
File sizes after gzip:
71.85 KB (+70.95 KB) build/static/js/2.489c17a1.chunk.js
1.17 KB (-74 B) build/static/js/runtime-main.c08b891b.js
902 B (-912 B) build/static/js/main.a5e0768c.chunk.js
885 B (-290 B) build/static/js/3.b3637929.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css
import("./" + routeMapping[selected.value])
,我们可以看到目录中的每个文件都./
变成一个单独的块。
File sizes after gzip:
71.86 KB build/static/js/9.e1addb36.chunk.js
50.68 KB build/static/js/1.ee2fd9a6.chunk.js
49.51 KB build/static/js/0.ee6ff7e2.chunk.js
1.77 KB (-477 B) build/static/js/main.21dcc146.chunk.js
1.24 KB (+496 B) build/static/js/runtime-main.834cfecb.js
1.15 KB build/static/js/3.5ca48094.chunk.js
919 B (-70.95 KB) build/static/js/2.4da83169.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css
283 B build/static/js/5.0753ed26.chunk.js
283 B build/static/js/4.7720f7a0.chunk.js
177 B build/static/js/10.880ba423.chunk.js
160 B build/static/js/6.f3a8c74d.chunk.js
延迟加载
延迟加载是代码拆分的后续步骤。由于代码已在逻辑断点处拆分,因此我们在需要时加载它们。这样,我们可以大大提高性能。尽管总的加载时间可以相同,但是可以改善初始加载时间。通过这样做,我们避免加载用户根本无法访问的内容。
动态导入会延迟加载任何JavaScript模块。React.lazy
通过限制将动态导入作为常规组件进行渲染,使操作变得更加容易。
以下是类似的用户界面。Other Articles
链接下方的内容会延迟加载。
Other Articles
单击链接后,它将加载其余内容:
创建要延迟加载的组件src/OtherRouteComp.js
。
当前,此React组件必须默认导出。命名的导出不支持延迟加载。
import React from "react";
const articles = [
"10 Useful Plugins for Visual Studio Code",
"How to Use VS Code to Debug Unit Test Cases",
"How to Use Chrome DevTools to Debug Unit Test Cases",
"Natural Language Processing With Node.js",
];
const OtherRouteComp = () => (
<>
<h1>Other Articles</h1>
<ul>
{articles.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
</>
);
export default OtherRouteComp;
上面的组件可以延迟加载到src/App.js
:
import React, { Suspense } from "react";
import { NavLink, Route, Switch } from "react-router-dom";
import "./App.css";
const App = () => {
const OtherRouteComp = React.lazy(() => import("./OtherRouteComp"));
return (
<div className="App">
<Suspense fallback={<div>Loading...</div>}>
<h1>This is an example of React Lazy Loading</h1>
<p>Jennifer's other articles are not loaded until the link is clicked.</p>
<ul>
<NavLink to="/others">Other Articles</NavLink>
</ul>
<Switch>
<Route key="/others" path="/others" component={OtherRouteComp} />
</Switch>
</Suspense>
</div>
);
};
export default App;
第6行延迟加载OtherRouteComp
,返回返回解析为由导出的组件的Promise src/OtherRouteComp.js
。该组件被包装在中<Suspense>
,该组件在第10行具有一个后备用户界面,以显示加载期间的过渡。
同样,由于延迟加载,还会生成一个额外的块。
File sizes after gzip:
47.91 KB (+480 B) build/static/js/2.27608de7.chunk.js
1.17 KB (-1 B) build/static/js/runtime-main.fce20771.js
911 B (+232 B) build/static/js/main.6680ea99.chunk.js
372 B (-10 B) build/static/js/3.df091b29.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css
在"network"选项卡上,它显示Other Articles
单击链接后已加载新块。这种延迟加载行为适用于所有动态导入案例。
错误边界
延迟加载会返回一个promise。如果此转换失败,该怎么办?
精心设计的用户体验可以很好地处理这种情况。错误边界用于此目的。它是一个经典组件,可在其子组件树中的任何位置捕获JavaScript错误。它记录这些错误并显示一个后备用户界面,而不是崩溃的组件树。
下面是src/App.js
与MyErrorBoundary
集合在第11和第25行。
import React, { Suspense } from "react";
import { NavLink, Route, Switch } from "react-router-dom";
import { MyErrorBoundary } from "./MyErrorBoundary";
import "./App.css";
const App = () => {
const OtherRouteComp = React.lazy(() => import("./OtherRouteComp"));
return (
<div className="App">
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<h1>This is an example of React Lazy Loading</h1>
<p>
Jennifer's other articles are not loaded until the link is clicked.
</p>
<ul>
<NavLink to="/others">Other Articles</NavLink>
</ul>
<Switch>
<Route key="/others" path="/others" component={OtherRouteComp} />
</Switch>
</Suspense>
</MyErrorBoundary>
</div>
);
};
export default App;
src/MyErrorBoundary.js
是典型的错误边界组件:
import React from "react";
export class MyErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error);
console.error(errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong...</h1>;
}
return this.props.children;
}
}
错误边界具有一种或两种生命周期方法:
-
static getDerivedStateFromError()
(第9-11行):设置错误状态以呈现后备用户界面。 -
componentDidCatch()
(第13-16行):用于记录错误信息。
我们可以通过在第6行进行初始化hasError
来模拟错误true
。然后,我们将遇到由第20行定义的以下后备用户界面:
错误边界的粒度取决于开发人员。可能存在多个级别的错误边界。
结论
我们已经讨论了好处,并提供了动态导入,代码拆分,延迟加载和错误边界的示例。
最后,我们要强调不要过度使用任何技术。静态导入对于加载初始依赖项是更可取的,并且可以从静态分析工具和摇树中更轻松地受益。仅在必要时使用动态导入。
参考
Dynamic Import, Code Splitting, Lazy Loading, and Error Boundaries