basic points
- 基于路由的代码分割和基于组件的代码分割
webpack可以设置多入口文件,可以通过入口文件进行代码分割,即所谓的基于路由的分割;基于路由的分割可以常见于多tab页切换等场景;
但是由于即使是多入口文件,某个文件中的代码也未必是需要首屏全部加载的,比如弹窗等;如果可以基于组件进行代码分割,可以更加细粒度地控制首屏需要加载的代码及其他代码的加载时机,可以有效提高页面性能和用户体验;
- 考虑自己实现一个动态载入的组件,需要考虑哪些问题
-
需要基于
ES6 新特性import() 实现,然后通过states 和生命周期函数共同控制组件的可见性及展示内容,直接看官方demo:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class MyComponent extends React.Component {
state = {
Bar: null
};
componentWillMount() {
import('./components/Bar').then(Bar => {
this.setState({ Bar: Bar.default });
});
}
render() {
let {Bar} = this.state;
if (!Bar) {
return <div>Loading...</div>;
} else {
return <Bar/>;
};
}
}
真实场景考虑的问题会更加复杂:比如动态加载失败
import() ,比如SSR 怎么处理,比如加载完成前的站位组件不可能只是单纯的静态文本Loading... 等; -
React Loadable 是一个轻量级的封装库,可以实现基于组件的代码分割功能。
看下
-
使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import React from "react";
import "./styles.css";
import Loadable from "react-loadable";
const LoadableSub = Loadable({
loader: () => import("./Sub"),
loading: () => {
return <div>Loading...</div>;
}
});
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<LoadableSub />
</div>
);
}
``` -
Loadable 实质是一个高阶组件,形式为1
2
3
4Loadable({
loader: () => import('xxx'), // component to be loaded dynamically.
loading: <HoldingComponent /> // 预占位组件,动态组件未加载时显示loading信息等
}) -
构建高可用的
Loading 组件——错误处理、避免占位信息和有效信息展示交替时的闪烁、加载超时、预加载、同时加载多个资源等;-
loader 加载失败处理:失败时loading 会接收到一个error 的prop 对象,否则为null ;1
2
3
4
5
6
7function Loading(props) {
if (props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else {
return <dxiv>Loading...</div>;
}
} -
Loadable 支持设置设置一个默认的延迟时间,超过该时间才会显示loading ;对应的Loadable 属性为delay 默认值为200ms ,超过则loading 的名为pastDelay 的prop 为true ;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Loadable({
loader: () => import('xxx')
loading: Loading,
delay: 300, // ms
});
const Loading = (props) => {
if (props.error) {
return <div>Woops! Error occured.<button onClick={props.retry}>Retry</button><div>
} else if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null;
}
}; -
loader 因网络问题等加载失败处理——Loadable 支持设置timeout ,默认被禁用;对应在loading 组件中可以通过props.timeout 属性获取状态,超时为true ;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const LoadableSub = Loadable({
loader: () => import("./Sub"),
loading: Loading,
delay: 500,
timeout: 8000
});
const Loading = props => {
if (props.error) {
return <div>Woops, errors occurred!</div>;
} else if (props.pastDelay) {
return <div>Loading...</div>;
} else if (props.timeOut) {
return <div>Timeout, please retry</div>;
} else {
return null;
}
}; -
动态加载多个资源,可以使用
Loadable.map({}) ; -
Loadable 支持预加载,单个组件的预加载使用方法类似于LoadableSub.preload() ; -
客户端进行动态加载,可能会出现白屏等,所以如果
SSR 时能够对动态加载做一定的处理,那么可以优化该体验;This really sucks, but the good news is that React Loadable is designed to make server-side rendering work as if nothing is being loaded dynamically.
-
-
服务端渲染
-
确保渲染前对应的组件已加载就绪。预加载所有
loadable component ——使用Loadable.preloadAll ,它返回一个promise 对象,并且在所有loadable component 就绪后resolve ;1
2
3
4
5Loadable.preloadAll().then(() => {
app.listen(3000, () => {
console.log('Running on http://localhost:3000/');
});
}); -
客户端如何处理
SSR APP
a. 声明需要加载的模块,使用Loadable 的opt.modules 和webpack ;另外,需要在babel 配置中加入以下配置信息1
2
3
4
5
6
7
8
9
10
11
12Loadable({
loader: () => import('./Bar'),
modules: ['./Bar'],
webpack: () => [require.resolveWeak('./Bar')],
});
// babel配置
{
"plugins": [
"react-loadable/babel"
]
}b. 确定当某个请求完成后,哪些组件会被渲染;
Loadable.Capture 应运而生,可以记录已渲染的模块;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './App';
import Loadable from 'react-loadable';
const app = express();
app.get('/', (req, res) => {
let modules = [];
let html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.push(moduleName)>
<App/>
</Loadable.Capture>
);
console.log({ modules });
res.end(`${html}`);
});c. 把已加载的
modules 映射为webpack bundles ——为确保客户端能够加载到所有服务端渲染的modules ,需要将其转为webpack 创建的bundles ;包括两步:首先,需要借助
webpack 的React Loadable Webpack plugin 告知我们每个module 位于哪个bundle 中;react-loadable.json 中存储了bundles 的相关信息;1
2
3
4
5
6
7
8
9
10// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack';
export default {
plugins: [
new ReactLoadablePlugin({
filename: './dist/react-loadable.json',
}),
],
};然后我们回到服务端使用这些数据将
modules 转为bundles ;这里的转换需要借助react-loadable/webpack 中的getBundles 方法;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
import stats from './dist/react-loadable.json';
app.get('/', (req, res) => {
let modules = [];
let html = ReactDOMServer.renderToString(
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
<App/>
</Loadable.Capture>
);
let bundles = getBundles(stats, modules); // 获取到对应的bundles
});然后我们可以在html标签中使用
标签渲染这些 bundles ;保持这些动态加载对应的包在html中的引入位置先于主包之前引入,从而才能保证这些
bundles 能够在App 渲染前被加载;
然而,由于webpack manifest (包括了bundles 的解析逻辑)文件位于主包中,因此需要把它提出到一个单独的chunk ;可以借助CommonsChunkPlugin 实现:1
2
3
4
5
6
7
8
9// webpack.config.js
export default {
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
})
]
}注意: 由于
webpack 4 中的CommonsChunkPlugin 已经被移除,manifest 文件不需要再单独提取;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20let 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>
<script src="/dist/main.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>window.main();</script>
</body>
</html>
`);d. 客户端预加载就绪的
loadable component
我们可以在客户端使用Loadable.preloadReady() 或Loadable.preloadAll() 预加载页面中包含的loadable component ;1
2
3
4
5
6
7
8
9
10
11// src/entry.js
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App';
window.main = () => {
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(<App/>, document.getElementById('app'));
});
};
-
Now server-side rendering should work perfectly!
参考文献
- jamiebuilds/react-loadable github