手把手教您实现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 相关文章推荐
合并table相同单元格的jquery插件分享(很精简)
Jun 20 Javascript
使用jQuery.fn自定义jQuery翻页插件
Jan 20 Javascript
js实现最短的XML格式化工具实例
Mar 12 Javascript
跨域请求的完美解决方法(JSONP, CORS)
Jun 12 Javascript
AngularJS过滤器详解及示例代码
Aug 16 Javascript
jQuery简单实现中间浮窗效果
Sep 04 Javascript
微信小程序中用WebStorm使用LESS
Mar 08 Javascript
Vue + Webpack + Vue-loader学习教程之相关配置篇
Mar 14 Javascript
简单明了区分escape、encodeURI和encodeURIComponent
May 26 Javascript
elementUI select组件value值注意事项详解
May 29 Javascript
解决layui 三级联动下拉框更新时回显的问题
Sep 03 Javascript
VUE和Antv G6实现在线拓扑图编辑操作
Oct 28 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中创建空文件的代码[file_put_contents vs touch]
2012/01/20 PHP
php安全之直接用$获取值而不$_GET 字符转义
2012/06/03 PHP
php 发送带附件邮件示例
2014/01/23 PHP
wamp服务器访问php非常缓慢的解决过程
2015/07/01 PHP
PHP函数实现从一个文本字符串中提取关键字的方法
2015/07/01 PHP
总结PHP中DateTime的常用方法
2016/08/11 PHP
PHP实现找出链表中环的入口节点
2018/01/16 PHP
PHP实现二维数组中的查找算法小结
2018/06/09 PHP
PHP程序员学习使用Swoole的理由
2018/06/24 PHP
PHP设计模式之建造者模式(Builder)原理与用法案例详解
2019/12/12 PHP
奉献给JavaScript初学者的编写开发的七个细节
2011/01/11 Javascript
基于jquery tab切换(防止页面刷新)
2012/05/23 Javascript
图解prototype、proto和constructor的三角关系
2016/07/31 Javascript
javascript 删除数组元素和清空数组的简单方法
2017/02/24 Javascript
基于jquery日历价格、库存等设置插件
2020/07/05 jQuery
深入理解vue Render函数
2017/07/19 Javascript
js断点调试心得分享(必看篇)
2017/12/08 Javascript
详解vue axios用post提交的数据格式
2018/08/07 Javascript
this在vue和小程序中的使用详解
2019/01/28 Javascript
jQuery实现的隔行变色功能【案例】
2019/02/18 jQuery
vue计算属性computed的使用方法示例
2019/03/13 Javascript
vue实现密码显示与隐藏按钮的自定义组件功能
2019/04/23 Javascript
JavaScript中0、空字符串、'0'是true还是false的知识点分享
2019/09/16 Javascript
vue项目中极验验证的使用代码示例
2019/12/03 Javascript
通过JS判断网页是否为手机打开
2020/10/28 Javascript
[02:42]2014DOTA2国际邀请赛 三冰专访:我会打到Ti20
2014/07/13 DOTA
解决安装tensorflow遇到无法卸载numpy 1.8.0rc1的问题
2018/06/13 Python
对django中render()与render_to_response()的区别详解
2018/10/16 Python
Django框架验证码用法实例分析
2019/05/10 Python
CSS伪类与CSS伪元素的区别及由来具体说明
2012/12/07 HTML / CSS
法国发饰品牌:Alexandre De Paris
2018/12/04 全球购物
计算机求职信
2013/12/01 职场文书
分公司总经理岗位职责
2014/08/03 职场文书
2015毕业设计工作总结
2015/07/24 职场文书
2019年员工晋升管理制度范本!
2019/07/08 职场文书
使用Pytorch训练two-head网络的操作
2021/05/28 Python