React-router-v4 - Webpack 實作按需加載(code-splitting)
源碼
方法一、結合 bundle-loader
實作按需加載
bundle-loader
1. 首先建立一個包裝元件 Bundle
Bundle
一下是 react-router4.0 官方文檔中給出的例子
import React from 'react';
export default class Bundle extends React.Component {
state = {
mod: null
}
componentWillMount() {
this.load(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps);
}
}
// load 方法,用于更新 mod 狀态
load(props) {
// 初始化
this.setState({
mod: null
});
/*
調用傳入的 load 方法,并傳入一個回調函數
這個回調函數接收 在 load 方法内部異步擷取到的元件,并将其更新為 mod
*/
props.load(mod => {
this.setState({
mod: mod.default ? mod.default : mod
});
});
}
render() {
/*
将存在狀态中的 mod 元件作為參數傳遞給目前包裝元件的'子'
*/
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
- Bundle 元件會接受一個名為
的load
props
- load 值一是一個元件異步加載的方法
load -> function (cb) {...cb()}
- 這個方法需要傳入一個回調函數作為參數
- 回調函數會在在方法内異步接收加載完的元件
2. 建立包裝元件的方法
import React from 'react';
import Bundle from './Bundle';
// 預設加載元件,可以直接傳回 null
const Loading = () => <div>Loading...</div>;
/*
包裝方法,第一次調用後會傳回一個元件(函數式元件)
由于要将其作為路由下的元件,是以需要将 props 傳入
*/
const lazyLoad = loadComponent => props => (
<Bundle load={loadComponent}>
{Comp => (Comp ? <Comp {...props} /> : <Loading />)}
</Bundle>
);
export default lazyLoad;
結合第一步中加載後的元件會被傳入
this.props.children(this.state.mod)
中渲染
{Comp => (Comp ? <Comp {...props} /> : <Loading />)}
3. 在 Router 配置中使用
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { hot } from 'react-hot-loader';
// 引入包裝函數
import lazyLoad from './lazyLoad';
import App from '../containers/App';
/*
使用 bundle-loader 插件引用元件,傳回的實際上是包裝後的元件異步加載的函數
*/
import Home from 'bundle-loader?lazy&name=home!../containers/Home';
import Test from 'bundle-loader?lazy&name=test!../containers/Test';
const Root = () => (
<div>
<Switch>
<Route path="/" render={props => (
<App>
<Switch>
<!-- 包裝後作為路由映射的元件 -->
<Route path="/" exact component={lazyLoad(Home)} />
<Route path="/home" component={lazyLoad(Home)} />
<Route path="/test" component={lazyLoad(Test)} />
<Route render={() => <Redirect to="/" />} />
</Switch>
</App>
)}
/>
<Route render={() => <Redirect to="/" />} />
</Switch>
</div>
);
export default hot(module)(Root);
方法二、 bundle-loader
的另一種使用方式 - 在 webpack 中配置
bundle-loader
1. 建立一個包裝元件 Bundle
Bundle
建立的元件與 方法一 中完全一緻
2. 建立包裝元件的函數
函數與 方法一 中完全一緻
3. webpack 中配置 loader
由于不是所有檔案都是需要包裝的元件,是以這裡要做一個區分,就是 需要包裝的元件要寫為
js
[name].bundle.js
module.exports = {
module: {
rules: [
{
test: /\.bundle\.js$/,
loader: 'bundle-loader',
include: path.join(__dirname, 'src'), // 源碼目錄
options: {
lazy: true,
name: '[name]'
}
}
]
}
}
4. Router 配置中使用
import lazyLoad from './lazyLoad';
import App from '../containers/App';
/*
其他配置使用與 方法一 一緻
隻是引用元件的時候不在需要在路徑中配置 bundle-loader
需要按需加載的元件也要檔案名變為 [name].bundle.js 的格式
*/
// import Home from 'bundle-loader?lazy&name=home!../containers/Home';
// import Test from 'bundle-loader?lazy&name=test!../containers/Test';
import Home from '../containers/Home.bundle';
import Test from '../containers/Test.bundle';
...
方法三、使用 import()
方法代替 bundle-loader 實作
import()
import('../xxx.js')
傳回的是一個 promise,是以需要改寫 Bundle 元件,此外不在需要 bundle-loader ,其在 webpack 中的配置應該删除
1. 建立一個包裝元件 Bundle
Bundle
import React from 'react';
export default class Bundle extends React.Component {
state = {
mod: null
}
componentWillMount() {
this.load(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.load !== this.props.load) {
this.load(nextProps);
}
}
// 更改 load 方法為異步函數
async load(props) {
this.setState({
mod: null
});
/*
使用 props.load() 傳回的是一個 promise
*/
const mod = await props.load();
this.setState({
mod: mod.default ? mod.default : mod
});
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null;
}
}
2. 建立包裝元件的函數
函數與 方法一 中完全一緻
3. Router 配置中使用
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { hot } from 'react-hot-loader';
import lazyLoad from './lazyLoad';
import App from '../containers/App';
// 動态引入
const Home = lazyLoad(() => import('../containers/Home'));
const Test = lazyLoad(() => import('../containers/Test'));
const Root = () => (
<div>
<Switch>
<Route path="/" render={props => (
<App>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/home" component={Home} />
<Route path="/test" component={Test} />
<Route render={() => <Redirect to="/" />} />
</Switch>
</App>
)}
/>
<Route render={() => <Redirect to="/" />} />
</Switch>
</div>
);
export default hot(module)(Root);
問題
1. 雖然在配置 webpack 時設定了 output: {chunkFilename: 'chunk/[name].[chunkhash].js'}
但是使用 ‘方法三’ 拆分後的包名還是以數字開始的,如下
output: {chunkFilename: 'chunk/[name].[chunkhash].js'}
Time: ms
Asset Size Chunks Chunk Names
chunk/c8b.js kB [emitted]
chunk/.e5cc1c11d3.js kB [emitted]
static/js/app.f61c516b85.bundle.js kB [emitted] app
static/js/vendor.f5f812985.bundle.js kB [emitted] vendor
static/js/runtime.bbb4b5baf.bundle.js kB [emitted] runtime
static/css/app.ec6e1795a1.css kB [emitted] app
index.html kB [emitted]
可以使用 webpack 提供的一個特殊的注釋文法 來提供 chunk name (需要 Webpack > 2.4),這樣生成的子產品就能與 bundle-loader 一緻的命名
const Home = lazyLoad(() => import(/* webpackChunkName: "Home" */'../containers/Home'));
const Test = lazyLoad(() => import(/* webpackChunkName: "Test" */'../containers/Test'));
2. 而使用 bundle-loader 的方法,由于配置了參數 name ,是以打包後會有子產品命名如下
Time: ms
Asset Size Chunks Chunk Names
chunk/Home.a8c77815d.js kB [emitted] Home
chunk/Test.f60c.js kB [emitted] Test
static/js/app.edc8970d4.js kB [emitted] app
static/js/vendor.f5f812985.js kB [emitted] vendor
static/js/runtime.f777aeb81.js kB [emitted] runtime
static/css/app.ec6e1795a1.css kB [emitted] app
index.html kB [emitted]
參考
- react-router
- Create React App