本文将从以下几个方面描述React的资源优化:
1. Code Splitting;
2. externals&CDN;
3. DllPlugin;
版本信息
"webpack": "4.29.6",
"react": "16.8.6",
"react-router": "5.0.0",
一、路由Code Splitting:
- @loadable/component
One great feature of the web is that we don’t have to make our visitors download the entire app before they can use it. You can think of code splitting as incrementally downloading the app. To accomplish this we’ll use webpack,
@babel/plugin-syntax-dynamic-import
, andloadable-components
.
在react-router@5官方文档中推荐了"@loadable/component"做Code Splitting,具体使用如下:
{
"presets": ["@babel/preset-react"],
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
import loadable from "@loadable/component";
import Loading from "./Loading.js";
const LoadableComponent = loadable(() => import("./Dashboard.js"), {
fallback: <Loading />
});
export default class LoadableDashboard extends React.Component {
render() {
return <LoadableComponent />;
}
}
- react的React.lazy+ Suspense
react官网推荐了React.lazy方式进行Code Splitting,具体使用如下:
import { lazy, Suspense } from 'react';
const LoadableComponent = lazy(() => import("./Dashboard.js"));
export default class LoadableDashboard extends React.Component {
render() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LoadableComponent />;
</Suspense>
</div>
)
}
}
React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。
然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。
- 自定义方式
其实不管是vue还是react,其路由懒加载的实现得益于wepack的异步模块打包,webpack会对代码中异步引入的模块单独打包一份,直到真正调用的时候才去服务端拿。
const a = () => import('./LoadableComponent')
const a = (r)=>require.ensure([], () => r(require('./LoadableComponent‘)),'chunkname')
以上代码本质是一样的,最终返回一个promise对象,其实就是webpack异步模块打包方法,只有在模块真正调用的时候才会加载。
在vue-router中我们只要在路由配置的component中直接传入() => import('./LoadableComponent')即可。而在react的路由中,它路由配置中的component必须传入react 的component对象。所以需要对返回promise对象进行处理。
- es6+import+async高阶组件
import('path/to/module') -> Promise
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
'module'
);
webpackChunkName:新 chunk 的名称。从 webpack 2.6.0 开始,[index] and [request] 占位符,分别支持赋予一个递增的数字和实际解析的文件名。Adding this comment will cause our separate chunk to be named [my-chunk-name].js instead of [id].js.
webpackMode:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入。支持以下选项:
"lazy"(默认):为每个 import() 导入的模块,生成一个可延迟加载(lazy-loadable) chunk。
"lazy-once":生成一个可以满足所有 import() 调用的单个可延迟加载(lazy-loadable) chunk。此 chunk 将在第一次 import() 调用时获取,随后的 import() 调用将使用相同的网络响应。注意,这种模式仅在部分动态语句中有意义,例如 import(`./locales/${language}.json`),其中可能含有多个被请求的模块路径。
"eager":不会生成额外的 chunk,所有模块都被当前 chunk 引入,并且没有额外的网络请求。仍然会返回 Promise,但是是 resolved 状态。和静态导入相对比,在调用 import()完成之前,该模块不会被执行。
"weak":尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于通用渲染(SSR)是非常有用的。
上面说到react的路由中要求必须传入一个react的component对象,然而现在返回的是一个延时的promise对象
,这个时候我们可以考虑在react的生命周期函数上做文章,而最终又需要返回一个react组件,所以我们可以考虑使用高阶组件。具体实现方式如下:
//lazyLoad.js
import React from 'react';
export default function lazyLoad(componentfn) {
class LazyloadComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
component: null
}
}
async componentWillMount() {
const { component} = await componentfn();
this.setState({component})
}
render() {
const C = this.state.component;
return C ? <C {...this.props}/> : null;
}
}
return LazyloadComponent;
}
//router.js
import lazyLoad from './lazyLoad'
const a = lazyLoad(() => import("./LoadableComponent"))
- es6+纯import(高阶函数)
在上面使用了async+await,本质上是返回一个promise对象,在promise返回后加载组件,但是用纯import也是可以实现这个功能的。
//lazyLoad.js
import React from 'react';
export default function lazyLoad(componentfn) {
class LazyloadComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
component: null
}
}
componentWillMount() {
this.load();
}
load(){
componentfn().then((Com)=>{ //组件加载完成时
this.setState({
component:Com.default?Com.default:null
});
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props}/> : null;
}
}
return LazyloadComponent;
}
- es6+require.ensure(高阶函数)
require.ensure() 是 webpack 特有的,已经被 import() 取代。
require.ensure(
dependencies: String[],
callback: function(require),
errorCallback: function(error),
chunkName: String
)
require.ensurer虽然已经不推荐使用,但是require.ensure作为原始的懒加载方式,还是可以实现的,具体实现如下:
//lazyLoad.js
import React from 'react';
export default function lazyLoad(componentfn) {
class LazyloadComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
component: null
}
}
componentWillMount() {
this.load();
}
load(){
new Promise((resolve,reject)=>{
require.ensure([], function(require) {//[]依赖项
var c = componentfn().default;
resolve(c);
});
}).then((data)=>{
this.setState({
Com:data
});
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props}/> : null;
}
}
return LazyloadComponent;
}
//router.js
import lazyLoad from './lazyLoad'
const a = lazyLoad(() => require("./LoadableComponent"))
二、externals&CDN:
externals可以防止将某些import的包打包进bundle中,而是在运行时再去外部(script的方式)获取这些扩展依赖,这样会减少bundle包大小。
先说使用方法,基本上分三步:
- 引入cdn library资源
- 配置 webpack externals
- 文件中引用library
//index.html
<head>
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
</head>
// webpack.config.js
module.exports = {
externals: {
'react': 'React',// '包名':'全局变量'
'react-dom': 'ReactDOM'
}
}
// 包名react指的是 `import React from 'react'`中的'react'
//全局变量React指的是react暴露出来的全局对象名
//index.js
import React from 'react'
下面引用时webpack官方文档,对externals对说明:
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
三、DllPlugin & DllReferencePlugin:
DllPlugin结合DllRefrencePlugin插件的运用,对将要产出的bundle文件进行拆解打包,将不需要改动的第三方插件与自己的业务代码进行分开打包,可以很彻底地加快webpack的打包速度,从而在开发过程中极大地缩减构建时间。
DllPlugin
这个插件是在一个额外的独立
的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。 这个插件会生成一个名为manifest.json
的文件,这个文件是用来让 [DLLReferencePlugin
]映射到相关的依赖上去。
DllPlugin的作用就是做了两件小事:根据entry,生成一份vendor.dll文件和生成一份manifest.json文件
DllReferencePlugin
这个插件是在 webpack主配置文件
中设置的, 这个插件把只有 dll 的 bundle(们)(dll-only-bundle(s)) 引用到需要的预编译的依赖。
DllPlugin和DllRefrencePlugin需要配合使用,它们一个负责生成dll文件,一个负责使用dll文件,DllPlugin的需要一个单独的webpack配置文件,这个配置文件告诉webpack-cli应该如何打包这个dll,而DllRefrencePlugin是在主配置文件中使用。
- 使用步骤
- 配置一份webpack配置文件,用于生成动态链接库。
const path = require('path')
const webpack = require('webpack')
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')
// dll文件存放的目录
const dllPath = 'public/vendor'
module.exports = {
entry: {
vendor: ['antd','react-redux','redux','redux-thunk','axios']
},
output: {
path: path.join(__dirname, dllPath),
filename: '[name].dll.js',
library: '[name]_[hash]'
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
path: path.join(__dirname, dllPath, '[name]-manifest.json'),
name: '[name]_[hash]',
context: process.cwd()
})
]
}
- 使用动态链接库,黄金搭档
DllReferencePlugin
externals:{
'react': 'React',// '包名':'全局变量'
'react-dom': 'ReactDOM'
},
plugins: [
new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: require('./public/vendor/vendor-manifest.json')
}),
- 在html中引用dll文件
<body>
<div id="app"></div>
<script src="./public/vendor/vendor.dll.js"></script>
</body>
DllPlugin优化,使用于将项目依赖的基础模块(第三方模块)抽离出来,然后打包到一个个单独的动态链接库中。当下一次打包时,通过ReferencePlugin,如果打包过程中发现需要导入的模块存在于某个动态链接库中,就不能再次被打包,而是去动态链接库中get到。
DllPlugin实际上也是属于公共代码提取的范畴,但与CommonsChunkPlugin不一样的是,它不仅仅是把公用代码提取出来放到一个独立的文件供不同的页面来使用,它更重要的一点是:把公用代码和它的使用者(业务代码)从编译这一步就分离出来。