
安装:
yarn add react-loadable
范例:
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}}
手册:
假想你已经实现了一个react应用,并且正在使用webpack进行构建,一切进行的都是那么顺利。但是突然有一天你发现你应用中的模块正在变大,这使你的应用运行变得越来越慢。这时候你就需要对你的代码进行代码拆分了!

代码分割就是将包含整个应用的一个大模块,分割成包含应用各个独立部分的许多小模块。实现这个看起来很难,但是像webpack这样的工具内置了这样的功能,而React Loadable是为了使其更简单实现而诞生的。
基于路由拆分 VS 基于组件拆分
你经常能听到别人建议你将程序按照不同路由进行拆分,然后通过异步的方式加载。这对于很多用户来说这样已经干的很不错了,因为点击一条链接,然后等待页面加载,并浏览这对很多上网用户来说是很平常的操作,可以很欣然的接受。
但是其实我们可以做的更好的。
现在使用的大部分react路由工具都是一个简单的组件,没有什么与众不同的地方。所以如果我们使用基于组件分割的方式进行优化的话,我们又能得到什么呢?

很多事实证明,有很多地方根据组件进行程序拆分要比根据路由进行拆分简单的多。例如Modals,tabs等许多会隐藏内容的UI组件,它们会在用户进行某些操作的时候才会被加载。
例如:你的应用程序有一个选项卡组件,用户很有可能根本就不会点进其他的选项页面,那我们又为什么要在父路由上将选项卡的所有页面组价加载出来的?
很多地方,你可以等优先级高的组件加载完成后再加载其他的组件。也就是说,那个位于页面底部并且内部加载了很多库的组件,普遍认为没有必要和位于页面顶部的展示组件同时加载。
由于路由其实就是组件,所以我们还是可以很轻松在路由层面分割代码。
在你的应用中使用新的代码分割功能是很简单的,以至于你根本不用反复思考。你只需要改动少量的代码因为一切都应该是自动化完成的。
react-loadable简介
react-loadable是一个在react应用是使用非常简单的轻量级的代码分割组件库。
loadable是一个可以使你在组件渲染之前动态加载任何模块的高阶组件。(一个可以创建组件的方法)
让我们想象两个组件,一个是引入进来的组件,一个是需要渲染的组件:
import Bar from './components/Bar';
class Foo extends React.Component {
render() {
return <Bar/>;
}
}
这种情况下,我们通过import将bar组件引入是一种同步的操作,但是我们在Foo组件渲染之前并不需要它,那我们为什么不推迟到需要render的时候在将Bar进行引入呢???
使用dynamic import(动态引入)的方法,我们可以修改我们的组件,让加载Bar组件变成异步的。
class MyComponent extends React.Component {
state = {
Bar: null;
};
componentWillMount {
import(./component/Bar).then(Bar => {
this.setState({ Bar: Bar.default });
});
}
render {
let { Bar } = this.state;
if (!Bar) {
return <div>Loading...</div>;
} else {
return <Bar />
}
}
}
但是上面写的是整个的操作流程,并不是简单的代码分割用例。当我们import失败的时候怎么办?服务端渲染的时候我们又怎么办?
这时我们就可以抽象出来一个loadable对象来解决这个问题。
import Loadable from 'react-loadable';
const LoadableBar = Loadable({
loader: () => import('./component/Bar'),
loading() {
return <div>Loading</div>
}
});
class MyComponent extends React.Component {
render() {
return <LoadableBar />;
}
}
使用import()进行自动化的代码分割
当你在webpack2+版本中使用import的时候,即使你没有额外的配置,它也能自动的进行代码分割。
这意味着使用import()和React Loadable可以很快容易的按照你想要的方式进行代码的分割,并且找出最适合你的程序的代码分割实践方式。
创建一个完美的“Loading...”组件
通常来说在加载是只给用户一个静态的“loading”文案是不合适的,因为你要考虑表现出更多的错误状态,如超时等,并且给用户一个较好的使用体验。
function Loading {
return <div>Loading...</div>
}
Loadable({
loader: () =>import('./WillFailToLoad'),
loading: Loading
});
为了使加载组件看起来更完美,你的组将将会接受多个不同的props参数。
Loading的错误状态
当你需要loader的组件加载失败的时候,你的Loading组件将会接收到一个Error对象(如果成功了将会接收到一个null)
function Loading(props) {
if (props) {
return <div>Error! <button onclick={ props.retry }>Retry</button></div>
} else {
return <div>Loading...</div>
}
}
避免Loading组件闪屏
有时组件加载真的很快(<200ms),这时loading字样将会很快的在屏幕上一闪而过。
用户研究表明,这样会让用户感觉加载时间比实际要用的时间长很多。但是如果你一开始什么都不展示,反而会让用户觉得加载速度很好,体验很不错~
所以当你的组件的加载时间要比你设置的delay时间长的时候,你的Loading组件接收的props中会有一个pastDelay属性其值为true。
function Loading(props) {
if (props.error) {
return <div>Error!<button onclick={ props.retry }>{ Retry }</button></div>
} else if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null;
}
}
delay值默认为200ms,但是你可以喜欢设置成多少都可以。
Loadable ({
loader: () => import('./components/Bar'),
loading: Loading,
delay: 300
}) ;
loader加载超时
有的时候网络断开连接,失败或者永久性挂起的时候,这时候用户不知道是该再等一等,还是应该刷新重试。
当加载超时的时候loading组件会收到一个值为true的timeout属性。
function Loading(props) {
if (props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else if (props.timedOut) {
return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
} else if (props.pastDelay) {
return <div>Loading...</div>;
} else {
return null;
}
}
然而这个timeout属性默认是被禁止的,你可以传递一个timeout配置值给Loadable来开启这个属性
Loadable({
loader: () => import('./components/Bar');
loading: Loading,
timeout: 10000
});
定制化的render
Loadable会默认渲染你导出的模块,如果你想要改变这个默认的行为,你可以使用render option.
Loadable({
loader: () => import('./my-component'),
render(loaded, props) {
let Component = loaded.namedExport;
return <Component {...props}/>;
}
});
加载多个资源
从技术上来说,你可以通过loader()任何东西,只要它是一个promise对象并且你渲染了任何东西。但是这样写起来确实让人很烦。
这时你可以使用Loadable Map,它会使这一切简单很多。
Loadable.Map({
loader: {
Bar: () => import('./Bar'),
i18n: () => fetch('./i18n/bar.json').then(res => res.json())
},
render() {
let Bar = loaded.Bar.default;
let i18n = loaded.i18n;
return <Bar { ...props } i18n={ i18n } />;
}
});
当你使用Loadable.Map的时候,render()方法是必须要写的,它会给你的loader传递一个loaded参数,参数中是一个加载时的映射对象。
预编译
作为优化点,你可以在你的组件render之前决定它是否需要预加载。
例如:如果当一个按钮被点击的时候,你需要加载一个新的组件。你其实可以在用户鼠标悬停在按钮之上的时候,将这个组件预加载出来。
Loadble对外暴露出来了一个预加载的静态方法,我们可以通过这个方法来做成这个事情。
const LoadableBar = Loadable({
loader: () => import('./Bar'),
loading: Loading,
});
class MyComponent extends React.Component {
state = { showBar: false };
onClick = () => {
this.setState({ showBar: true });
};
onMouseOver = () => {
LoadableBar.preload();
};
render() {
return (
<div>
<button
onClick={this.onClick}
onMouseOver={this.onMouseOver}>
Show Bar
</button>
{this.state.showBar && <LoadableBar/>}
</div>
)
}
}
Server-Side Rendering
当你准备render所有那些需要动态加载的组件的时候,你将会得到满屏的loading提示。
这是一个很恶心的体验,但是好消息是React Loadable设计之初就是支持服务端渲染的。
现在我们就使用express开启一个服务:
import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './components/App';
const app = express();
app.get('/', (req, res) => {
res.send(`
<!doctype html>
<html>
<head>...</head>
<body>
<div id="app">${ReactDOMServer.renderToString(<App/>)}</div>
<script src="/dist/main.js"></script>
</body>
</html>
`);
});
在服务端预加载你的所有loadable组件
通过服务端渲染正确内容的第一步就是确保当你要渲染组件到页面的时候,你的所有loadable组件都已经加载完了。
可以使用 Loadable.preloadAll 方法来确定这件事情,当你所有的组件加载完成之后,它会返回一个执行成功的promise对象。
Loadable.preloadAll().then(() => {
app.listen(3000, () => {
console.log('Running on http://localhost:3000/');
});
});
在客户端拿到服务端的渲染状态
这个地方稍微有一点棘手,让我们先提前做好准备。
为了能让我们的客户端连接服务端,我们需要在服务端写一份相同的代码。
做这个首先我们需要通过loadable组件告诉我们哪一个模块正在被渲染。
定义一下哪个模块正在被加载
这里有两个配置: Loadable和Loadable.Map他们使用opts.modules和opts.webpack这两个参数告诉我们哪个模块正在尝试被加载。
Loadable({
loader: () => import('./Bar'),
modules: ['./Bar'],
webpack: () => [require.resolveWeak('./Bar')]
});
而且我们不需要对这两个参数太操心,因为React Loadable中有Babel插件帮助我们解决这个问题。
我们只需要将 react-loadable/babel配置添加到你的Babel配置文件中:
{
"plugins": {
"react-loadable/babel"
}
}
这样这两个参数就会自动的被支持。
找出哪个动态模块正在被渲染
接下来,我们就需要找出来,当我们请求打过来的时候,哪一个模块需要被渲染。
为了完成这个,有一个Loadable.Capture的组件可以用,它可以捕获所有被渲染的组件。
import Loadable from 'react-loadable';
app.get('/', (req, res) => {
let modules = [];
let html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<App/>
</Loadable.Capture>
);
console.log(modules);
res.send(`...${html}...`);
});
将映射文件加载到打包文件上面
为了保证客户端渲染的所有模块都是服务端渲染的,我们需要将其映射文件对应到webpack创建的打包文件上。
这包含两部分
第一,我们需要webpack告诉我们那个模块需要那个打包文件,为此我们可以使用React Loadable Webpack 插件,在react-loadable/webpack中引入ReactLoadablePlugin,并将其写入到webpack的配置文件中。
传递一个filename属性,webpack会将我们的打包数据以JSON的形式写入这个文件中.
// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';
export default {
plugins: [
new ReactLoadablePlugin({
filename: './dist/react-loadable.json',
}),
]
}
然后,我们回到服务端,使用这个数据转化为我们打包模块的数据。
为了将模块数据转化成打包数据,需要从webpack中的'react-loadable/webpack'引入getBundles方法。
import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
import stats from './dist/react-loadable.json';
app.get('/', (req, res) => {
let html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<App/>
</Loadable.Capture>
);
let bundles = getBundles(stats, modules);
// ...
});
我们可以在我们的html文件中使用<script>标签引用那些打包好的模块。
很重要的一点就是这些模块一定要要在主模块之前被引入,所以我就可以在浏览器上渲染出同样的效果啦。
而且,webpack的manifest(包含了解释模块的逻辑)存在于主模块中,他需要自己找出自己对应的模块。
这对于CommonsChunkPlugin这个插件来说很容易做到。
// webpack.config.js
export default {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
})
]
}
注意:在webpack4中,CommonChunkPlugin被移除,manifest不在需要被提取。
let bundles = getBundles(stats, modules);
res.send(`
<!doctype html>
<html lang="en">
<head>...</head>
<body>
<div id="app">${html}</div>
<script src="/dist/manifest.js"></script>
${bundles.map(bundle => {
return `<script src="/dist/${bundle.file}"></script>`
// alternatively if you are using publicPath option in webpack config
// you can use the publicPath value from bundle, e.g:
// return `<script src="${bundle.publicPath}"></script>`
}).join('\n')}
<script src="/dist/main.js"></script>
</body>
</html>
`);
在客户端上预加载那些可被加载的组件
我们可以在客户端上使用Loadable.preloadReady()方法,在页面上来预加载那些可被加载的组件。
// src/entry.js
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App';
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(<App/>, document.getElementById('app'));
})
API文档
Loadable
一个可以在渲染之前动态加载组件的的高阶组件,并且可以在加载的模块不可用时加载一个loading组件。
const LoadableComponent = Loadable({
loader: () => import('./Bar'),
loading: Loading,
delay: 200,
timeout: 10000,
});
Loadable会返回一个LoadableComponent对象。
Loadable.Map
一个允许你在同时加载多个资源的高阶组件
Loadable.Map的loader属性接收一个属性为方法的对象,并且需要一个render方法.
Loadable.Map({
loader: {
Bar: () => import('./Bar'),
i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
},
render(loaded, props) {
let Bar = loaded.Bar.default;
let i18n = loaded.i18n;
return <Bar {...props} i18n={i18n}/>;
}
});
使用Loadable.Map的render方法时,组件的参数是一个和loader配置相同的一个对象。
Loadable和Loadable.Map 配置
opts.loader:
一个返回加载对应组件的promise对象
Loadable({
loader: () => import('./Bar'),
});
当使用Loadable.Map时接收一个属性全部为方法的对象。
Loadable.Map({
loader: {
Bar: () => import('./Bar'),
i18n: () => fetch('./i18n/bar.json').then(res => res.json()),
},
});
当使用Loadable.Map时render参数是必须要有的
opts.loading
当加载模块加载时或者失败时加载一个loadingComponent组件
Loadable({
loading: LoadingComponent,
});
如果你什么都不想显示的话,你只需要让loading方法返回null就可以啦
Loadable({
loading: () => null,
});
opts.delay
可以设置需要多少毫秒延迟之后才让你的loading样式消失,设置这个参数后,就会传递给loading组件一个props.pastDelay参数
Loadable({
delay: 200
});
opts.timeout
设置的时间一到,就会给你的loading组件传递一个props.timeout参数。这个参数默认是关闭的。
Loadable({
timeout: 10000
});
opts.render
对需要加载的模块可以进行自定义的渲染的方法。
接收一个包含opts.loader的值的loaded对象和传递给LoadableComponent的props值。
Loadable({
render(loaded, props) {
let Component = loaded.default;
return <Component {...props}/>
}
});
opts.webpack
一个非必须的方法,这个方法返回一个包含webpack打包模块id的数组,这个id你可以通过require.resolveWeak
Loadable({
loader: () => import(./Foo),
webpack: () => [require.resolveWeak('./Foo')]
});
这个配置可以通过Babel插件自动适配。
opts.modules
可选参数,带有模块导入路径的数组
Loadable({
loader: () => import('./my-component'),
modules: ['./my-component']
});
这个配置可以通过Babel插件自动适配。
LoadableComponent
LoadableComponent是通过Loadable和Loadable.Map返回出来的组件对象。
const LoadableComponent = Loadable({
// ...
});
传递给这个组件的props,会通过render函数传递给那些动态加载的组件。
LoadableComponent.preload()
这是LoadableComponent上的静态方法,,它可以用来提前加载组件。
const LoadableComponent = Loadable({...});
LoadableComponent.preload();
这个方法返回执行返回的是一个promise对象,但是不最好不要等promise执行成功后再去更新你的UI,因为大部分的用例表明这是一个用户体验非常不好的应用。
LoadingComponent
这是一个你传递给opts.loading参数的组件。
function LoadingComponent(props) {
if (props.error) {
// When the loader has errored
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else if (props.timedOut) {
// When the loader has taken longer than the timeout
return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;
} else if (props.pastDelay) {
// When the loader has taken longer than the delay
return <div>Loading...</div>;
} else {
// When the loader has just started
return null;
}
}
Loadable({
loading: LoadingComponent,
});
props.error
当加载模块失败的时候会传递给LoadingComponent一个Error对象,当加载没有发生错误的时候会传递给组件一个null对象。
function LoadingComponent (props) {
if (props.error) {
return <div>Error!</div>
} else {
<div>Loading...</div>
}
}
props.retry
这个属性是一个方法属性,当组件加载失败的时候传递给LoadingComponent组件,使用这个方法可以尝试重新加载组件。
function LoadingComponent (props) {
if (props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>
} else {
return <div>Loading...</div>
}
}
props.timeOut
timeOut是一个类型是布尔值的类型,当组件加载超时的时候,传递给LoadingComponent组件。
function LoadingComponent (props) {
if (props.timeOut) {
return <div>Tabking a long time</div>
} else {
return <div>Loading...</div>
}
}
props.pastDelay
当给LoadingComponent设置了delay属性后,会给组件传递一个pastDelay参数。
function LoadingComponent (props) {
if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null;
}
}
Loadable.preloadAll()
这个方法会递归调用你的所有的LoadableComponent.preload方法,直到他们的状态都变为resolved。允许你预加载所有的动态模块在服务端这样的环境中。
Loadable.preloadAll().then(() => {
app.listen(3000, () => {
console.log('Running on http://localhost:3000/');
});
});
需要注意的是,这需要你在模块初始化的时候就定义了你的loadable组件,而不是在你的程序已经开始渲染的时候。
正确:
// During module initialization...
const LoadableComponent = Loadable({...});
class MyComponent extends React.component {
componentDidMount() {
// ...
}
}
错误:
class MyComponent extends React.Component {
componentDidMount() {
// During app render ...
const LoadableComponent = Loadable({...});
}
}
注意:如果你的程序中有多个react-loadable副本,那么Loadable.preloadAll()将不会执行
Loadable.preloadReady()
检查在浏览器上模块是否已经加载完成了,这个方法和LoadableComponent.preload方法是对应的。
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(<App />, document.getElementById('app'));
});
Loadable.Capture
一个可以告知那个模块被渲染的组件。
接收一个report参数,这个参数方法可以拿到每一个经由React Loadable渲染的模块的名字。
let modules = [];
let html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<App />
</Loadable.Capture>
);
console.log(modules);
Babel Plugin
当我们给每一个loadable组件加上opts.webpack和opts.modules配置时,我们往往需要手动的去配置一些麻烦的配置。
但是你可以通过添加babel插件,让插件帮你自动去完成那些繁琐的操作。
{
"plugins": ["react-loadable/babel"]
}
Input
import Loadable from 'react-loadable';
const LoadableMyComponent = Loadable({
loader: () => import('./MyComponent'),
});
const LoadableComponents = Loadable.Map({
loader: {
One: () => import('./One'),
Two: () => import('./Two'),
},
});
Output
import Loadable from 'react-loadable';
import path from 'path';
const LoadableMyComponent = Loadable({
loader: () => import('./MyComponent'),
webpack: () => [require.resolveWeak('./MyComponent')],
modules: [path.join(__dirname, './MyComponent')],
});
const LoadableComponents = Loadable.Map({
loader: {
One: () => import('./One'),
Two: () => import('./Two'),
},
webpack: () => [require.resolveWeak('./One'), require.resolveWeak('./Two')],
modules: [path.join(__dirname, './One'), path.join(__dirname, './Two')],
});
Webpack Plugin
为了能够在服务端渲染的时候使用正确的打包文件,你需要使用React Loadable webpack Plugin提供的映射功能。
// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';
export default {
plugins: [
new ReactLoadablePlugin({
filename: './dist/react-loadable.json'
}),
],
}
上述代码操作将会引入一个映射文件用来找出模块对应的打包文件。
getBundles
通过react-loadable/webpack获得的一个可以将模块转换成打包之后模块的方法。
import { getBundles } from 'react-loadable/webpack';
let bundles = getBundles(stats, moudles);
常被问到的问题
怎样避免重复定义
当我们使用Loadable()时可能会加载相同的loading组件和delay值。为了解决这个繁琐的操作,你可以用你自己的高阶组件将Loadable包一层,然后手动给它传递一个参数。
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
export default function MyLoadable(opts) {
return Loadable(Object.assign({
loading: Loading,
delay: 200,
timeout: 10000,
}, opts));
};
这时候你就只需要自己设置一个loader属性啦。
import MyLoadable from './MyLoadable';
const LoadableMyComponent = MyLoadable({
loader: () => import('./MyComponent'),
});
export default class App extends React.Component {
render() {
return <LoadableMyComponent/>;
}
}
不幸的是,当使用高阶组将将Loadable包裹起来后,react-loadable/babel就会失效,这时你就需要手动的设置modules和webpack值。
import MyLoadable from './MyLoadable';
const LoadableMyComponent = MyLoadable({
loader: () => import('./MyComponent'),
modules: ['./MyComponent'],
webpack: () => [require.resolveWeak('./MyComponent')],
});
export default class App extends React.Component {
render() {
return <LoadableMyComponent/>;
}
}
如何在服务端渲染的时候使用例如.css或.map类型的资源
当你调用getBundles方法的时候,除了会返回你的JS代码依赖的webpack配偶之外,还会返回文件类型。
这样,你就可以筛选出你想要的文件类型。
let bundles = getBundles(stats, modules);
let styles = bundles.filter(bundle => bundle.file.endsWith('.css'));
let scripts = bundles.filter(bundle => bundle.file.endsWith('.js'));
res.send(`
<!doctype html>
<html lang="en">
<head>
...
${styles.map(style => {
return `<link href="/dist/${style.file}" rel="stylesheet"/>`
}).join('\n')}
</head>
<body>
<div id="app">${html}</div>
<script src="/dist/main.js"></script>
${scripts.map(script => {
return `<script src="/dist/${script.file}"></script>`
}).join('\n')}
</body>
</html>
`);
网友评论