在基于 create-react-app 的React项目中进行代码分片、按需加载(code splitting)/ 免webpack配置 react /async /javascript /代码分片 /按需加载 为何须要代码分片 Facebook 的 create-react-app 是一款很是优秀的开发脚手架。它为咱们生成了 React 开发环境,自带 webpack 默认配置。 它会经过 webpack 打包咱们的应用,产生一个 bundle.js 文件。随着咱们的项目越写越复杂,bundle.js 文件会随之增大。javascript
因为该文件是惟一的,因此无论用户查看哪一个页面、使用哪一个功能,都必须先下载全部的功能代码。java
当 bundle.js 大到必定程度,就会明显影响用户体验。react
此时,咱们就须要 code splitting ,将代码分片,实现按需异步加载,从而优化应用的性能。webpack
代码分片的原理 ES模块(ECMAScript modules)都是静态的。编译时就必须指明 肯定的导入(import)和导出(export)。 这也是规定 import 声明必须出如今模块顶部的缘由所在。web
可是咱们能够经过 dynamic import() 来实现动态加载的功能。 dynamic import() 是 stage 3 中的一个提案。这是一个 运算符 operator 而非函数 function 。 咱们把模块的名字做为参数传入,它会返回一个 Promise ,当模块加载完成后,该 Promise 就会 fulfilled。chrome
当你在代码中新增了一个 import() ,用它动态导入模块时, Webpack 2 会自动据此完成代码分片,不须要任何额外的手动配置。npm
以路由为中心进行代码分片 React 项目中的路由通常用 React Router,它能够将多页面的应用构建为 SPA ,即单页面应用。网络
此处,咱们以其最新版 React Router v4 为例。app
分片前异步
... ... 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.divmin-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