本篇文章經過分析react-loadable包的源碼,手把手教你實現一個react的異步加載高階組件react
// 組件應用
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
整個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
啓動render
promise
接下來還有個細節,異步組件並無直接啓動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實在打不開,只能將源碼放在碼雲上面了點擊下載源碼