手把手教您實現react異步加載高階組件

本篇文章經過分析react-loadable包的源碼,手把手教你實現一個react的異步加載高階組件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高階組件webpack

2.組件實現過程

整個Loaded函數大致以下git

// 收集全部須要異步加載的組件 用於預加載
const ALL_INITIALIZERS = [];

function Loadable(opts){
    return createLoadableComponent(load, opts);
}
// 靜態方法 預加載全部組件
Loadable.preloadAll = function(){

}
複製代碼

接下來實現createLoadableComponent以及load函數github

// 預加載單個異步組件
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函數用於加載單個組件並返回該組件的初始加載狀態web

接着咱們實現核心部分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啓動renderpromise

接下來還有個細節,異步組件並無直接啓動React.createElement去渲染,而是採用opts.render方法,這是由於webpack打包生成的單獨異步組件chunk暴露的是一個對象,其default纔是對應的組件bash

實現以下app

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

最後實現所有預加載方法dom

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;
複製代碼

因爲最近github實在打不開,只能將源碼放在碼雲上面了點擊下載源碼

相關文章
相關標籤/搜索