本文將從以下幾個方面描述React的資源優(yōu)化:
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官網(wǎng)推薦了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 接受一個函數(shù)在孝,這個函數(shù)需要動態(tài)調(diào)用 import()诚啃。它必須返回一個 Promise,該 Promise 需要 resolve 一個 defalut export 的 React 組件私沮。
然后應(yīng)在 Suspense 組件中渲染 lazy 組件始赎,如此使得我們可以使用在等待加載 lazy 組件時做優(yōu)雅降級(如 loading 指示器等)。
- 自定義方式
其實不管是vue還是react,其路由懶加載的實現(xiàn)得益于wepack的異步模塊打包仔燕,webpack會對代碼中異步引入的模塊單獨打包一份造垛,直到真正調(diào)用的時候才去服務(wù)端拿。
const a = () => import('./LoadableComponent')
const a = (r)=>require.ensure([], () => r(require('./LoadableComponent‘)),'chunkname')
以上代碼本質(zhì)是一樣的晰搀,最終返回一個promise對象五辽,其實就是webpack異步模塊打包方法,只有在模塊真正調(diào)用的時候才會加載厕隧。
在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] 占位符排龄,分別支持賦予一個遞增的數(shù)字和實際解析的文件名。Adding this comment will cause our separate chunk to be named [my-chunk-name].js instead of [id].js.
webpackMode:從 webpack 2.6.0 開始翎朱,可以指定以不同的模式解析動態(tài)導(dǎo)入橄维。支持以下選項:
"lazy"(默認(rèn)):為每個 import() 導(dǎo)入的模塊,生成一個可延遲加載(lazy-loadable) chunk拴曲。
"lazy-once":生成一個可以滿足所有 import() 調(diào)用的單個可延遲加載(lazy-loadable) chunk争舞。此 chunk 將在第一次 import() 調(diào)用時獲取,隨后的 import() 調(diào)用將使用相同的網(wǎng)絡(luò)響應(yīng)澈灼。注意竞川,這種模式僅在部分動態(tài)語句中有意義店溢,例如 import(`./locales/${language}.json`),其中可能含有多個被請求的模塊路徑委乌。
"eager":不會生成額外的 chunk床牧,所有模塊都被當(dāng)前 chunk 引入,并且沒有額外的網(wǎng)絡(luò)請求遭贸。仍然會返回 Promise戈咳,但是是 resolved 狀態(tài)久窟。和靜態(tài)導(dǎo)入相對比蝇率,在調(diào)用 import()完成之前,該模塊不會被執(zhí)行桦山。
"weak":嘗試加載模塊算利,如果該模塊函數(shù)已經(jīng)以其他方式加載(即册踩,另一個 chunk 導(dǎo)入過此模塊,或包含模塊的腳本被加載)效拭。仍然會返回 Promise暂吉,但是只有在客戶端上已經(jīng)有該 chunk 時才成功解析。如果該模塊不可用缎患,Promise 將會是 rejected 狀態(tài)慕的,并且網(wǎng)絡(luò)請求永遠不會執(zhí)行。當(dāng)需要的 chunks 始終在(嵌入在頁面中的)初始請求中手動提供挤渔,而不是在應(yīng)用程序?qū)Ш皆谧畛鯖]有提供的模塊導(dǎo)入的情況觸發(fā)肮街,這對于通用渲染(SSR)是非常有用的。
上面說到react的路由中要求必須傳入一個react的component對象判导,然而現(xiàn)在返回的是一個延時的promise對象
嫉父,這個時候我們可以考慮在react的生命周期函數(shù)上做文章,而最終又需要返回一個react組件眼刃,所以我們可以考慮使用高階組件绕辖。具體實現(xiàn)方式如下:
//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(高階函數(shù))
在上面使用了async+await,本質(zhì)上是返回一個promise對象,在promise返回后加載組件擂红,但是用純import也是可以實現(xiàn)這個功能的仪际。
//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(高階函數(shù))
require.ensure() 是 webpack 特有的,已經(jīng)被 import() 取代昵骤。
require.ensure(
dependencies: String[],
callback: function(require),
errorCallback: function(error),
chunkName: String
)
require.ensurer雖然已經(jīng)不推薦使用树碱,但是require.ensure作為原始的懶加載方式,還是可以實現(xiàn)的变秦,具體實現(xiàn)如下:
//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 中排除依賴」的方法惑淳。相反,所創(chuàng)建的 bundle 依賴于那些存在于用戶環(huán)境(consumer's environment)中的依賴饺窿。此功能通常對 library 開發(fā)人員來說是最有用的歧焦,然而也會有各種各樣的應(yīng)用程序用到它。
防止將某些 import 的包(package)打包到 bundle 中肚医,而是在運行時(runtime)再去從外部獲取這些擴展依賴(external dependencies)绢馍。
三、DllPlugin & DllReferencePlugin:
DllPlugin結(jié)合DllRefrencePlugin插件的運用肠套,對將要產(chǎn)出的bundle文件進行拆解打包舰涌,將不需要改動的第三方插件與自己的業(yè)務(wù)代碼進行分開打包,可以很徹底地加快webpack的打包速度你稚,從而在開發(fā)過程中極大地縮減構(gòu)建時間瓷耙。
DllPlugin
這個插件是在一個額外的獨立
的 webpack 設(shè)置中創(chuàng)建一個只有 dll 的 bundle(dll-only-bundle)。 這個插件會生成一個名為manifest.json
的文件刁赖,這個文件是用來讓 [DLLReferencePlugin
]映射到相關(guān)的依賴上去搁痛。
DllPlugin的作用就是做了兩件小事:根據(jù)entry,生成一份vendor.dll文件和生成一份manifest.json文件
DllReferencePlugin
這個插件是在 webpack主配置文件
中設(shè)置的宇弛, 這個插件把只有 dll 的 bundle(們)(dll-only-bundle(s)) 引用到需要的預(yù)編譯的依賴鸡典。
DllPlugin和DllRefrencePlugin需要配合使用,它們一個負(fù)責(zé)生成dll文件枪芒,一個負(fù)責(zé)使用dll文件彻况,DllPlugin的需要一個單獨的webpack配置文件,這個配置文件告訴webpack-cli應(yīng)該如何打包這個dll舅踪,而DllRefrencePlugin是在主配置文件中使用纽甘。
- 使用步驟
- 配置一份webpack配置文件,用于生成動態(tài)鏈接庫硫朦。
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()
})
]
}
- 使用動態(tài)鏈接庫贷腕,黃金搭檔
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優(yōu)化,使用于將項目依賴的基礎(chǔ)模塊(第三方模塊)抽離出來咬展,然后打包到一個個單獨的動態(tài)鏈接庫中泽裳。當(dāng)下一次打包時,通過ReferencePlugin破婆,如果打包過程中發(fā)現(xiàn)需要導(dǎo)入的模塊存在于某個動態(tài)鏈接庫中涮总,就不能再次被打包,而是去動態(tài)鏈接庫中g(shù)et到祷舀。
DllPlugin實際上也是屬于公共代碼提取的范疇瀑梗,但與CommonsChunkPlugin不一樣的是烹笔,它不僅僅是把公用代碼提取出來放到一個獨立的文件供不同的頁面來使用,它更重要的一點是:把公用代碼和它的使用者(業(yè)務(wù)代碼)從編譯這一步就分離出來抛丽。