自己动手造拖拉机轮子系列 -(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;
      }
    }
  }
}
posted @ 2018-10-20 20:21  FeMiner  阅读(983)  评论(0编辑  收藏  举报