react-router4代码分割
react-router4官方文档: https://reacttraining.com/react-router/web/guides/code-splitting
对于SPA项目,出于加载速度的考虑,肯定不能在页面初始化的时候加载全部js文件,因此需要将代码分块,也就是所谓的code splitting,我这里基于react-router4.1.1展示一个code split的例子,当然,你也可以参考react官方文档
思路解析
react-router4是对之前react-router的一次大改,按照官方的说法,是将路由的问题转变成了react组件的问题,所以在react-router4里面,不同于以往使用require.ensure,我们使用一些其他办法异步的请求组件的js文件。
方案A 使用bundle-loader
bundle-loader git地址:https://github.com/webpack-contrib/bundle-loader
先仿着官方的示例先写一个Bundle组件,简化了一下,大概会是这样的
{/*
// 调用示例
<Bundle load={require('bundle-loader?lazy!./somefile.js')}>
{(Cmp) => <Cmp></Cmp>}
</Bundle>
*/}
// Bundle.js
import React, { Component } from 'react'
class Bundle extends Component {
constructor() {
super()
this.state = {
mod: null
}
}
componentDidMount() {
this.props.load((mod) => {
this.setState({
mod: mod.default || mod
})
})
}
render() {
return (
this.state.mod ? this.props.children(this.state.mod) : null
)
}
}
export default Bundle
在被传入的load方法被调用的时候,相应的js文件才会被请求和加载
然后在入口的路由文件里面这样写(假设我们有两个组件,Cp1, Cp2)
import React from 'react'
import {
BrowserRouter,
Route
} from 'react-router-dom'
import Bundle from './bundle'
let CodeSplit = () => {
return (
<BrowserRouter>
<div>
<Route path={'/cp1'} render={() => {
return (<Bundle load={require('bundle-loader?lazy!./cp1')}>
{(Cp1) => <Cp1></Cp1>}
</Bundle>)
}
}></Route>
<Route path={'/cp2'} render={() => {
return (<Bundle load={require('bundle-loader?lazy!./cp2')}>
{(Cp2) => <Cp2></Cp2>}
</Bundle>)
}
}></Route>
</div>
</BrowserRouter>
)
}
export default CodeSplit
这样,代码分割就完成了。
注意
这里有一个小坑,如果你跟我一样使用的是create-react-app的话,你会发现,在运行代码的时候,会报这个错误
Line 35: Unexpected '!' in 'bundle-loader?lazy!./cp1'. Do not use import syntax to configure webpack loaders import/no-webpack-loader-syntax
Line 42: Unexpected '!' in 'bundle-loader?lazy!./cp2'. Do not use import syntax to configure webpack loaders import/no-webpack-loader-syntax
这是因为create-react-app不支持webpack-loader,具体的可以看看这个issue
why I can't use bundle-loader like this: https://github.com/facebookincubator/create-react-app/issues/2477
解决办法也很简单,采用方案B
方案B 使用import()
import() 属于es的一个proposal,也就是提案,还没有正式立项,所以具体会有什么问题我这里也不清楚,不过babel已经支持,所以我们这里可以尝试使用,将之前使用bundle-loader的例子改造一下
因为import返回一个promise,所以我们这里将componentDidMount变成一个async函数
{/*
// 调用方法
<Bundle load={() => import(./somefile.js)}></Bundle>
*/}
import React, { Component } from 'react'
class Bundle extends Component {
constructor() {
super()
this.state = {
mod: null
}
}
async componentDidMount() {
const {default: mod} = await this.props.load()
this.setState({
mod: mod.default || mod
})
}
render() {
return (
this.state.mod ? this.props.children(this.state.mod) : null
)
}
}
export default Bundle
然后在入口文件的路由里面这么用
import React from 'react'
import {
BrowserRouter,
Route
} from 'react-router-dom'
import Bundle from './bundle'
let CodeSplit = () => {
return (
<BrowserRouter>
<div>
<Route path={'/cp1'} render={() => {
return (<Bundle load={() => import('./cp1')}>
{(Cp1) => <Cp1></Cp1>}
</Bundle>)
}
}></Route>
<Route path={'/cp2'} render={() => {
return (<Bundle load={() => import('./cp2')}>
{(Cp2) => <Cp2></Cp2>}
</Bundle>)
}
}></Route>
</div>
</BrowserRouter>
)
}
export default CodeSplit
OK,一个大致的代码分割功能差不多就完成了
*************** 2017.12.7 更新 ****************
方案B改进版
方案B虽然实现了我们异步加载组件的需求,但是调用还是显得比较麻烦,我们需要一种更优雅的方式来实现异步加载,同时还希望能传递参数给组件和自定义组件在加载时候的显示效果,所以这里对方案B进一步进行封装
因为代码比较简单,所以我这里直接把我项目里的代码贴过来了
// async-component.js
/**
* 用于react router4 code splitting
*/
import React, {Component} from 'react'
/**
* @param {Function} loadComponent e.g: () => import('./component')
* @param {ReactNode} placeholder 未加载前的占位
*/
export default (loadComponent, placeholder = null) => {
class AsyncComponent extends Component {
unmount = false
constructor() {
super()
this.state = {
component: null
}
}
componentWillUnmount() {
this.unmount = true
}
async componentDidMount() {
const {default: component} = await loadComponent()
if(this.unmount) return
this.setState({
component: component
})
}
render() {
const C = this.state.component
return (
C ? <C {...this.props}></C> : placeholder
)
}
}
return AsyncComponent
}
整体思路和之前的代码是一致的
然后调用的时候只需这么写
Demo组件,就是一个简单的无状态组件
// demo.jsx
import React from 'react'
const Demo = () => {
return (
<div>demo</div>
)
}
export default Demo
调用示例
import asyncComponent from './async-component'
// 获取到异步组件
const AsyncDemo = asyncComponent(() => import('./demo'))
//...
render() {
return (
<Route path="/demo" component={AsyncDemo}></Route>
)
}
// 如果要传参
render() {
return (
<Route path="/demo" render={() => {
<AsyncComponent test="hello"></AsyncComponent>
}}></Route>
)
}
参数也可以通过asyncComponent函数进行传递,不过需要更改下async-component.js的代码,因为比较简单,所以这里就不展示了