React 16.6 的新發布帶來了一些只需很小努力就能給React元件對增加了很多力量的新特性。
其中有兩個是 React.Suspense
和 React.lazy()
, 這個很容易用在程式碼分割和懶載入上。
這篇文章關注在如何在 React 應用中使用兩個新特性和他們給 React 開發者帶來的新的潛力。
程式碼分割
過去幾年寫 JavaScript 應用的方式進化了。在 ES6(modules)的出現,Babel 編譯器,和其他打包工具像是 WebPack 和Browserify,JavaScript 應用現在可以用完全現代化的模式寫出容易維護的東西。
通常,每個模組被匯入合併在一個檔案叫做 bundle,這些 bundle 在一張頁面上包括了整個APP。然而,當 APP 增長的時候,這些 bundle 尺寸開始變得越來越大,因此影響了頁面載入時間。
打包工具像是 Webpack 和 Browserify 提供了程式碼分割的支援,可以在需要載入(懶載入)而不是一次性載入不同的 bundles 中引入分割程式碼,從而提高 app 的表現。
Dynamic Imports
程式碼風格的主要方式之一是使用動態匯入。動態匯入作用於 import()
語法,這還不是 JavaScript 語言標準的一部分,但是一個期望不久被接受的提案。
呼叫 import()
去載入模組依賴 JavaScript 的 Promises。因此,返回一個完整的載入的模組或者如果模組不存在的話就拒絕。
對於老的瀏覽器,es6-promise 補充應該用來補充 Promise
這兒有一個用 Webpack 打包的app的內容,看起來是動態匯入模組:
import(/* webpackChunkName: "moment" */ 'moment')
.then(({default: moment}) => {
const tommorrow =moment().startOf('day').add(1, 'day');
return tomorrow.format('LLL');
})
.catch(error => console.error("..."))
複製程式碼
當 Webpack 看到這樣的語法,它會為 moment
庫,動態建立一個分割包。
對於 React 應用,如果使用 create-react-app
或者 Next.js
,程式碼分割在 import()
中悄悄產生。
然而,如果自定義了 Webpack的設定,你需要檢查 Webpack 指導。對於 Babel 轉化,你需要 yarnpkg.com/en/package/… 外掛,允許 Babel 正確解析 import()
。
React 元件的程式碼分割
已經有幾種技術應用於 React 元件的程式碼分割上。常見的實現是動態 import()
在應用中懶載入路由元件——這個通常是作為基於路由程式碼分割的元件。
然而,這裡有個叫 React-loadable 的非常流行的包用於 React 元件的程式碼分割。它提供一個高階函式用 promise 來載入 React 元件,實現動態 import()
語法。
考慮下面叫做 MyComponent
的 React 元件:
import OtherComponent from './OtherComponent';
export defautl function MyComponent() {
return (
<div>
<h1>My Component</h1>
<OtherComponent />
</div>
)
}
複製程式碼
這裡,OtherComponent
是不會請求直到MyComponent
開始渲染。然而,因為我們靜態匯入了 OtherComponent
,它會和 MyComponent
一起打包。
我們可以使用 react-loadable
去延遲載入 OtherComponent
,直到我們渲染MyComponent
,從而程式碼分割成幾個包。這裡有個用 react-loadable
懶載入的OtherComponent
。
impoort Loadable from 'react-loadable';
const LoadableOtherComponent = loadable({
loader: () => import('./OtherComponent'),
loading: () => <div>Loading...</div>
});
export default function MyComponent() {
return (
<div>
<h1>My Component</h1>
<LoadableOtherComponent/>
</div>
)
}
複製程式碼
在這裡能看到在選擇物件中,元件被動態 import()
語法匯入,賦值給 loader
屬性。
React-loadable 也是用了 loading
屬性去具體指出當等待真正元件載入時,將會渲染的回撥元件。
你可以在這篇文件中瞭解你能通過 react-loadable
實現什麼。
使用 Suspense 和 React.lazy()
在 React 16.6 中,支援基礎元件的程式碼分割和懶載入已經通過 React.lazy()
和 React.Suspense
新增。
React.lazy() 和 Suspense 還沒有支援服務端。服務端的程式碼分割,仍然使用 React-Loadable。
React.lazy()
React.lazy()
很容易建立一個使用動態 import
的元件,而且像常規元件一樣渲染。當元件被渲染時,它會自動打包包含這個載入的元件。
當呼叫 import()
載入元件,React.lazy()` 使用一個必須返回一個 promise 的引數的方法。這個預設匯出包含 React 元件返回的 Promise 處理了模組。
當使用 React.lazy()
時,看起來像:
// 不使用 React.lazy()
import OtherComponent from './OtherComponent';
const MyComponent = () => {
<div>
<OtherComponent />
</div>
};
// 使用 React.lazy()
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const MyComponent = () => {
<div>
<OtherComonment />
</div>
}
複製程式碼
Suspense
一個使用 React.lazy() 的元件只會在它需要的時候被載入。
因此,這裡需要展示一些佔位符內容的格式,當懶載入元件正在被載入的時候,比如用一個載入指示器。
這就是 React.Suspense
所建立的。
React.Suspense
是一個包裹了懶載入元件的元件。你可以在不同的層級上使用一個 Suspense
元件包裹多個懶載入元件。
當所有懶載入元件載入後,這個 Suspense
元件使用 fallback
屬性可以接受任何你想渲染的元件作為一個佔位符。
import React, { Suspense } from 'react';
const LazyComponent = React.lazy(() => import('./OtherComponent'));
const MyComponent = () => {
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
}
複製程式碼
如果元件懶載入失敗,在懶載入之上放置明顯的錯誤邊界來展示不錯的使用者體驗。
我在 CodeSandbox 上已經建立了一個很簡單的例子來演示使用 React.lazy()
和 Suspense
作為懶載入元件。
這裡有個微型的app程式碼:
import React, { Suspense } from "react";
import Loader from "./components/Loader";
import Header from "./components/Header";
import ErrorBoundary from "./components/ErrorBoundary";
const Calendar = React.lazy(() => {
return new Promise(resolve => setTimeout(resolve, 5 * 1000)).then(
() =>
Math.floor(Math.random() * 10) >= 4
? import("./components/Calendar")
: Promise.reject(new Error())
);
});
export default function CalendarComponent() {
return (
<div>
<ErrorBoundary>
<Header>Calendar</Header>
<Suspense fallback={<Loader />}>
<Calendar />
</Suspense>
</ErrorBoundary>
</div>
);
}
複製程式碼
這裡,一個很簡單的 Loader
元件在懶載入 Calendar
元件中被建立用作回撥內容。當懶元件 Calendar
載入失敗,一個邊界提示被建立來展示友好的錯誤。
我這裡包裹了懶載入日曆來模擬5秒延時。為了增加 Calendar
元件載入失敗的概率,我也使用一個條件匯入 Calendar
元件,或者返回一個promise的rejects。
const Calendar = React.lazy(() => {
return new Promise(resolve => setTimeout(resolve, 5 * 1000)).then(
() => Math.floor(Math.random() * 10 )>= 4 ?
import("./components/Calendar"):
Promise.reject(new Error())
)
})
複製程式碼
下面的截圖展示了當渲染的時候元件看起來的示例。
命名匯出
如果你希望使用一個命名的匯出元件,那麼你需要再次匯出他們,作為在獨立的中間模組中的預設匯出。
如果你有一個 OtherComponent
作為命名匯出模組,你希望使用 React.lazy()
來載入 OtherComponent
,那麼你需要建立一箇中間模組來再次匯出 OtherComponent
作為
預設模組。
Component.js
export const FirstComponent = () => {/* 元件邏輯 */}
export const SecondComponent = () => {/* 元件邏輯 */}
export const OtherComponent = () => {/* 元件邏輯 */}
複製程式碼
OtherComponent.js
export { OtherComponet as defatul } from './Components';
複製程式碼
這時候你可以使用 React.lazy()
去載入 OtherComponent
從中間模組。
懶載入路由
使用 React.lazy()
和 Suspense
,現在很容易處理基於路由的程式碼分割而不使用其他外部依賴。你可以簡單地轉化應用的路由組建成為懶載入元件,包裹所有的路由通過 Suspense
元件。
下面的程式碼使用 React Router 展示了基於路由的程式碼分割:
import React, { Suspense } from 'react';
import { Router } from '@reach/router';
import Loading from './Loading';
const Home = React.lazy(() => import('./Home'));
const Dashboard = React.lazy(() => import('./Dashboard'));
const Overview = React.lazy(() => import('./Overview'));
const History = React.lazy(() => import('./History'));
const NotFound = React.lazy(() => import('./NotFound'));
function App() {
return (
<div>
<Suspense fallback={<Loading />}>
<Router>
<Home path="/" />
<Dashboard path="dashboard">
<Overview path="/" />
<History path="/history" />
</Dashboard>
<NotFound default />
</Router>
</Suspense>
</div>
)
}
複製程式碼
總結
With the new React.lazy()
和 React.Suspense
, code-splitting and lazy-loading React components has been made very easy.
現在開始從 React 16.6享受吧。