在 Create React App 中如何使用程式碼分離
雖然說「程式碼分離」並不是構建 React 應用程式的必要步驟,但是如果你對什麼是「程式碼分離」感興趣並想知道它是如何幫我們構建大型 React 應用程式的話,請繼續往下閱讀。
程式碼分離
在開發 React.js 單頁應用時,隨著業務的增長,程式碼量也會有增長的趨勢。有時使用者只是訪問應用程式(或路由)的一部分,但是卻可能會載入了大量首次頁面載入時不必要的元件,這會影響我們應用的初始載入時間。
你可能已經注意到,在我們使用 Create React App 來構建應用時,Create React App 最終會生成一個大的 .js 檔案。 這個檔案包含了我們應用中所有的 JavaScript 程式碼,但如果一個使用者他只是想要在登入頁登入,我們載入其他的元件程式碼是沒有意義的。當我們的應用還比較小的時候,載入所有的程式碼不是一個問題,但是隨著應用越來越大,這個問題就會慢慢凸顯出來。為了解決這個問題,Create React App 有一個非常簡單的內建方法來分割我們的程式碼。這個功能被稱為 「程式碼分離」
Create React App(從 1.0 版本開始)允許我們使用動態import() 來載入部分應用程式碼,更多內容可以看 ofollow,noindex" target="_blank">這裡 。
動態 import() 可用於我們的 React 應用程式中的任何元件,另外它與 React Router 配合得非常好。因為我們在使用 React Router 建立路由的時候已經指出出來哪些路徑應該載入哪些元件,只有當我們導航到這個路徑的時候再載入這個元件是很有意義的。
「程式碼分離」 與 「React Router v4」
使用「React Router」來定義路由的程式碼結構一般是這樣的:
/* Import the components */ import Home from "./containers/Home"; import Posts from "./containers/Posts"; import NotFound from "./containers/NotFound"; /* Use components to define routes */ export default () => <Switch> <Route path="/" exact component={Home} /> <Route path="/posts/:id" exact component={Posts} /> <Route component={NotFound} /> </Switch>;
首先我們引入路由需要的元件,然後定義相關的路由,Switch 元件用於渲染與路徑相匹配的路由。
我們在檔案頂部通過使用 import 靜態引入了所有的元件,這意味著不管我們訪問哪個路由,這些元件都會被全部載入。通過「程式碼分離」我們想要實現的是在訪問一個頁面的時候只加載跟這個頁面匹配的元件。
建立非同步元件
為此我們來看如何動態引入相關的元件。
首先在 src/components/AsyncComponent.js 檔案中新增以下程式碼:
import React, { Component } from "react"; export default function asyncComponent(importComponent) { class AsyncComponent extends Component { constructor(props) { super(props); this.state = { component: null }; } async componentDidMount() { const { default: component } = await importComponent(); this.setState({ component: component }); } render() { const C = this.state.component; return C ? <C {...this.props} /> : null; } } return AsyncComponent; }
我們在這段程式碼裡做了這麼幾件事情:
-
asyncComponent
函式接受一個引數importComponent
,呼叫這個方法將動態引入給定的元件,看接下來asyncComponent
方法的使用會讓你更有感覺一些。 -
在元件 componentDidMount 時,我們只需要呼叫傳入的importComponent 函式,並將動態載入的元件儲存在
AsyncComponent
元件的 state 中。 -
最後,在 render 方法裡,我們需要判斷下元件是否已經載入完成。如果元件還未載入成功,最簡單的處理方式是直接返回 null,但是為了給使用者更好的體驗,我們可以加一個元件正在載入的反饋,比如可以渲染一個「loading spinner」。
使用非同步元件
現在讓我們在路由中使用這個元件來替換我們之前靜態引入元件的方式。
import Home from "./containers/Home";
我們將使用asyncComponent動態引入我們想要的元件。
const AsyncHome = asyncComponent(() => import("./containers/Home"));
需要重點注意的是,我們在這裡並沒有直接引入元件,而是建立了一個函式,然後將這個函式作為引數傳遞給asyncComponent 方法,它將在AsyncHome 建立的時候動態引入。
我們在這裡傳遞一個函式似乎有點奇怪,為什麼不直接傳入一個字串(比如 './containers/Home')然後在AsyncComponent 函式中再執行動態import() 呢? 這是因為我們需要在元件建立的地方明確宣告我們是動態引入元件,而 Webpack 也是基於此來分割我們的應用程式程式碼。它會識別這些 import
的地方,然後將這些分割出來的 import 生成所需的程式碼塊。 @wSokra 和 @dan_abramov 指出了這一點。
接下來我們就可以在路由中使用 AsyncHome 元件了,當路由匹配時,React Router 將建立AsyncHome元件,然後 AsyncHome 就會動態引入Home元件。
<Route path="/" exact component={AsyncHome} />
現在讓我們回到 Notes 這個專案並應用這些更改。
更改後, src/Routes.js 應如下所示
import React from "react"; import { Route, Switch } from "react-router-dom"; import asyncComponent from "./components/AsyncComponent"; import AppliedRoute from "./components/AppliedRoute"; import AuthenticatedRoute from "./components/AuthenticatedRoute"; import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; const AsyncHome = asyncComponent(() => import("./containers/Home")); const AsyncLogin = asyncComponent(() => import("./containers/Login")); const AsyncNotes = asyncComponent(() => import("./containers/Notes")); const AsyncSignup = asyncComponent(() => import("./containers/Signup")); const AsyncNewNote = asyncComponent(() => import("./containers/NewNote")); const AsyncNotFound = asyncComponent(() => import("./containers/NotFound")); export default ({ childProps }) => <Switch> <AppliedRoute path="/" exact component={AsyncHome} props={childProps} /> <UnauthenticatedRoute path="/login" exact component={AsyncLogin} props={childProps} /> <UnauthenticatedRoute path="/signup" exact component={AsyncSignup} props={childProps} /> <AuthenticatedRoute path="/notes/new" exact component={AsyncNewNote} props={childProps} /> <AuthenticatedRoute path="/notes/:id" exact component={AsyncNotes} props={childProps} /> {/* Finally, catch all unmatched routes */} <Route component={AsyncNotFound} /> </Switch> ;
只需進行一些更改,我們的應用程式就可以進行程式碼分割,而且也沒有增加更多的複雜性,這非常酷。讓我們再回過頭來看下之前的 src/Routes.js。
import React from "react"; import { Route, Switch } from "react-router-dom"; import AppliedRoute from "./components/AppliedRoute"; import AuthenticatedRoute from "./components/AuthenticatedRoute"; import UnauthenticatedRoute from "./components/UnauthenticatedRoute"; import Home from "./containers/Home"; import Login from "./containers/Login"; import Notes from "./containers/Notes"; import Signup from "./containers/Signup"; import NewNote from "./containers/NewNote"; import NotFound from "./containers/NotFound"; export default ({ childProps }) => <Switch> <AppliedRoute path="/" exact component={Home} props={childProps} /> <UnauthenticatedRoute path="/login" exact component={Login} props={childProps} /> <UnauthenticatedRoute path="/signup" exact component={Signup} props={childProps} /> <AuthenticatedRoute path="/notes/new" exact component={NewNote} props={childProps} /> <AuthenticatedRoute path="/notes/:id" exact component={Notes} props={childProps} /> {/* Finally, catch all unmatched routes */} <Route component={NotFound} /> </Switch> ;
需要注意的是,取代了之前靜態引入所有元件的方式,我們通過使用 asyncComponent 來建立元件,然後通過這個方法建立的元件將在必要時執行動態載入。
現在,如果您使用 npm run build
構建應用程式,你會看到程式碼分離已經成功。
每個.chunk.js 檔案都是不同的動態載入執行的時候構建出來的。當然,我們的應用程式非常小,分割的各個部分根本不重要。但是如果有一個頁面,是用來編輯筆記的,這個頁面包含一個富文字編輯器,那麼你可以想象它打包出來的檔案將會多麼大,不幸的是,它還會影響我們應用的初始載入時間。
通過使用 npm run deploy
來部署我們的應用程式,我們可以線上看下這個 例子 ,通過瀏覽器的控制檯我們可以看到,我們的檔案是按需載入的。
哇塞!我們只對程式碼做了一些簡單的更改,我們的應用程式就可以讓使用 Create React App 建立的專案擁有程式碼分離的功能了。
下一步
現在看來這好像很容易實現的樣子,但是你可能想知道如果載入新元件的請求花費了太長時間或失敗了會發生什麼,或者有時候我們可能想要預載入某些元件,比如,使用者登入的頁面,我們希望可以去預載入我們的個人主頁頁面。
上面提到過,我們可以在元件還在載入的時候渲染一個 「loading spinner」元件, 但我們可以更進一步以解決其中的一些邊緣情況。 這裡有一個很好的高階元件可以很好地完成這個任務,這個元件就是 react-loadable .
首先我們通過 npm 安裝這個元件
$ npm install --save react-loadable
然後使用它代替我們上面的 asyncComponent 方法
const AsyncHome = Loadable({ loader: () => import("./containers/Home"), loading: MyLoadingComponent });
AsyncHome 元件的使用方式跟之前是完全一樣的,另外這裡的MyLoadingComponent 我們可以寫成下面這樣。
const MyLoadingComponent = ({isLoading, error}) => { // Handle the loading state if (isLoading) { return <div>Loading...</div>; } // Handle the error state else if (error) { return <div>Sorry, there was a problem loading the page.</div>; } else { return null; } };
這個元件程式碼非常簡單,從程式碼裡可以看到我們在這個元件裡處理了各種邊緣情況。
想要了解如何新增預載入以及該元件其他的功能,你可以檢視 react-loadable 的 github 倉庫來了解更多特性,並享受「code splitting」的樂趣!