最新消息:react官方已支持懶加載https://reactjs.org/docs/code-splitting.html#reactlazy
文章webpack分片chunk加載原理中深入探究了異步chunk的加載原理,根據這個特性,在大型單頁應用中,很容易實現大到子業務,中到子路由,小到子模塊或者子組件的按需加載。react-loadable即封裝了組件按需加載的流程並對外提供了一系列配置選項,極大的改善了開發體驗,在業界算是實現按需加載的首選了。本文不打算深入分析其源碼實現,而是根據其對外提供的配置選項,自己動手實現一個類似的“異步組件加載器”,而且因為懶惰,這里並不會實現它的高級特性,也不會支持服務端渲染。😁
我們從react-loadable官方文檔給出的第一個簡單例子開始
/*App.js*/
import React from "react"
import Loadable from "react-loadable"
import Loading from "./Loading"
const LoadableComponent = Loadable({
loader: () => import("./MyComponent"),
loading: Loading,
})
export default class App extends React.Component {
render() {
return <LoadableComponent />
}
}
/* MyComponent.js*/
import React from 'react';
export default class MyComponent extends React.Component {
render() {
return <div>我是一只小小鳥</div>
}
}
/* Loading.js */
import React from 'react';
export default function Loading() {
return (
<div>Loading</div>
)
}
基礎功能
從上面的例子可以看出,App渲染的組件LoadbaleComponent是通過Loadable方法處理后返回的組件,也就是說實際上Loadable是一個高階組件。(A higher order component for loading components with dynamic imports)。這個高級組件接受一個配置對象作為參數,返回異步加載組件,配置對象中loader是一個異步加載器防范,該方法返回一個Promise,MyComponent組件是需要異步加載的組件,Loading是處於加載過程中展示的組件。根據這個特性,可以很容易實現如下Loadable的基礎功能
function Loadable(opts) {
const { loading: LoadingComponent, loader } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true, // 是否加載中
loaded: null // 待加載的模塊
}
}
componentDidMount() {
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
})
.catch(() => {})
}
render() {
const { loading, loaded } = this.state
if (loading) {
return <LoadingComponent />
} else if (loaded) {
// 默認加載default組件
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
加載失敗處理
接下來我們給Loading組件添加上加載失敗重試的功能,當組件加載失敗時來給用戶一些提示或者交互。為此給Loadable方法返回的組件添加一個error狀態,並添加一個retry方法,並提取出加載模塊的方法_loadModule
function Loadable(opts) {
const { loading: LoadingComponent, loader } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
loaded: null
}
}
_loadModule = () => {
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
})
.catch((error) => {
this.setState({
error
})
})
}
retry = () => {
this.setState({
loading: true,
error: null
})
this._loadModule()
}
componentDidMount() {
this._loadModule()
}
render() {
const { loading, error, loaded } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry}/>
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
同時改造Loading組件,使其可以接受一些狀態屬性
/* Loading.js */
import React from 'react';
export default function Loading(props) {
if(props.error) {
return <div>Error! <button onClick={ props.retry }>Retry</button></div>;
} else {
return <div>Loading</div>
}
}
避免閃屏
當組件加載很快的時候,Loading組件的展示會一閃而過,給用戶造成閃屏的感覺,體驗不是很好。react-loadable通過配置delay選項來避免這個問題,當組件加載時間小於delay時,不展示Loading組件,也就是說Loading組件會推遲delay時間才展示,下面我們我們實現這個功能,默認推遲時間是200ms。
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200 } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
if (typeof delay === "number") {
if (delay === 0) {
this.setState({ pastDelay: true });
} else {
this._delay = setTimeout(() => {
this.setState({ pastDelay: true });
}, delay);
}
}
// 異步加載組件
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
this._clearTimeouts()
})
.catch((error) => {
this.setState({
error
})
this._clearTimeouts()
})
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
}
... ...
... ...
componentWillUnmount() {
this._clearTimeouts()
}
render() {
const { loading, error, loaded, pastDelay } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} />
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
在_loadModule方法中來設置delay時間是否完成的狀態即可,仿照react-loadable的處理方式,把postDelay狀態傳給Loading組件處理,這個方便開發人員自定義處理方式。
/* Loading.js */
import React from "react"
export default function Loading(props) {
if (props.error) {
return (
<div>
Error! <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.pastDelay) {
return <div>Loading</div>
} else {
return null
}
}
加載超時處理
此外,react-loadable還支持超時處理,當由於各種原因組件加載被掛起時,給用戶一些反饋,timeout功能實現方式類似於delay功能。
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
timedOut: false,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
... ...
... ...
// 網絡超時
if (typeof timeout === "number") {
this._timeout = setTimeout(() => {
this.setState({ timedOut: true });
}, timeout);
}
// 異步加載組件
... ...
... ...
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
this._timeout && clearTimeout(this._timeout);
}
retry = () => {
this.setState({
loading: true,
error: null,
timeOut: false
})
this._loadModule()
}
... ...
... ...
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props}/>
} else {
return null;
}
}
}
}
/* Loading.js */
import React from "react"
export default function Loading(props) {
if (props.error) {
return (
<div>
Error! <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.timedOut) {
return (
<div>
Taking a long time... <button onClick={props.retry}>Retry</button>
</div>
)
} else if (props.pastDelay) {
return <div>Loading...</div>
} else {
return null
}
}
自定義組件渲染方式
默認情況下,react-loadable會渲染模塊的默認導出,如果想自定義這個行為,可以傳入render配置渲染。為此我們只需要在配置對象中添加render方法並在高階組件返回的返回組件render方法中添加少許代碼
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout, render } = opts
return class LoadableComponent extends React.Component {
... ...
... ...
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
if(render) {
return render(loaded, this.props)
}
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...props} />
} else {
return null;
}
}
}
}
我們可以在MyComponent.js中添加一個導出的組件來測試功能。
/* MyComponent.js*/
import React from "react"
export class NestedComponent extends React.Component {
render() {
return <div>我是一只大大鳥</div>
}
}
export default class MyComponent extends React.Component {
render() {
return <div>我是一只小小鳥</div>
}
}
/* App.js */
const LoadableComponent = Loadable({
loader: () => import("./MyComponent"),
loading: Loading,
render: (loaded, props) => {
let Component = loaded.NestedComponent;
return <Component {...props}/>;
}
})
下面是完整的Loadable代碼
function Loadable(opts) {
const { loading: LoadingComponent, loader, delay = 200, timeout, render } = opts
return class LoadableComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
error: null,
timedOut: false,
loaded: null,
pastDelay: false
}
}
_loadModule = () => {
// 推遲加載Loading
if (typeof delay === "number") {
if (delay === 0) {
this.setState({ pastDelay: true });
} else {
this._delay = setTimeout(() => {
this.setState({ pastDelay: true });
}, delay);
}
}
// 網絡超時
if (typeof timeout === "number") {
this._timeout = setTimeout(() => {
this.setState({ timedOut: true });
}, timeout);
}
// 異步加載組件
loader()
.then((loaded) => {
this.setState({
loading: false,
loaded
})
this._clearTimeouts()
})
.catch((error) => {
this.setState({
error
})
this._clearTimeouts()
})
}
_clearTimeouts = () => {
this._delay && clearTimeout(this._delay)
this._timeout && clearTimeout(this._timeout);
}
retry = () => {
this.setState({
loading: true,
error: null,
timeOut: false
})
this._loadModule()
}
componentDidMount() {
this._loadModule()
}
componentWillUnmount() {
this._clearTimeouts()
}
render() {
const { loading, error, loaded, pastDelay, timedOut } = this.state
if (loading || error) {
return <LoadingComponent error={error} retry={this.retry} pastDelay={pastDelay} timedOut={timedOut} />
} else if (loaded) {
if(render) {
return render(loaded, this.props)
}
const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
return <LoadedComponent {...this.props} />
} else {
return null;
}
}
}
}
