[译]react-loadable

1,322 阅读13分钟

Also See:

  • react-loadable-visibility - 在“react loadable”的基础上构建并保持与“react loadable”相同的API,此库使您能够加载屏幕上可见的内容。

GUIDE


指南

当你把React应用程序和Webpack捆绑在一起,一切都很顺利。但是有一天你会发现你的应用程序包越来越大,以至于它正在减慢速度。

是时候开始拆分你的应用程序了!

A single giant bundle vs multiple smaller bundles

代码拆分是将一个包含整个应用程序的大捆绑包拆分为多个包含应用程序单独部分的小捆绑包的过程。

这看起来很难用Webpack来设计,但它很难做到这一点。

基于路由的拆分与基于组件的拆分

一个常见的建议是将你的应用程序分成不同的路由,并异步加载每个路由。对于许多应用程序来说,这似乎足够好用了——作为一个用户,点击一个链接等待页面加载是一种熟悉的web体验。

但我们可以做得更好。

使用大多数路由工具进行React时,路由只是一个组件。他们没有什么特别的地方(对不起,Ryan和Michael,您很特别)。那么,如果我们优化了围绕组件而不是路由进行拆分呢?那会给我们带来什么?

Route vs. component centric code splitting

事实证明:相当多。有很多地方不仅仅是路线,你可以很容易地将你的应用程序分开。Modals、tabs和更多的UI组件会隐藏内容,直到用户做了一些事情来显示内容。

**示例:**可能你的应用程序在选项卡组件中隐藏了一个地图。为什么每次当用户可能永远不会访问父路由时,都会为父路由加载一个巨大的映射库?

更不用说所有可以推迟加载内容的位置,直到更高优先级的内容加载完毕。页面底部的那个组件加载了一堆库:为什么要和顶部的内容同时加载呢?

由于路由只是组件,所以我们仍然可以在路由级别轻松地进行代码拆分。

在你的应用中引入新的代码分割点应该很容易,你不会再三考虑。这应该是一个改变几行代码的问题,其他一切都应该是自动化的。

引入 React Loadable

React Loadable是一个很小的库,它使以组件为中心的代码拆分在React中非常容易。

Loadable是一个高阶组件(创建组件的函数),它允许您在将任何模块呈现到应用程序之前动态加载它。

让我们设想两个组件,一个导入并呈现另一个。

import Bar from './components/Bar'; 
class Foo extends React.Component {  
    render() {    
        return <Bar/>;  
    }
}

现在我们依赖于“Bar”通过“import”同步导入,但在呈现之前我们不需要它。那我们为什么不推迟呢?

使用动态导入tc39提案目前处于第3阶段)我们可以修改组件以异步加载“Bar”。

class MyComponent extends React.Component {
	state = {    
        Bar: null  
    };   
	componentWillMount() {    
        import('./components/Bar')
            .then(
            	Bar => {      
                    this.setState(
                        { 
                            Bar 
                        });    
                });  
    }   
	render() {    
        let {Bar} = this.state;    
        if (!Bar) {      
            return <div>Loading...</div>;    
        } else {      
            return <Bar/>;    
        };  
    }
}

但这是一大堆工作,它甚至不能处理一堆案件。当“import()”失败时呢?服务器端渲染呢?

相反,您可以使用“Loadable”来抽象问题。

import Loadable from 'react-loadable'; 
const LoadableBar = Loadable({  
    loader: () => import('./components/Bar'),  
    loading() {    
    	return <div>Loading...</div>  
	}
}); 
class MyComponent extends React.Component {  
    render() {    
        return <LoadableBar/>;  
    }
}

import()上的自动代码拆分

将“import()`与Webpack 2+一起使用时,它将自动代码拆分不需要额外的配置。

这意味着您只需切换到“import()”并使用React Loadable,就可以轻松试验新的代码拆分点。找出最适合你的应用程序的性能。

创建一个很棒的“加载…”组件

呈现静态“加载…”与用户的通信不够。您还需要考虑错误状态、超时,并使之成为一种美好的体验。

function Loading() {  
    return <div>Loading...</div>;
} 
Loadable({  
    loader: () => import('./WillFailToLoad'), // oh no!  loading: Loading,
});

为了让这一切变得更好,你的加载组件收到一些不同的props

加载失败

当你的组件loader 失败(import失败), 你的 loading component 将会收到一个 error prop, 它将是一个“Error”对象(否则它将是“null”)。

function Loading(props) {  
    if (props.error) {    
        return <div>
            Error! 
            <button onClick={ props.retry }>
                Retry
            </button>
        </div>;  
    } else {    
        return <div>Loading...</div>;  
    }
}

避免加载组件闪烁

有时组件加载非常快(<200ms),loading组件只会在屏幕上快速闪烁。

大量的用户研究已经证明,这会导致用户感知事物的时间比实际时间长。如果你什么都不显示,用户会认为它更快。

所以你的加载组件也会得到一个pastDelayprop只有当组件的加载时间比设置的delay长时才会出现这种情况

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;  
    }
}

此延迟默认为“200ms”,但您也可以自定义 delay

Loadable({  
    loader: () => import('./components/Bar'),  
    loading: Loading,  
    delay: 300, // 0.3 seconds
});

当“loader”花费太长时间时超时

有时网络连接很糟糕,从不解决或失败,它们只是永远挂在那里。这对用户来说太糟糕了,因为他们不知道是应该一直花这么长时间,还是应该尝试刷新。

loading component 将收到一个 timedOut proploader 超时.

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;  
    }
}

但是,此功能在默认情况下处于禁用状态。要打开它,可以传递一个 timeout option .

Loadable({  
    loader: () => import('./components/Bar'),  
    loading: Loading,  
    timeout: 10000, // 10 seconds
});

自定义渲染

默认情况下,“Loadable”将呈现返回模块的“default”导出。如果要自定义此行为,可以使用 render option.

Loadable({  
    loader: () => import('./my-component'),  
    render(loaded, props) {    
    	let Component = loaded.namedExport;   
    	return <Component {...props}/>;  
	}
});

加载多个资源

从技术上讲只要loader() 返回任意一个promise并且 能够渲染某些内容,您就可以做任何想要的事情. 但写出来可能有点烦人。

为了便于并行加载多个资源,可以使用Loadable.Map.

Loadable.Map({  
    loader: {    
        Bar: () => import('./Bar'),    
        i18n: () => fetch('./i18n/bar.json').then(res => res.json()),  
	},  
    render(loaded, props) {    
        let Bar = loaded.Bar.default;    
        let i18n = loaded.i18n;    
        return <Bar {...props} i18n={i18n}/>;  
    },
});

使用 Loadable.Map render() 方法 是必须的. 它将被传递一个“loaded”参数, 该参数将是与loader形状匹配的对象。

预加载

作为一种优化,您还可以决定在呈现组件之前预加载组件。

例如,如果您需要在按下按钮时加载新组件,则可以在用户将鼠标悬停在按钮上时开始预加载组件。

通过Loadable创建的组件的 静态方法 preload 可以实现这样的效果

const LoadableBar = Loadable({  
    loader: () => import('./Bar'),  
    loading: Loading,
}); 
class MyComponent extends React.Component {  
    state = { showBar: false };   
	onClick = () => {    
        this.setState({ showBar: true });  
    };   
	onMouseOver = () => {    
        LoadableBar.preload();  
    };   
	render() {    
        return (      
            <div>        
                <button          
                    onClick={this.onClick}          
                    onMouseOver={this.onMouseOver}
                >          
                    Show Bar        
                </button>        
                {this.state.showBar && <LoadableBar/>}      
            </div>    
        )  
    }
}

Server-Side Rendering

SERVER SIDE RENDERING


服务器端渲染

当你去呈现所有这些动态加载的组件时,你会得到一大堆加载屏幕。

这真的很糟糕,但好消息是React Loadable的设计目的是使服务器端呈现像没有动态加载一样工作。

这是我们使用Express的启动服务器。

import express from 'express';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import App from './components/App'; 
const app = express(); 
app.get('/', (req, res) => {  
    res.send(`    
		<!doctype html>    
		<html lang="en">      
			<head>...</head>      
			<body>        
				<div id="app">${ReactDOMServer.renderToString(<App/>)}</div>        					<script src="/dist/main.js"></script>      
			</body>    
		</html>  
	`);}); 
app.listen(3000, () => {  
    console.log('Running on http://localhost:3000/');
});

在服务器上预加载所有可加载组件

从服务器渲染正确内容的第一步是确保在渲染时,所有可加载组件均已加载。

为此,您可以使用Loadable.preloadAll 方法。它返回一个promise,当所有可加载组件准备就绪时,该promise将会resolve。

Loadable.preloadAll().then(() => {  
    app.listen(3000, () => {    
        console.log('Running on http://localhost:3000/');  
    });
});

在客户端上拾取服务器端渲染的应用程序

这是有些棘手的地方。因此,让我们为自己做一点准备。

为了使我们能够提取从服务器渲染的内容,我们需要拥有用于服务器渲染的所有相同代码。

为此,我们首先需要可加载组件告诉我们它们正在渲染哪个模块。

声明正在加载的模块

有两个选择 LoadableLoadable.Map ,被用于告诉我们组件要加载哪些模块: opts.modulesopts.webpack.

Loadable({  
    loader: () => import('./Bar'),  
    modules: ['./Bar'],  
    webpack: () => [require.resolveWeak('./Bar')],
});

但不要太担心这些选项。React Loadable包含一个 Babel插件来为您添加它们。

只需将react-loadable/babel插件添加到您的Babel配置中即可:

{  
    "plugins": [    
        "react-loadable/babel"  
    ]
}

现在,将自动提供这些选项。

找出要渲染的动态模块

接下来,我们需要找出发出请求时实际渲染了哪些模块。

为此,有一个Loadable.Capture组件可用于收集所有已渲染的模块。

import Loadable from 'react-loadable'; 
app.get('/', (req, res) => {  
    let modules = [];   
    let html = ReactDOMServer.renderToString(    
        <Loadable.Capture report={moduleName => modules.push(moduleName)}>      
            <App/>   
        </Loadable.Capture>  
    );   
    console.log(modules);   
    res.send(`...${html}...`);
});

将加载的模块映射到包

为了确保客户端加载在服务器端呈现的所有模块,我们需要将它们映射到Webpack创建的包。

这分为两个部分。

首先,我们需要Webpack告诉我们每个模块位于哪个包中。为此,有一个React Loadable Webpack插件

导入ReactLoadablePluginfromreact-loadable/webpack并将其包括在您的webpack配置中。将其传递给filename用于存储有关我们包的JSON数据的位置。

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack'; 
export default {  
    plugins: [    
        new ReactLoadablePlugin({      
            filename: './dist/react-loadable.json',    
        }),  
    ],
};

然后,我们将返回到服务器,并使用此数据将模块转换为包。

要将模块转换为包,请从Webpack导入getBundles 方法react-loadable/webpack和从Webpack导入数据。

import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack';
import stats from './dist/react-loadable.json'; 
app.get('/', (req, res) => {  
    let modules = [];   
    let html = ReactDOMServer.renderToString(    
        <Loadable.Capture report={moduleName => modules.push(moduleName)}>      
            <App/>    
        </Loadable.Capture>  
    );   
    let bundles = getBundles(stats, modules);   // ...
});

然后,我们可以将这些捆绑包呈现为``HTML中的标签。

重要的是,捆绑包必须包含主捆绑包之前,以便浏览器可以在呈现应用程序之前先加载捆绑包。

但是,由于Webpack清单(包括解析包的逻辑)位于主包中,因此需要将其提取到自己的块中。

使用CommonsChunkPlugin可以轻松做到这一点

// webpack.config.js
export default {  
    plugins: [    
        new webpack.optimize.CommonsChunkPlugin({      
            name: 'manifest',      
            minChunks: Infinity    
        })  
    ]
}

注意:从Webpack 4开始,CommonsChunkPlugin已被删除,清单不再需要提取。

let bundles = getBundles(stats, modules); 
res.send(`  
	<!doctype html>  
	<html lang="en">    
		<head>...</head>    
		<body>      
			<div id="app">${html}</div>      
			<script src="/dist/manifest.js"></script>      
			${bundles.map(bundle => {        
    			return `<script src="/dist/${bundle.file}"></script>`        
                // alternatively if you are using publicPath option in webpack config        			  // you can use the publicPath value from bundle, e.g:        
                // return `<script src="${bundle.publicPath}"></script>`    }).join('\n')}      
                <script src="/dist/main.js"></script>    
         </body>  
     </html>
`);

在客户端上预加载就绪的可加载组件

我们可以Loadable.preloadReady()在客户端上使用该方法来预加载页面中包含的可加载组件。

Loadable.preloadAll()一样, 它返回一个 promise, 在解决方案上这意味着我们可以为我们的应用程序填充

// src/entry.js
import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import App from './components/App'; 
Loadable.preloadReady().then(() => {  
    ReactDOM.hydrate(<App/>, document.getElementById('app'));
}); 

Now server-side rendering should work perfectly!

API DOCSAPI Docs

Loadable

一个高阶组件,用于在呈现模块之前动态加载模块 ,而在模块不可用时呈现加载(loading)组件。

const LoadableComponent = Loadable({  
    loader: () => import('./Bar'),  
    loading: Loading,  
    delay: 200,  
    timeout: 10000,});

返回一个LoadableComponent

Loadable.Map

一个高阶组件,使您可以并行加载多个资源。

Loadable.Map的opts.loader接受函数的对象,并需要一个opts.render方法。

Loadable.Map({  
    loader: {    
        Bar: () => import('./Bar'),    
        i18n: () => fetch('./i18n/bar.json').then(res => res.json()),  
	},  
    render(loaded, props) {    
        let Bar = loaded.Bar.default;    
        let i18n = loaded.i18n;    
        return <Bar {...props} i18n={i18n}/>;  
    }
});

Loadable.Maprender()方法的loaded参数是一个与loader形状相同的对象。

Loadable and Loadable.Map Options

opts.loader

返回加载模块的promise

Loadable({  
    loader: () => import('./Bar'),
});

Loadable.Map中,loader是一个对象。

Loadable.Map({  
    loader: {    
        Bar: () => import('./Bar'),    
        i18n: () => fetch('./i18n/bar.json').then(res => res.json()),  
	},
});

Loadable.Map还需要传递一个 opts.render函数。

opts.loading

在模块加载过程中或加载出错时呈现的loading组件(LoadingComponent )。

Loadable({  
    loading: LoadingComponent,
});

此选项是必需的,如果您不想渲染任何东西,请返回null

Loadable({  
    loading: () => null,
});

opts.delay

延迟时间(以毫秒为单位),将作为属性props.pastDelay传递到loading 组件的。默认为200

Loadable({  
    delay: 200
});

了解有关延迟的更多信息

opts.timeout

超时时间(以毫秒为单位),将作为属性 props.timedOut传递到loading组件。默认情况下是关闭的。

Loadable({  
	timeout: 10000
});

了解有关超时的更多信息

opts.render

用于自定义已加载模块的呈现的函数。

接收opts.loader的解析值loaded 和传递给 LoadableComponentprops

Loadable({
    render(loaded, props) {   
    	let Component = loaded.default;    
    	return <Component {...props}/>;  
    }
});

opts.webpack

一个可选函数,它返回一个可以使用的Webpack模块ID数组require.resolveWeak

Loadable({  
    loader: () => import('./Foo'),  
    webpack: () => [require.resolveWeak('./Foo')],
});

该选项可以通过Babel插件自动执行。

opts.modules

一个可选数组,其中包含用于导入的模块路径。

Loadable({  
    loader: () => import('./my-component'),  
    modules: ['./my-component'],
});

该选项可以通过Babel插件自动执行。

LoadableComponent

通过LoadableLoadable.Map 返回的组件

const LoadableComponent = Loadable({  
    // ...
});

传递给该组件的props将直接通过opts.render传递给动态加载的组件。

LoadableComponent.preload()

这是“LoadableComponent”上的静态方法,可用于提前加载组件。

const LoadableComponent = Loadable({...}); 
LoadableComponent.preload();

这将返回一个promise,但是您应该避免等待该promise解析来更新UI。在大多数情况下,它会造成糟糕的用户体验。

了解有关预加载的更多信息

LoadingComponent

通过opts.loading传递的组件

function LoadingComponent(props) {  
    if (props.error) {    
        // When the loader has errored    
        return <div>Error! <button onClick={ props.retry }>Retry</button></div>;  
    } else if (props.timedOut) {    
        // When the loader has taken longer than the timeout    
        return <div>Taking a long time... <button onClick={ props.retry }>Retry</button></div>;  
    } else if (props.pastDelay) {    
        // When the loader has taken longer than the delay    
        return <div>Loading...</div>;  
    } else {    
        // When the loader has just started    
        return null;  
    }
} 
Loading({  
    loading: LoadingComponent,
});

阅读有关加载组件的更多信息

props.error

loader 失败时,传递给LoadableComponentError对象。如果没有错误,则传递null

function LoadingComponent(props) {  
    if (props.error) {    
        return <div>Error!</div>;  
    } else {    
        return <div>Loading...</div>;  
    }
}

了解有关错误的更多信息

props.retry

loader 失败时,传递给LoadableComponentprop retry, 是一个函数,用于重新加载组件。

function LoadingComponent(props) {  
    if (props.error) {    
        return <div>Error! <button onClick={ props.retry }>Retry</button></div>;  
    } else {    
        return <div>Loading...</div>;  
    }
}

了解有关错误的更多信息

props.timedOut

在设置 timeout后传递给 LoadingComponent的布尔属性

function LoadingComponent(props) {  
    if (props.timedOut) {    
        return <div>Taking a long time...</div>;  
    } else {    
        return <div>Loading...</div>;  
    }
}

了解有关超时的更多信息

props.pastDelay

在设置delay后传递给 LoadingComponent的布尔属性

function LoadingComponent(props) {  
    if (props.pastDelay) {    
        return <div>Loading...</div>;  
    } else {    
        return null;  
    }
}

了解有关延迟的更多信息

Loadable.preloadAll()

这将递归调用所有的LoadableComponent.preload 方法,直到它们都被解析。允许您在服务器等环境中预加载所有动态模块。

Loadable.preloadAll().then(() => {  
    app.listen(3000, () => {    
        console.log('Running on http://localhost:3000/');  
    });
});

重要的是要注意,这要求您在初始化模块时而不是在渲染应用程序时声明所有可加载组件。

Good:

// During module initialization...
const LoadableComponent = Loadable({...}); 
class MyComponent extends React.Component {  
      componentDidMount() {    // ...  
	  }
}

Bad:

// ... 
class MyComponent extends React.Component {  
    componentDidMount() {    
        // During app render...    
        const LoadableComponent = Loadable({...});  
    }
}

注意: 如果您的应用中有多个副本Loadable.preloadAll(),则将无法使用react-loadable

阅读有关在服务器上进行预加载的更多信息

Loadable.preloadReady()

检查浏览器中已经加载的模块,然后调用匹配的 LoadableComponent.preload方法。

Loadable.preloadReady().then(() => {  
    ReactDOM.hydrate(<App/>, document.getElementById('app'));
});

了解有关在客户端上进行预加载的更多信息

Loadable.Capture

用于报告呈现了哪些模块的组件。

接受一个通过React Loadable渲染的report 函数,这个函数接受moduleName

let modules = []; 
let html = ReactDOMServer.renderToString(  
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>    
        <App/>  
    </Loadable.Capture>
); 
console.log(modules);

阅读有关捕获渲染模块的更多信息

Babel Plugin

提供opts.webpack以及opts.modules对于每个可加载的组件,都需要做大量的手动工作。

相反,您可以将Babel插件添加到您的配置中,它将自动为您执行以下操作:

{  
	"plugins": ["react-loadable/babel"]
}

Input

import Loadable from 'react-loadable'; 
const LoadableMyComponent = Loadable({  
    loader: () => import('./MyComponent'),
}); 
const LoadableComponents = Loadable.Map({  
    loader: {    
    	One: () => import('./One'),    
      	Two: () => import('./Two'),  
	},
});

Output

import Loadable from 'react-loadable';
import path from 'path'; 
const LoadableMyComponent = Loadable({  
    loader: () => import('./MyComponent'),  
    webpack: () => [require.resolveWeak('./MyComponent')],  
    modules: [path.join(__dirname, './MyComponent')],
}); 
const LoadableComponents = Loadable.Map({  
    loader: {    
        One: () => import('./One'),    
        Two: () => import('./Two'),  
    },  
    webpack: () => [require.resolveWeak('./One'), require.resolveWeak('./Two')],
    modules: [path.join(__dirname, './One'), path.join(__dirname, './Two')],
});

阅读有关声明模块的更多信息

Webpack Plugin

为了在渲染服务器端时向下发送正确的包,您将需要React Loadable Webpack插件为您提供模块到包的映射。

// webpack.config.js
import { ReactLoadablePlugin } from 'react-loadable/webpack'; 
export default {  
    plugins: [    
        new ReactLoadablePlugin({      
            filename: './dist/react-loadable.json',    
        }),  
    ],
};

这将创建一个文件(opts.filename),您可以导入该文件以将模块映射到包。

阅读有关将模块映射到包的更多信息

getBundles

“react loadable/webpack”导出用于将模块转换为bundle的方法。

import { getBundles } from 'react-loadable/webpack'; 
let bundles = getBundles(stats, modules);

阅读有关将模块映射到包的更多信息

FAQFAQ



FAQ


FAQ

如何避免重复?

每次使用Loadable()时指定相同的loading组件或delay会很快重复。相反,您可以用自己的高阶组件(HOC)包装Loadable,以设置默认选项。

import Loadable from 'react-loadable';
import Loading from './my-loading-component'; 
export default function MyLoadable(opts) {  
    return Loadable(Object.assign({    
        loading: Loading,    
        delay: 200,    
        timeout: 10000,  
    }, opts));
};

然后,您可以在使用时指定一个oader

import MyLoadable from './MyLoadable'; 
const LoadableMyComponent = MyLoadable({  
    loader: () => import('./MyComponent'),
}); 
export default class App extends React.Component {  
    render() {    
    	return <LoadableMyComponent/>;  
	}
}

不幸的是,目前使用包装的Loadable会中断react-loadable / babel,因此在这种情况下,您必须手动添加所需的属性(moduleswebpack)。

import MyLoadable from './MyLoadable'; 
const LoadableMyComponent = MyLoadable({  
    loader: () => import('./MyComponent'),  
    modules: ['./MyComponent'],  
    webpack: () => [require.resolveWeak('./MyComponent')],
}); 
export default class App extends React.Component {  
    render() {    
        return <LoadableMyComponent/>; 
    }
}

如何通过服务器端渲染处理其他样式.css或sourcemaps.map

当您调用getBundles时,根据您的Webpack配置,它可能会返回JavaScript以外的文件类型。

要解决此问题,您应该手动过滤掉您关心的文件扩展名:

let bundles = getBundles(stats, modules); 
let styles = bundles.filter(bundle => bundle.file.endsWith('.css'));
let scripts = bundles.filter(bundle => bundle.file.endsWith('.js')); 
res.send(`  
	<!doctype html>  
	<html lang="en">    
		<head>      
			...      
			${styles.map(style => {        
    			return `<link href="/dist/${style.file}" rel="stylesheet"/>`      }).join('\n')}    
		</head>    
		<body>      
			<div id="app">${html}</div>      
			<script src="/dist/main.js"></script>      
			${scripts.map(script => {        
    			return `<script src="/dist/${script.file}"></script>`      
}).join('\n')}    
		</body>  
	</html>
`);