手把手教您实现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在IE和Firefox浏览器下的7个差异兼容写法小结
Jun 18 Javascript
jquery隐藏标签和显示标签的实例
Nov 11 Javascript
javascript字符串替换及字符串分割示例代码
Dec 12 Javascript
json格式数据的添加,删除及排序方法
Jan 21 Javascript
JavaScript纯色二维码变成彩色二维码
Jul 23 Javascript
npm国内镜像 安装失败的几种解决方案
Jun 04 Javascript
详解webpack-dev-server的简单使用
Apr 02 Javascript
微信小程序实现的canvas合成图片功能示例
May 03 Javascript
搭建一个Koa后端项目脚手架的方法步骤
May 30 Javascript
微信小程序实现页面分享onShareAppMessage
Aug 12 Javascript
基于JS抓取某高校附近共享单车位置 使用web方式展示位置变化代码实例
Aug 27 Javascript
深入了解JS之作用域和闭包
Jun 16 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中的Base62类(适用于数值转字符串)
2013/08/12 PHP
PHP带节点操作的无限分类实现方法详解
2016/11/09 PHP
Laravel框架中Blade模板的用法示例
2017/08/30 PHP
PHP添加PNG图片背景透明水印操作类定义与用法示例
2019/03/12 PHP
laravel 配置路由 api和web定义的路由的区别详解
2019/09/03 PHP
文本框中,回车键触发事件的js代码[多浏览器兼容]
2010/06/07 Javascript
基于jquery实现的表格分页实现代码
2011/06/21 Javascript
利用js实现遮罩以及弹出可移动登录窗口
2013/07/08 Javascript
jQuery中delegate与on的用法与区别示例介绍
2013/12/20 Javascript
JavaScript操作Cookie方法实例分析
2015/05/27 Javascript
禁用backspace网页回退功能的实现代码
2016/11/15 Javascript
JavaScript字符串对象(string)基本用法示例
2017/01/18 Javascript
jQuery的中 is(':visible') 解析及用法(必看)
2017/02/12 Javascript
前端开发之CSS原理详解
2017/03/11 Javascript
详解如何用webpack打包一个网站应用项目
2017/07/12 Javascript
jQuery+datatables插件实现ajax加载数据与增删改查功能示例
2018/04/17 jQuery
vue 项目打包通过命令修改 vue-router 模式 修改 API 接口前缀
2018/06/13 Javascript
bootstrap table表格插件之服务器端分页实例代码
2018/09/12 Javascript
JS数据类型STRING使用实例解析
2019/12/18 Javascript
浅谈vue权限管理实现及流程
2020/04/23 Javascript
在Python的web框架中配置app的教程
2015/04/30 Python
python正则表达式面试题解答
2020/04/28 Python
Python基于Flask框架配置依赖包信息的项目迁移部署
2018/03/02 Python
pandas的唯一值、值计数以及成员资格的示例
2018/07/25 Python
python读取目录下所有的jpg文件,并显示第一张图片的示例
2019/06/13 Python
python中wx模块的具体使用方法
2020/05/15 Python
PyQt5实现简单的计算器
2020/05/30 Python
python框架flask入门之环境搭建及开启调试
2020/06/07 Python
苹果美国官方商城:Apple美国
2016/08/24 全球购物
德国网上药房:Apotal
2017/04/04 全球购物
施华洛世奇日本官网:SWAROVSKI日本
2018/05/04 全球购物
KIKO MILANO西班牙官网:意大利领先的化妆品和护肤品品牌
2019/05/03 全球购物
岳庙导游词
2015/02/04 职场文书
2015年乡镇卫生院妇幼保健工作总结
2015/05/19 职场文书
接触艺术对孩子学习思维有益
2019/08/06 职场文书
漫画「你在春天醒来」第10卷封面公开
2022/03/21 日漫