自己動手造拖拉機輪子系列 -(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;
      }
    }
  }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM