Ba la la la ~ 讀者朋友們,你們好啊,又到了冷鋒時間,話不多說,發車!
React 組件代碼分割和加載
當你的應用足夠龐大時,把所有代碼簡單地打成一個 bundle,啟動時間會很長。你需要將 app 分割成幾個 bundle,按需加載。
A single giant bundle vs. multiple smaller bundles
Browserify 和Webpack 等工具可以很好地解決如何將一個大 bundle 分割的問題。
那么你就需要決定在哪兒可以分離出另一個 bundle 進行異步加載。App 還需要在加載時給用戶提示。
基于路由的分割 vs 基于組件的分割
通常的建議是將 app 分成獨立的路徑,然后每個異步加載。這對大多 app 都適用,點擊鏈接然后加載一個新的頁面,這種體驗還可以。
但是我們可以做得更好。
React 的多數路由工具都是一個路徑就是一個組件。沒什么特別的。如果我們在組件上進行優化而不是讓路徑來負責這個任務會怎樣呢?
Route vs. component centric code splitting
顯然組件的方式更好些。你可以輕松地在更多地方分割 app,Modals、tabs以及很多用戶觸發才展示內容的 UI 組件等,而不僅是路徑。
更不用說那些延遲加載直到高優先級的內容加載完的地方。頁面底部的組件加載一堆庫:為什么在頂部時就要加載那些庫呢?
你也可以簡單地按路由分割,因為它們也是組件。看哪種方式更適合你的 app 了。
但是我們需要讓組件級分割和路由一樣簡單。新的分割應該改幾行代碼就可以了,其它都會自動完成。
React Loadable 介紹
大家都說組件分割很難實現,然后我就寫了一個小庫——React Loadable。
Loadable 是一款可以輕松分割組件級 bundle 的高階組件(創建組件的函數)。
假設有兩個組件,其中一個引入并渲染另一個。
import AnotherComponent from './another-component';
class MyComponent extends React.Component {
render() {
return <AnotherComponent/>;
}
}
目前通過 import 同步引入 AnotherComponent 這個依賴。我們需要一種可以異步加載的方式。
dynamic import(目前處于第 3 階段的 tc39 提議)可以使組件異步加載 AnotherComponent。
class MyComponent extends React.Component {
state = {
AnotherComponent: null
};
componentWillMount() {
import('./another-component').then(AnotherComponent => {
this.setState({ AnotherComponent });
});
}
render() {
let {AnotherComponent} = this.state;
if (!AnotherComponent) {
return <div>Loading...</div>;
} else {
return <AnotherComponent/>;
};
}
}
但是這需要一系列的人為操作,而且有許多不同的場景無法適用。import() 失敗了怎么辦呢?服務端渲染呢?
這個問題可以用 Loadable 進行抽象。Loadable 使用起來很簡單,只要傳入加載組件的函數和加載組件過程中展示的“Loading”組件就可以了。
import Loadable from 'react-loadable';
function MyLoadingComponent() {
return <div>Loading...</div>;
}
const LoadableAnotherComponent = Loadable({
loader: () => import('./another-component'),
LoadingComponent: MyLoadingComponent
});
class MyComponent extends React.Component {
render() {
return <LoadableAnotherComponent/>;
}
}
但是如果組件加載失敗了呢?我們還需要有 error 狀態。
為了給你最大的控制權,決定什么時候展示什么,error 只會簡單地作為 LoadingComponent 的屬性拋出。
function MyLoadingComponent({ error }) {
if (error) {
return <div>Error!</div>;
} else {
return <div>Loading...</div>;
}
}
import() 自動分割代碼
import() 的一個優點是增加新代碼時,Webpack 2 可以自動分割代碼。
也就是說你只要使用 React Loadable、改用 import(),就可以輕松地用新的代碼分割點進行試驗,來看看哪種方法最適合你的應用。
此處可以查看示例項目,或者查閱 Webpack 2 文檔(注:一些相關文檔位于 require.ensure() 章節)。
Loading 組件避免一閃而過
有時組件加載很快(<200ms),loading 屏只在屏幕上一閃而過。
一些用戶研究已證實這會導致用戶花更長的時間接受內容。如果不展示任何 loading 內容,用戶會接受得更快。
所以 loading 組件有一個 pastDelay 屬性,僅在組件加載時間超過設置的 delay 時值為 true。
export default function MyLoadingComponent({ error, pastDelay }) {
if (error) {
return <div>Error!</div>;
} else if (pastDelay) {
return <div>Loading...</div>;
} else {
return null;
}
}
delay 默認是 200ms,可以向 Loadable 傳遞第 3 個參數自定義 delay。
Loadable({
loader: () => import('./another-component'),
LoadingComponent: MyLoadingComponent,
delay: 300
});
預加載
你也可以在組件渲染前預加載進行優化。
例如需要在點擊按鈕時加載新的組件,就可以在用戶懸浮在按鈕上時預加載組件。
Loadable 創建的組件會暴露一個 preload 靜態方法用來實現上述效果。
let LoadableMyComponent = Loadable({
loader: () => import('./another-component'),
LoadingComponent: MyLoadingComponent,
});
class MyComponent extends React.Component {
state = { showComponent: false };
onClick = () => {
this.setState({ showComponent: true });
};
onMouseOver = () => {
LoadableMyComponent.preload();
};
render() {
return (
<div>
<button onClick={this.onClick} onMouseOver={this.onMouseOver}>
Show loadable component
</button>
{this.state.showComponent && <LoadableMyComponent/>}
</div>
)
}
}
服務端渲染
Loader 通過最后一個參數支持服務端渲染。
向正在異步加載的模塊傳遞精確路徑,Loader 就會在服務端運行時同步地 require() 模塊。
import path from 'path';
const LoadableAnotherComponent = Loadable({
loader: () => import('./another-component'),
LoadingComponent: MyLoadingComponent,
delay: 200,
serverSideRequirePath: path.join(__dirname, './another-component')
});
也就是說經過異步加載、代碼分割的 bundle 可以在服務端同步渲染。這樣客戶端獲取備份會有問題。我們可以在服務端渲染全部應用,但是在客戶端需要一個個加載 bundle。
但是如果我們可以指定哪些 bundle 需要加入服務端的 bundle 進程呢?那么我們就可以一次性向客戶端裝載那些 bundle了,客戶端就可以準確獲取服務端渲染的狀態了。
在 Loadable 中我們能夠拿到服務端需要的全部路徑,所以我們可以增加一個 flushServerSideRequires 函數,返回最后在服務端渲染的所有路徑。然后通過 webpack --json 命令可以匹配文件和該文件結束所在的 bundle(此處查看代碼)。
以上為個人意見,如有雷同,純屬巧合,歡迎大家多提意見!Bey 了 個 Bey ~