自己动手造拖拉机轮子系列 -(react-loadable)
最新消息:react官方已支持懒加载https://reactjs.org/docs/code-splitting.html#reactlazy
文章webpack分片chunk加载原理中深入探究了异步chunk的加载原理,根据这个特性,在大型单页应用中,很容易实现大到子业务,中到子路由,小到子模块或者子组件的按需加载。react-loadable即封装了组件按需加载的流程并对外提供了一系列配置选项,极大的改善了开发体验,在业界算是实现按需加载的首选了。本文不打算深入分析其源码实现,而是根据其对外提供的配置选项,自己动手实现一个类似的“异步组件加载器”,而且因为懒惰,这里并不会实现它的高级特性,也不会支持服务端渲染。😁
我们从react-loadable官方文档给出的第一个简单例子开始
/*App.js*/
import React from "react"
import Loadable from "react-loadable"
import Loading from "./Loading"
const LoadableComponent = Loadable({
loader: () => import("./MyComponent"),
loading: Loading,
})
export default class App extends React.Component {
render() {
return <LoadableComponent />
}
}
/* MyComponent.js*/
import React from 'react';
export default class MyComponent extends React.Component {
render() {
return <div>我是一只小小鸟</div>
}
}
/* Loading.js */
import React from 'react';
export default function Loading() {
return (
<div>Loading</div>
)
}
基础功能
从上面的例子可以看出,App
渲染的组件LoadbaleComponent
是通过Loadable
方法处理后返回的组件,也就是说实际上Loadable
是一个高阶组件。(A higher order component for loading components with dynamic imports)。这个高级组件接受一个配置对象作为参数,返回异步加载组件,配置对象中loader
是一个异步加载器防范,该方法返回一个Promise
,MyComponent
组件是需要异步加载的组件,Loading
是处于加载过程中展示的组件。根据这个特性,可以很容易实现如下Loadable
的基础功能
function Loadable(opts) {
const { loading: LoadingComponent, loader } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true, // 是否加载中
loaded: null // 待加载的模块
}
}
componentDidMount() {
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
})
.catch(() => {})
}
render() {
const { loading, loaded } = this.state
if (loading) {
return <LoadingComponent />
} else if (loaded) {
// 默认加载default组件
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
加载失败处理
接下来我们给Loading
组件添加上加载失败重试的功能,当组件加载失败时来给用户一些提示或者交互。为此给Loadable
方法返回的组件添加一个error
状态,并添加一个retry
方法,并提取出加载模块的方法_loadModule
function Loadable(opts) {
const { loading: LoadingComponent, loader } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
loaded: null
}
}
_loadModule = () => {
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
})
.catch((error) => {
this.setState({
error
})
})
}
retry = () => {
this.setState({
loading: true,
error: null
})
this._loadModule()
}
componentDidMount() {
this._loadModule()
}
render() {
const { loading, error, loaded } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry}/>
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
同时改造Loading
组件,使其可以接受一些状态属性
/* Loading.js */
import React from 'react';
export default function Loading(props) {
if(props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else {
return <div>Loading</div>
}
}
避免闪屏
当组件加载很快的时候,Loading
组件的展示会一闪而过,给用户造成闪屏的感觉,体验不是很好。react-loadable通过配置delay
选项来避免这个问题,当组件加载时间小于delay
时,不展示Loading
组件,也就是说Loading
组件会推迟delay
时间才展示,下面我们我们实现这个功能,默认推迟时间是200ms。
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200 } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
if (typeof delay === "number") {
if (delay === 0) {
this.setState({ pastDelay: true });
} else {
this._delay = setTimeout(() => {
this.setState({ pastDelay: true });
}, delay);
}
}
// 异步加载组件
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
this._clearTimeouts()
})
.catch((error) => {
this.setState({
error
})
this._clearTimeouts()
})
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
}
... ...
... ...
componentWillUnmount() {
this._clearTimeouts()
}
render() {
const { loading, error, loaded, pastDelay } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} />
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
在_loadModule
方法中来设置delay
时间是否完成的状态即可,仿照react-loadable的处理方式,把postDelay
状态传给Loading
组件处理,这个方便开发人员自定义处理方式。
/* Loading.js */
import React from "react"
export default function Loading(props) {
if (props.error) {
return (
<div>
Error! <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.pastDelay) {
return <div>Loading</div>
} else {
return null
}
}
加载超时处理
此外,react-loadable还支持超时处理,当由于各种原因组件加载被挂起时,给用户一些反馈,timeout
功能实现方式类似于delay
功能。
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
timedOut: false,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
... ...
... ...
// 网络超时
if (typeof timeout === "number") {
this._timeout = setTimeout(() => {
this.setState({ timedOut: true });
}, timeout);
}
// 异步加载组件
... ...
... ...
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
this._timeout && clearTimeout(this._timeout);
}
retry = () => {
this.setState({
loading: true,
error: null,
timeOut: false
})
this._loadModule()
}
... ...
... ...
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
/* Loading.js */
import React from "react"
export default function Loading(props) {
if (props.error) {
return (
<div>
Error! <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.timedOut) {
return (
<div>
Taking a long time... <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null
}
}
自定义组件渲染方式
默认情况下,react-loadable会渲染模块的默认导出,如果想自定义这个行为,可以传入render
配置渲染。为此我们只需要在配置对象中添加render
方法并在高阶组件返回的返回组件render
方法中添加少许代码
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout, render } = opts
return class LoadableComponent extends React.Component {
... ...
... ...
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
if(render) {
return render(loaded, this.props)
}
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...props} />
} else {
return null;
}
}
}
}
我们可以在MyComponent.js中添加一个导出的组件来测试功能。
/* MyComponent.js*/
import React from "react"
export class NestedComponent extends React.Component {
render() {
return <div>我是一只大大鸟</div>
}
}
export default class MyComponent extends React.Component {
render() {
return <div>我是一只小小鸟</div>
}
}
/* App.js */
const LoadableComponent = Loadable({
loader: () => import("./MyComponent"),
loading: Loading,
render: (loaded, props) => {
let Component = loaded.NestedComponent;
return <Component {...props}/>;
}
})
下面是完整的Loadable
代码
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout, render } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
timedOut: false,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
// 推迟加载Loading
if (typeof delay === "number") {
if (delay === 0) {
this.setState({ pastDelay: true });
} else {
this._delay = setTimeout(() => {
this.setState({ pastDelay: true });
}, delay);
}
}
// 网络超时
if (typeof timeout === "number") {
this._timeout = setTimeout(() => {
this.setState({ timedOut: true });
}, timeout);
}
// 异步加载组件
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
this._clearTimeouts()
})
.catch((error) => {
this.setState({
error
})
this._clearTimeouts()
})
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
this._timeout && clearTimeout(this._timeout);
}
retry = () => {
this.setState({
loading: true,
error: null,
timeOut: false
})
this._loadModule()
}
componentDidMount() {
this._loadModule()
}
componentWillUnmount() {
this._clearTimeouts()
}
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
if(render) {
return render(loaded, this.props)
}
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props} />
} else {
return null;
}
}
}
}