代码分割与异步加载
代码分割与异步加载
异步加载组件
React 16 中的异常处理翻译自React 官方文档,从属于笔者的React 与前端工程化实践系列中的React 组件分割与解耦章节;也可以使用 create-webpack-app运行本部分示例。
为了方便进行代码分割与异步加载,
new webpack.optimize.CommonsChunkPlugin(options)
options.name
or options.names
( string|string[]
): 默认的公共Chunk 的名称,也可以传入一个已经在entry 中设置过的Chunk ,这样就会默认把该Chunk 作为公共Chunk 。如果留空,或者设置了options.async
或者options.children
那么默认所有的Chunks 都会被抽取公共文件( 即使用随机文件名) ,否则使用options.filename
作为这个公共Chunk 的名称。options.filename
( string
): 输出的公共文件的文件名模板,可以使用output.filename
或者output.chunkFilename
来作为占位符。options.minChunks
( number|Infinity|function(module, count) -> boolean
): 当进行Chunk 抽取时候的最小单元,这个值必须大于或者等于2 ,或者不小于Chunks 的数目。如果使用Infinity
即是自动创建Commons Chunk ,但是不会传入其他模块的内容,可以用于在设定Vendor Chunks 的时候避免污染。options.chunks
(string[]`): 根据Chunk 的名称选定需要处理的Chunk 列表,这些Chunk 必须是Commons Chunk 的子Chunk ,如果忽略的话默认是全部的Chunk 。options.children
( boolean
): 如果为true 则选定所有Common Chunk 的子Chunk 。options.async
( boolean|string
): 设定为真之后默认开启异步加载模式。options.minSize
( number
): 在Common Chunk 创建之前所需要的最小的大小,避免文件太小了还要创建Common Chunk 。
注意,code split
是不支持
Multiple Entries Common Chunk( 多个入口情况下公共Chunk)
创建一个额外的公共模块,包含
name: "commons",
// (the commons chunk name)
filename: "commons.js",
// (the filename of the commons chunk)
// minChunks: 3,
// (Modules must be shared between 3 entries)
// chunks: ["pageA", "pageB"],
// (Only use these entries)
})
下面看一个复杂一点的例子:
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3",
},
output: {
filename: "[name].entry.chunk.js",
},
plugins: [new CommonsChunkPlugin("commons.chunk.js")],
};
这种配置下会编译出多个单独的入口块p1.entry.chunk.js
p2.entry.chunk.js
p3.entry.chunk.js
commons.chunk.js
commons.chunk.js
xx.entry.chunk.js
。也可以创建多个公共代码块:
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3",
ap1: "./admin/page1",
ap2: "./admin/page2"
},
output: {
filename: "[name].js"
},
plugins: [
new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
]
};
// <script>s required:
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js
分割第三方库与分割公共模块的区别在与需要设置
vendor: ["react", "other-lib"],
//或者 vendor:"./vendor.js",然后在vendor.js中使用require("react")来指定公共库
app: "./entry"
}
new CommonsChunkPlugin({
name: "vendor",
// filename: "vendor.js"
// (Give the chunk a different name)
minChunks: Infinity,
// (with more entries, this ensures that no other module
//goes into the vendor chunk)
})
这样打包之后就会多出一个 vendor.js
文件,之后在引入我们自己的代码之前,都要先引入这个文件。比如在 index.html
中
<script src="app.js" charset="utf-8"></script>
除了这种方式之外,还可以通过引用外部文件的方式引入第三方库,比如像下面的配置
{
externals: {
'react': 'React'
}
}
externals
对象的require
时用的,比如 require('react')
,对象的window.React
。这时候 index.html
就变成下面这样
<script src="//cdn.bootcss.com/react/0.14.7/react.min.js"></script>
<script src="/build/bundle.js"></script>
Async Chunk( 异步代码块)
一般加载一个网页都会把全部的
- src/Components/Button.scss
.button {
background: tomato;
color: white;
}
- src/Components/Button.html
<a class="button" href="{{link}}">{{text}}</a>
- src/Components/Button.js
import $ from "jquery";
import template from "./Button.html";
import Mustache from "mustache";
import "./Button.scss";
export default class Button {
constructor(link) {
this.link = link;
}
onClick(event) {
event.preventDefault();
alert(this.link);
}
render(node) {
const text = $(node).text(); // Render our button
$(node).html(Mustache.render(template, { text })); // Attach our listeners
$(".button").click(this.onClick.bind(this));
}
}
按钮最终呈现的样式如下所示:

在主模块中,这个
import $ from "jquery";
// This is a split point
require.ensure([], () => {
// All the code in here, and everything that is imported
// will be in a separate file
const library = require("some-big-library");
$("foo").click(() => library.doSomething());
});
所有在require.ensure
中定义的文件会被切分为多个大的独立分块,这些独立的分块会在需要被调用时被使用
bundle.js
|- jquery.js
|- index.js // our main file
chunk1.js
|- some-big-libray.js
|- index-chunk.js // the code in the callback
当然,开发者并不需要手动导入
src/index.js
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button');
const button = new Button('google.com');
button.render('a');
});
}
如果在编译时候使用如下参数:--display-chunks
,那么可以查看具体的被打包的情况:
$ webpack --display-modules --display-chunks
Hash: 432341dc518c06c9d8da
Version: webpack 1.12.2
Time: 952ms
Asset Size Chunks Chunk Names
bundle.js 3.88 kB 0 [emitted] main
1.bundle.js 287 kB 1 [emitted]
chunk {0} bundle.js (main) 294 bytes [rendered]
[0] ./src/index.js 294 bytes {0} [built]
chunk {1} 1.bundle.js 278 kB {0} [rendered]
[1] ./src/Components/Button.js 2.02 kB {1} [built]
[2] ./~/jquery/dist/jquery.js 248 kB {1} [built]
[3] ./src/Components/Button.html 72 bytes {1} [built]
[4] ./~/mustache/mustache.js 19.3 kB {1} [built]
[5] ./src/Components/Button.scss 1.05 kB {1} [built]
[6] ./~/css-loader!./~/sass-loader!./src/Components/Button.scss 212 bytes {1} [built]
[7] ./~/css-loader/lib/css-base.js 1.51 kB {1} [built]
[8] ./~/style-loader/addStyles.js 6.09 kB {1} [built]
如上所述,入口文件bundle.js
中只会包含部分

Chunk Limit
当编写代码时,我们可能会自己加入很多的代码分割点来实现这样一种代码的按需加载,每一个小的代码文件就会被称为一个
--optimize-max-chunks 15
或者new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})
--optimize-min-chunk-size 10000
或者 new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})
异步加载模式

bundle-loader
// 当请求某个Bundle时,Webpack会为我们自动加载
var waitForChunk = require("bundle-loader!./file.js");
//我们需要等待Chunk加载完成才能获取到文件详情
waitForChunk(function(file) {
// use file like is was required with
// var file = require("./file.js");
});
// wraps the require in a require.ensure block
我们同样可以自定义
require("bundle-loader?lazy&name=my-chunk!./file.js");
我们可以很方便地利用
import HomePage from "./pages/HomePage";
import AdminPage from "./pages/admin/AdminPage";
import AdminPageSettings from "./pages/admin/AdminPageSettings";
export default function routes(fromServer) {
return (
<Router history={browserHistory}>
<Route path="/" component={HomePage}/>
<Route path="/admin" component={AdminPage}/>
<Route path="/admin/settings" component={AdminSettingsPage}/>
<Router/>
)
}
其中/admin
这个地址时才会加载相关组件,此时我们就可以在
{
...
module: {
loaders: [{
// use `test` to split a single file
// or `include` to split a whole folder
test: /.*/,
include: [path.resolve(__dirname, 'pages/admin')],
loader: 'bundle?lazy&name=admin'
}]
}
...
}
该配置会自动帮我们从主文件中移除1.admin.js
文件中,然后在
import HomePage from "./pages/HomePage";
import AdminPage from "./pages/admin/AdminPage";
import AdminPageSettings from "./pages/admin/AdminPageSettings";
const isReactComponent = (obj) => Boolean(obj && obj.prototype && Boolean(obj.prototype.isReactComponent));
const component = (component) => {
return isReactComponent(component)
? {component}
: {getComponent: (loc, cb)=> component(
comp=> cb(null, comp.default || comp))}
};
export default function routes(fromServer) {
return (
<Router history={browserHistory}>
<Route path="/" {...component(HomePage)}/>
<Route path="/admin" {...component(AdminPage)}/>
<Route path="/admin/settings"
{...component(AdminSettingsPage)}/>
<Router/>
)
}
React 懒加载组件封装
有时候我们需要将某个厚重的组件设置为异步加载,这里我们将常见的懒加载操作封装为某个组件及其高阶组件接口,源代码参考LazilyLoad:
import React from "react";
/**
* @function 支持异步加载的封装组件
*/
class LazilyLoad extends React.Component {
constructor() {
super(...arguments);
this.state = {
isLoaded: false,
};
}
componentWillMount() {
this.load(this.props);
}
componentDidMount() {
this._isMounted = true;
}
componentWillReceiveProps(next) {
if (next.modules === this.props.modules) return null;
this.load(next);
}
componentWillUnmount() {
this._isMounted = false;
}
load(props) {
this.setState({
isLoaded: false,
});
const { modules } = props;
const keys = Object.keys(modules);
Promise.all(keys.map((key) => modules[key]()))
.then((values) =>
keys.reduce((agg, key, index) => {
agg[key] = values[index];
return agg;
}, {})
)
.then((result) => {
if (!this._isMounted) return null;
this.setState({ modules: result, isLoaded: true });
});
}
render() {
if (!this.state.isLoaded) return null;
return React.Children.only(this.props.children(this.state.modules));
}
}
LazilyLoad.propTypes = {
children: React.PropTypes.func.isRequired,
};
export const LazilyLoadFactory = (Component, modules) => {
return (props) => (
<LazilyLoad modules={modules}>
{(mods) => <Component {...mods} {...props} />}
</LazilyLoad>
);
};
export const importLazy = (promise) => promise.then((result) => result.default);
export default LazilyLoad;
回调方式懒加载
这里我们使用类似于importLazy
主要是为了兼容
render(){
return ...
<LazilyLoad modules={{
LoadedLate: () => importLazy(System.import('../lazy/loaded_late.js'))
}}>
{
({LoadedLate}) => {
return <LoadedLate />
}
}
</LazilyLoad>
...
}
高阶组件方式懒加载
在入门介绍中我们讲过可以利用
// @flow
import React, { Component, PropTypes } from 'react';
import { LazilyLoadFactory } from '../../../common/utils/load/lazily_load';
/**
* 组件LoadedJquery
*/
export default class LoadedJQuery extends Component {
/**
* @function 默认渲染函数
*/
render() {
return (
<div
ref={(ref) => this.props.$(ref).css('background-color', 'red')}>
jQuery加载完毕
</div>
);
}
}
export default LazilyLoadFactory(
LoadedJQuery,
{
$: () => System.import('jquery'),
}
);
这里我们将加载完毕的
AsyncComponent
// Usage:
//
// function loader() {
// return new Promise((resolve) => {
// if (process.env.LAZY_LOAD) {
// require.ensure([], (require) => {
// resolve(require('./SomeComponent').default);
// });
// }
// });
// }
// ...
// <AsyncComponent loader={loader} />
//
// In the future, loader() could be:
// const loader = () => import('./SomeComponent');
import React, { PropTypes } from "react";
import Spinner from "./Spinner";
import { withStyles, css } from "../themes/withStyles";
function DefaultPlaceholder({ height = 300, styles }) {
return (
<div {...css(styles.container, { height })}>
<Spinner />
</div>
);
}
const WrappedPlaceholder = withStyles(({ color }) => ({
container: {
backgroundColor: color.white,
},
}))(DefaultPlaceholder);
DefaultPlaceholder.propTypes = {
height: PropTypes.number,
styles: PropTypes.shape({
backgroundColor: PropTypes.string,
}),
};
export { WrappedPlaceholder };
export default class AsyncComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
Component: null,
};
}
componentDidMount() {
this.props.loader().then((Component) => {
this.setState({ Component });
});
}
render() {
const { Component } = this.state;
const { renderPlaceholder, placeholderHeight } = this.props;
if (Component) {
return <Component {...this.props} />;
}
return renderPlaceholder ? (
renderPlaceholder()
) : (
<WrappedPlaceholder height={placeholderHeight} />
);
}
}
AsyncComponent.propTypes = {
// specifically loader is a function that returns a promise. The promise
// should resolve to a renderable React component.
loader: PropTypes.func.isRequired,
placeholderHeight: PropTypes.number,
renderPlaceholder: PropTypes.func,
};
加载事件与图片懒加载
在真实的应用开发中用户体验是我们不可忽略的重要因素,特别是对于包含大量图片的网页,受限于onLoad
事件,其会在图片加载完毕之后被触发;我们可以先插入隐藏的img
标签以向服务端或者Feed
组件中,其核心功能就是展示用户上传的大图,我们希望在图片加载时给予用户加载中的提示,加载完毕之后再将图片渲染到界面上:
export default class Feed extends Component {
constructor(props) {
super(props);
this.state = { loadedItems: [] };
}
onLoad(feedItem) {
let updatedItems = this.state.loadedItems;
updatedItems.push({
name: feedItem.name,
imgPath: feedItem.imgPath,
});
this.setState({ loadedItems: updatedItems });
}
render() {
return (
<div className="feed">
<h1 className="feed__h1">{this.props.name}</h1>
{this.state.loadedItems.map((item, i) => (
<FeedItem
imgPath={item.imgPath}
name={item.name}
renderModal={this.props.renderModal}
key={i}
/>
))}
{this.props.items.length > this.state.loadedItems.length && (
<LoadingItem />
)}
<div className="hidden">
{this.props.items.map((item, i) => (
<img
src={item.imgPath}
onLoad={this.onLoad.bind(this, item)}
key={i}
/>
))}
</div>
</div>
);
}
}