痛点
首屏加载缓慢,目前的体积包过于庞大
解决思路
将目前的bundle进行拆包,主要的方式为两种:
- 路由懒加载
- 大体积module懒加载
方式
经过查看,目前市面上路由懒加载使用的工具有如下:
- require.ensure
- import()
- lazyload-loader
- react.lazy
- bundle-loader
- react-loadable
传送门
webpack异步加载的原理
Webpack实现路由懒加载的三种方式
基于webpack4+react 的js懒加载
webpack v3 结合 react-router v4 做 dynamic import — 按需加载(懒加载)
react-loadable路由懒加载
lazy
使用方式
修改页面类导出的方式
之前:
export {ProductDetail as ProductDetail} from './detail/product-detail';
之后:
const LazyProductDetail = lazy(() => import('./detail/product-detail').then(mod => mod.ProductDetail));
class ProductDetail extends Component {
render() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyProductDetail {...this.props}/>
</Suspense>
</div>
);
}
}
export {
ProductDetail
};
PS: 此外,react-loadable和react lazy使用方式基本一致
涉及修改内容
- web页面导出需修改导出方式
- 该种导出方式和react native不兼容
经过测试发现,目前React.lazy并不支持除default外的导出支持。遂后续采用的是react-loadable。
react-loadable+loader
使用方式
编写自定义webpack loader,通过解析每个模块导出页面的index.js文件,转换为lazy导出方式
导出方式如下:
export {ProductHome as ProductHome} from './product-home';
export {ProductItem as ProductItem} from './product-item';
export {ProductList as ProductList} from './product-list';
export {ProductDetail as ProductDetail} from './detail/product-detail';
export {IntelligentCustomer as IntelligentCustomer} from './detail/intelligent-customer';
export {TransferDetail as TransferDetail} from './detail/transfer-detail';
export {ProductListItem as ProductListItem} from './product-list-item';
export {IntentBuy as IntentBuy} from './detail/intent-buy';
loader代码:
let source = `
export {ProductHome as ProductHome1} from './product-home';
export { Test1 } from './test1';
export {ProductHome as ProductHome, ProTest as ProTest } from './product-home';
export { Test21, Test22 } from './test2'
export * from './test3';
export default Test3;
import { Test4 } from './test4';
import Test5 from './test5';
`;
function generate(source) {
let reg = /export[ ]{1,}\{(.*)\}[ ]{1,}from[ ]{1,}(.*)[;]{0,1}/;
let exportClassList = [];
let outsource = source.split('\n').filter(item => item.trim()).map(item => {
let out = item.match(reg);
if(out && out.length > 1 && out[1].indexOf(',') == -1) {
let classNameList = out[1].split(' as ');
let originClassName = classNameList[0].trim();
let exportClassName = classNameList.length > 1 ? classNameList[1].trim() : originClassName;
let path = out[2].replace(';', '').trim();
exportClassList.push(exportClassName);
return `
const ${exportClassName} = Loadable({
loader: () => import(${path}),
loading() {
return <div></div>
},
render(loaded, props) {
let Component = loaded.${originClassName};
return <Component {...props}/>;
}
});
`;
return `export {${originClassName} as ${exportClassName}} from ${path};`
} else {
return item;
}
}).join('\n');
return `
import React, { Component } from 'react';
import Loadable from 'react-loadable';
${outsource}
export {
${exportClassList.join(',')}
}
`;
return `${outsource}`
}
module.exports = function (content) {
return generate(content);
};
补充知识点
涉及修改内容
- 需规范我们的页面导出方式
- 配置webpack loader
bundle-loader
使用方式
- webpack修改配置,loader增加bundle-loader
module.exports = {
module: {
rules: [
{
test: /\.bundle\.js$/,
use: 'bundle-loader'
}
]
}
}
本质上是将匹配的文件包裹require.ensure, 代码如下:
if(query.lazy) {
result = [
"module.exports = function(cb) {\n",
" require.ensure([], function(require) {\n",
" cb(require(", loaderUtils.stringifyRequest(this, "!!" + remainingRequest), "));\n",
" }" + chunkNameParam + ");\n",
"}"];
}
此时,已不支持如下导出方式
export {ProductDetail as ProductDetail} from './detail/product-detail';
- 提供支持懒加载包裹的组件,如下:
class Bundle extends React.Component {
constructor(props){
super(props);
this.state = {
// 默认为空
mod: null
}
console.log("xxxxxx", props.load);
}
componentWillMount() {
// 加载初始状态
this.load(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps);
}
}
load(props) {
// 重置状态
this.setState({
mod: null
});
// 传入组件的组件
props.load((mod) => {
this.setState({
mod: mod.default ? mod.default : mod
});
});
}
render() {
// 不为空,则渲染传入的子组件函数
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
- 修改路由声明
<Route key={config.path} path={config.path} render={(props) => {
return <Bundle load={config.component}>
{
Comp => {
if (Comp) {
let C = Inject(Comp, config.options);
return <C {...props} {...parentState.query} title={config.options.title}/>
} else {
return <div>Loading...</div>
}
}
}
</Bundle>
}}exact/>
PS: 此外,require.ensure、import()和bundle-loader使用基本一致
涉及修改内容
- 页面导出在路由文件中按文件路径进行引用
- webpack需配置懒加载的文件项
- cp库支持懒加载路由