您的位置:首页 > 移动开发

在基于 create-react-app 的React项目中进行代码分片、按需加载(code splitting)/ 免webpack配置

2017-11-21 13:12 1271 查看


为什么需要代码分片

Facebook 的 create-react-app 是一款非常优秀的开发脚手架。它为我们生成了 React 开发环境,自带 webpack
默认配置。 它会通过 webpack 打包我们的应用,产生一个 
bundle.js
 文件。随着我们的项目越写越复杂,
bundle.js
 文件会随之增大。 

由于该文件是唯一的,所以不管用户查看哪个页面、使用哪个功能,都必须先下载所有的功能代码。 

当 
bundle.js
 大到一定程度,就会明显影响用户体验。

此时,我们就需要 code splitting ,将代码分片,实现按需异步加载,从而优化应用的性能。


代码分片的原理

ES模块(ECMAScript modules)都是静态的。编译时就必须指明 确定的导入(import)和导出(export)。 这也是规定 import 声明必须出现在模块顶部的原因所在。

但是我们可以通过 
dynamic
import()
 来实现动态加载的功能。 
dynamic import()
 是 stage 3 中的一个提案。这是一个 运算符 operator 而非函数 function 。
我们把模块的名字作为参数传入,它会返回一个 Promise ,当模块加载完成后,该 Promise 就会 fulfilled。

当你在代码中新增了一个 
import()
 ,用它动态导入模块时, Webpack 2 会自动据此完成代码分片,不需要任何额外的手动配置。


以路由为中心进行代码分片

React 项目中的路由一般用 React Router,它可以将多页面的应用构建为 SPA ,即单页面应用。 

此处,我们以其最新版 React Router v4 为例。

分片前

... ...
import {requireAuthentication} from './CheckToken'
import Home from '../components/Home/Home'
import Login from './LoginContainer'
import Signup from './SignupContainer'
import Profile from './ProfileContainer'
... ...
<Router>
<Switch>
<Route exact path='/' component={Home} />
<Route path='/login' component={Login} />
<Route path='/signup' component={Signup} />
<Route path='/profile' component={requireAuthentication(Profile)} />
... ...


分片后

新增 
AsyncComponent
,它将接受一个函数作为参数,实现异步地动态加载组件。例如:

const AsyncLogin = asyncComponent(() => import('./LoginContainer'))


至于为什么是以 
() => import('./LoginContainer')
 这样的箭头函数为参数,而非 
'./LoginContainer'
 这样的字符串,和
Webpack 的进行代码分片的机制有关。 

这么写看起来啰嗦,但可以让我们控制生成多少个 
.chunk.js
 这样的分片文件。

代码:

import React, { Component } from 'react'

export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props)

this.state = {
component: null
}
}

async componentDidMount() {
const { default: component } = await importComponent()

this.setState({
component: component
})
}

render() {
const C = this.state.component

return C ? <C {...this.props} /> : null
}
}

return AsyncComponent
}


路由

... ...
import {requireAuthentication} from './CheckToken'
import asyncComponent from './AsyncComponent'

const AsyncHome = asyncComponent(() => import('../components/Home/Home'))
const AsyncLogin = asyncComponent(() => import('./LoginContainer'))const AsyncSignup = asyncComponent(() => import('./SignupContainer'))
const AsyncProfile = asyncComponent(() => import('./ProfileContainer'))
... ...
<Router>
<Switch>
<Route exact path='/' component={AsyncHome} />
<Route path='/login' component={AsyncLogin} />
<Route path='/signup' component={AsyncSignup} />
<Route path='/profile' component={requireAuthentication(AsyncProfile)} />
... ...


此时再运行 
npm run build
,看编译的log,以及 
build/static/js/
 目录下的
js 文件,会发现多出了若干文件名 
.chunk.js
 结尾的文件。

  
npm start
 把项目跑起来,在 chrome 的 devTool 中,打开 
Network
 ,查看 
JS
 ,就可以看到异步动态按需加载分片文件的效果了。


以组件为中心进行代码分片

上面一小节是以路由为中心进行代码分片的思路与实现。但是 React Router 官网说得明白,React Router 是导航组件的集合。 

即,路由本身并没有什么特别的,它们也是组件。

如果以组件为中心进行代码分片,会带来额外的好处:

除了路由此外,还有很多地方可以进行代码分片。广阔天地,大有作为。

同一个组件中,针对不急着显示的东西,可以延迟其加载。

... ...

这里介绍 React Loadable 。

通过它,我们可以用使用 React 高阶组件 (Higher Order Component / HOC)实现异步加载
React 组件的功能,同时处理操作失败、网络错误等等边缘情况。

注:一个高阶组件,简言之就是一个函数,它接受的参数是 React 组件,返回的结果也是 React 组件。

React Loadable 可以通过 npm 安装 
react-loadable


首先,我们用 React Loadable 来重构刚才的代码

处理边缘情况的组件

import React from 'react'

const MyLoadingComponent = ({isLoading, error}) => {
// 加载中
if (isLoading) {
return <div>Loading...</div>
}
// 加载出错
else if (error) {
return <div>Sorry, there was a problem loading the page.</div>
}
else {
return null
}
}

export default LoadingComponent


路由

... ...
import {requireAuthentication} from './CheckToken'
import Loadable from 'react-loadable'
import LoadingComponent from '../components/common/Loading'

const AsyncHome = Loadable({
loader: () => import('../components/Home/Home'),
loading: LoadingComponent
})
const AsyncSignup = Loadable({
loader: () => import('./SignupContainer'),
loading: LoadingComponent
})
const AsyncLogin = Loadable({
loader: () => import('./LoginContainer'),
loading: LoadingComponent
})
const AsyncProfile = Loadable({
loader: () => import('./ProfileContainer'),
loading: LoadingComponent
})

... ...
<Router>
<Switch>
<Route exact path='/' component={AsyncHome} />
<Route path='/login' component={AsyncLogin} />
<Route path='/signup' component={AsyncSignup} />
<Route path='/profile' component={requireAuthentication(AsyncProfile)} />
... ...



进一步优化

重新运行项目,发现了可以进一步改进的地方。


防止 Loading 组件闪现

在页面跳转的时候,屏幕上会短暂的闪过 LoadingComponent 组件。

我们添加该组件的初衷,是在网络差的时候,给用户一个提示:“应用运行正常,只是正在加载中,请稍等。”

显然,如果网络良好,跳转足够快,LoadingComponent 组件根本没有必要出现。

React Loadable 可以很容易地实现这个功能。

LoadingComponent 组件接收一个 pastDelay 属性,该属性仅仅在延迟超过一个规定的值后才为 true 。

默认的延迟是 200ms,我们也可以自己指定别的时长。操作如下,我们将其设置为 300ms。

... ...
const AsyncLogin = Loadable({
loader: () => import('./LoginContainer'),
loading: LoadingComponent,
delay: 300
})
... ...


LoadingComponent 组件做相应调整。同时增加一些简单的样式。

import React from 'react'
import Footer from '../Footer/Footer'
import styled from 'styled-components'

const Wrap = styled.div`
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #B2EBF2;
text-align: center;
`

const LoadingComponent = (props) => {
if (props.error) {
return (
<Wrap>
<div>Error!</div>
<Footer />
</Wrap>
)
} else if (props.pastDelay) {
// 300ms 之后显示
return (
<Wrap>
<div>信息请求中...</div>
<Footer />
</Wrap>
)
} else {
return null
}
}

export default LoadingComponent



同一个组件中,延迟加载不急着显示的内容

例如这个组件,TopHeader 是优先显然的内容,Notification 是不一定显示的内容。我们可以推迟后者的加载。

... ...
import TopHeader from '../components/Header/TopHeader'
import Notification from './NotificationContainer'

class TopHeaderContainer extends Component {
... ...

return (
<div>
<TopHeader
sideButtons={tempIsAuthenticated}
logout={this.logout}
/>
<Notification />
</div>

)
}
... ...
export default connect(mapStateToProps, { logout })(TopHeaderContainer)


优化后

... ...
import TopHeader from '../components/Header/TopHeader'

import Loadable from 'react-loadable'
import LoadingComponent from '../components/common/Loading'

const AsyncNotification = Loadable({
loader: () => import('./NotificationContainer'),
loading: LoadingComponent,
delay: 300
})
... ...
class TopHeaderContainer extends Component {
... ...

return (
<div>
<TopHeader
sideButtons={tempIsAuthenticated}
logout={this.logout}
/>
<AsyncNotification />
</div>
)
}
}
... ...
export default connect(mapStateToProps, { logout })(TopHeaderContainer)
... ...


此外, 还可以实现 预加载(如 click 按钮显示某组件,那么在 hover 事件时就预先加载之)、服务端渲染 等等。

在此就不多做介绍了。


参考资料

ES proposal: import() – dynamically importing ES modules

Code Splitting in Create React App

Component-centric code splitting and loading in React

@dan_abramov
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息