手把手教您实现react异步加载高阶组件


Posted in Javascript onApril 07, 2020

本篇文章通过分析react-loadable包的源码,手把手教你实现一个react的异步加载高阶组件

1. 首先我们想象中的react异步加载组件应该如何入参以及暴露哪些API?

// 组件应用
import * as React from 'react';
import ReactDOM from 'react-dom';
import Loadable from '@component/test/Loadable';
import Loading from '@component/test/loading';
const ComponentA = Loadable({
  loader: () => import(
    /* webpackChunkName: 'componentA' */
    '@component/test/componentA.js'),
  loading: Loading, //异步组件未加载之前loading组件
  delay: 1000, //异步延迟多久再渲染
  timeout: 1000, //异步组件加载超时
})
ComponentA.preload(); //预加载异步组件的方式

const ComponentB = Loadable({
  loader: () => import(
    /* webpackChunkName: 'componentB' */
    '@component/test/componentB.js'),
  loading: Loading, //异步组件未加载之前loading组件
})

Loadable.preloadAll().then(() => {
  //
}).catch(err => {
  //
}); //预加载所有的异步组件

const App = (props) => {
  const [isDisplay, setIsDisplay] = React.useState(false);
  if(isDisplay){
    return <React.Fragment>
      <ComponentA />
      <ComponentB />
    </React.Fragment> 
  }else{
    return <input type='button' value='点我' onClick={()=>{setIsDisplay(true)}}/>
  }
}

ReactDOM.render(<App />, document.getElementById('app'));
// loading组件
import * as React from 'react';

export default (props) => {
  const {error, pastDelay, isLoading, timedOut, retry} = props;
  if (props.error) {
    return <div>Error! <button onClick={ retry }>Retry</button></div>;
   } else if (timedOut) {
    return <div>Taking a long time... <button onClick={ retry }>Retry</button></div>;
   } else if (props.pastDelay) {
    return <div>Loading...</div>;
   } else {
    return null;
   }
}

通过示例可以看到我们需要入参loaded、loading、delay、timeout,同时暴露单个预加载和全部预加载的API,接下来就让我们试着去一步步实现Loadable高阶组件

2.组件实现过程

整个Loaded函数大体如下

// 收集所有需要异步加载的组件 用于预加载
const ALL_INITIALIZERS = [];

function Loadable(opts){
  return createLoadableComponent(load, opts);
}
// 静态方法 预加载所有组件
Loadable.preloadAll = function(){

}

接下来实现createLoadableComponent以及load函数

// 预加载单个异步组件
function load(loader){
  let promise = loader();
  let state = {
    loading: true,
    loaded: null,
    error: null,
  }
  state.promise = promise.then(loaded => {
    state.loading = false;
    state.loaded = loaded;
    return loaded;
  }).catch(err => {
    state.loading = false;
    state.error = err;
    throw err;
  })
  return state;
}

// 创建异步加载高阶组件
function createLoadableComponent(loadFn, options){
  if (!options.loading) {
    throw new Error("react-loadable requires a `loading` component");
  }
  let opts = Object.assign({
    loader: null,
    loading: null,
    delay: 200,
    timeout: null,
  }, options);

  let res = null;

  function init(){
    if(!res){
      res = loadFn(options.loader);
      return res.promise;
    }
  }

  ALL_INITIALIZERS.push(init);

  return class LoadableComponent extends React{}
}

我们可以看到createLoadableComponent主要功能包括合并默认配置,将异步组件推入预加载数组,并返回LoadableComponent组件;load函数用于加载单个组件并返回该组件的初始加载状态

接着我们实现核心部分LoadableComponent组件

class LoadableComponent extends React.Component{
    constructor(props){
      super(props);
      //组件初始化之前调用init方法下载异步组件
      init(); 
      this.state = {
        error: res.error,
        postDelay: false,
        timedOut: false,
        loading: res.loading,
        loaded: res.loaded
      }
      this._delay = null;
      this._timeout = null;
    }
    componentWillMount(){
      //设置开关保证不多次去重新请求异步组件
      this._mounted = true;
      this._loadModule();
    }

    _loadModule(){
      if(!res.loading) return;
      if(typeof opts.delay === 'number'){
        if(opts.delay === 0){
          this.setState({pastDelay: true});
        }else{
          this._delay = setTimeout(()=>{
            this.setState({pastDelay: true});
          }, opts.delay)
        }
      }

      if(typeof opts.timeout === 'number'){
        this._timeout = setTimeout(()=>{
          this.setState({timedOut: true});
        }, opts.timeout)
      }

      let update = () => {
        if(!this._mounted) return;
        this.setState({
          error: res.error,
          loaded: res.loaded,
          loading: res.loading,
        });
      }
      // 接收异步组件的下载结果并重新setState来render
      res.promise.then(()=>{
        update()
      }).catch(err => {
        update()
      })
    }


    // 重新加载异步组件
    retry(){
      this.setState({
        error: null,
        timedOut: false,
        loading: false,
      });
      res = loadFn(opts.loader);
      this._loadModule();
    }
    // 静态方法 单个组件预加载
    static preload(){
      init()
    }


    componentWillUnmount(){
      this._mounted = false;
      clearTimeout(this._delay);
      clearTimeout(this._timeout);
    }

    render(){
      const {loading, error, pastDelay, timedOut, loaded} = this.state;
      if(loading || error){
        //异步组件还未下载完成的时候渲染loading组件
        return React.createElement(opts.loading, {
          isLoading: loading,
          pastDelay: pastDelay,
          timedOut: timedOut,
          error: error,
          retry: this.retry.bind(this),
        })
      }else if(loaded){
        // 为何此处不直接用React.createElement?
        return opts.render(loaded, this.props);
      }else{
        return null;
      }
    }    
  }

可以看到,初始的时候调用init方法启动异步组件的下载,并在_loadModule方法里面接收异步组件的pending结果,待到异步组件下载完毕,重新setState启动render

接下来还有个细节,异步组件并没有直接启动React.createElement去渲染,而是采用opts.render方法,这是因为webpack打包生成的单独异步组件chunk暴露的是一个对象,其default才是对应的组件

实现如下

function resolve(obj) {
  return obj && obj.__esModule ? obj.default : obj;
}
 
function render(loaded, props) {
  return React.createElement(resolve(loaded), props);
}

最后实现全部预加载方法

Loadable.preloadAll = function(){
  let promises = [];
  while(initializers.length){
    const init = initializers.pop();
    promises.push(init())
  }
  return Promise.all(promises);
}

整个代码实现如下

const React = require("react");

// 收集所有需要异步加载的组件
const ALL_INITIALIZERS = [];

// 预加载单个异步组件
function load(loader){
  let promise = loader();
  let state = {
    loading: true,
    loaded: null,
    error: null,
  }
  state.promise = promise.then(loaded => {
    state.loading = false;
    state.loaded = loaded;
    return loaded;
  }).catch(err => {
    state.loading = false;
    state.error = err;
    throw err;
  })
  return state;
}

function resolve(obj) {
  return obj && obj.__esModule ? obj.default : obj;
}
 
function render(loaded, props) {
  return React.createElement(resolve(loaded), props);
}

// 创建异步加载高阶组件
function createLoadableComponent(loadFn, options){
  if (!options.loading) {
    throw new Error("react-loadable requires a `loading` component");
  }
  let opts = Object.assign({
    loader: null,
    loading: null,
    delay: 200,
    timeout: null,
    render,
  }, options);

  let res = null;

  function init(){
    if(!res){
      res = loadFn(options.loader);
      return res.promise;
    }
  }

  ALL_INITIALIZERS.push(init);

  class LoadableComponent extends React.Component{
    constructor(props){
      super(props);
      init();
      this.state = {
        error: res.error,
        postDelay: false,
        timedOut: false,
        loading: res.loading,
        loaded: res.loaded
      }
      this._delay = null;
      this._timeout = null;
    }

    

    componentWillMount(){
      this._mounted = true;
      this._loadModule();
    }

    _loadModule(){
      if(!res.loading) return;
      if(typeof opts.delay === 'number'){
        if(opts.delay === 0){
          this.setState({pastDelay: true});
        }else{
          this._delay = setTimeout(()=>{
            this.setState({pastDelay: true});
          }, opts.delay)
        }
      }

      if(typeof opts.timeout === 'number'){
        this._timeout = setTimeout(()=>{
          this.setState({timedOut: true});
        }, opts.timeout)
      }

      let update = () => {
        if(!this._mounted) return;
        this.setState({
          error: res.error,
          loaded: res.loaded,
          loading: res.loading,
        });
      }

      res.promise.then(()=>{
        update()
      }).catch(err => {
        update()
      })
    }


    // 重新加载异步组件
    retry(){
      this.setState({
        error: null,
        timedOut: false,
        loading: false,
      });
      res = loadFn(opts.loader);
      this._loadModule();
    }

    static preload(){
      init()
    }


    componentWillUnmount(){
      this._mounted = false;
      clearTimeout(this._delay);
      clearTimeout(this._timeout);
    }

    render(){
      const {loading, error, pastDelay, timedOut, loaded} = this.state;
      if(loading || error){
        return React.createElement(opts.loading, {
          isLoading: loading,
          pastDelay: pastDelay,
          timedOut: timedOut,
          error: error,
          retry: this.retry.bind(this),
        })
      }else if(loaded){
        return opts.render(loaded, this.props);
      }else{
        return null;
      }
    }

    
  }

  return LoadableComponent;
}

function Loadable(opts){
  return createLoadableComponent(load, opts);
}

function flushInitializers(initializers){
  
  
}
Loadable.preloadAll = function(){
  let promises = [];
  while(initializers.length){
    const init = initializers.pop();
    promises.push(init())
  }
  return Promise.all(promises);
}

export default Loadable;

到此这篇关于手把手教您实现react异步加载高阶组件的文章就介绍到这了,更多相关react异步加载高阶组件内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript getElementsByTagName
Jan 31 Javascript
js中this的用法实例分析
Jan 10 Javascript
jQuery制作拼图小游戏
Jan 12 Javascript
浅析$.getJSON异步请求和同步请求
Jun 06 Javascript
js 动态添加元素(div、li、img等)及设置属性的方法
Jul 19 Javascript
js使用generator函数同步执行ajax任务
Sep 05 Javascript
20行JS代码实现粘贴板复制功能
Feb 06 Javascript
jQuery内容选择器与表单选择器实例分析
Jun 28 jQuery
django js 实现表格动态标序号的实例代码
Jul 12 Javascript
layui-tree实现Ajax异步请求后动态添加节点的方法
Sep 23 Javascript
vue实现将一个数组内的相同数据进行合并
Nov 07 Javascript
vue使用canvas实现移动端手写签名
Sep 22 Javascript
javascript绘制简单钟表效果
Apr 07 #Javascript
js中位数不足自动补位扩展padLeft、padRight实现代码
Apr 06 #Javascript
jquery实现两个div中的元素相互拖动的方法分析
Apr 05 #jQuery
js实现登录时记住密码的方法分析
Apr 05 #Javascript
Vue插件之滑动验证码用法详解
Apr 05 #Javascript
解决node终端下运行js文件不支持ES6语法
Apr 04 #Javascript
jQuery 图片查看器插件 Viewer.js用法简单示例
Apr 04 #jQuery
You might like
一段php加密解密的代码
2007/07/16 PHP
php将字符串转化成date存入数据库的两种方式
2014/04/28 PHP
VB中的RasEnumConnections函数返回632错误解决方法
2014/07/29 PHP
PHP SFTP实现上传下载功能
2017/07/26 PHP
PHP实现图的邻接矩阵表示及几种简单遍历算法分析
2017/11/24 PHP
PHP实现一个轻量级容器的方法
2019/01/28 PHP
Javascript Jquery 遍历Json的实现代码
2010/03/31 Javascript
jQuery 过滤not()与filter()实例代码
2012/05/10 Javascript
JavaScript数组去重的3种方法和代码实例
2015/07/01 Javascript
基于jquery实现可定制的web在线富文本编辑器附源码下载
2015/11/17 Javascript
AngularJS基础 ng-keypress 指令简单示例
2016/08/02 Javascript
移动端点击图片放大特效PhotoSwipe.js插件实现
2016/08/25 Javascript
jQuery编写网页版2048小游戏
2017/01/06 Javascript
浅析vue component 组件使用
2017/03/06 Javascript
解决vue router使用 history 模式刷新后404问题
2017/07/19 Javascript
Vue组件之自定义事件的功能图解
2018/02/01 Javascript
原生JS检测CSS3动画是否结束的方法详解
2019/01/27 Javascript
nuxt.js添加环境变量,区分项目打包环境操作
2020/11/06 Javascript
[02:00]DOTA2英雄COSPLAY闹市街头巡游助威2015国际邀请赛
2015/08/02 DOTA
使用Python编写一个简单的tic-tac-toe游戏的教程
2015/04/16 Python
在Python的Django框架中获取单个对象数据的简单方法
2015/07/17 Python
Python中的条件判断语句基础学习教程
2016/02/07 Python
Windows平台Python连接sqlite3数据库的方法分析
2017/07/12 Python
python spyder中读取txt为图片的方法
2018/04/27 Python
python游戏地图最短路径求解
2019/01/16 Python
正确理解Python中if __name__ == '__main__'
2019/01/24 Python
Django的性能优化实现解析
2019/07/30 Python
Python将二维列表list的数据输出(TXT,Excel)
2020/04/23 Python
Python实现CAN报文转换工具教程
2020/05/05 Python
CSS3感应鼠标的背景闪烁和图片缩放动画效果
2014/05/14 HTML / CSS
音乐学个人的自荐书范文
2013/11/26 职场文书
班级寄语大全
2014/04/10 职场文书
员工加薪申请报告
2015/05/15 职场文书
仓库管理制度范本
2015/08/04 职场文书
DjangoRestFramework 使用 simpleJWT 登陆认证完整记录
2021/06/22 Python
Go中使用gjson来操作JSON数据的实现
2022/08/14 Golang