這就是你日思夜想的 React 原生動態加載

這是第 51 篇不摻水的原創,想獲取更多原創好文,請掃 👆上方二維碼關注咱們吧~
本文首發於政採雲前端團隊博客:這就是你日思夜想的 React 原生動態加載javascript

React.lazy 是什麼

隨着前端應用體積的擴大,資源加載的優化是咱們必需要面對的問題,動態代碼加載就是其中的一個方案,webpack 提供了符合 ECMAScript 提案 的 import() 語法 ,讓咱們來實現動態地加載模塊(注:require.ensure 與 import() 均爲 webpack 提供的代碼動態加載方案,在 webpack 2.x 中,require.ensure 已被 import 取代)。html

在 React 16.6 版本中,新增了 React.lazy 函數,它能讓你像渲染常規組件同樣處理動態引入的組件,配合 webpack 的 Code Splitting ,只有當組件被加載,對應的資源纔會導入 ,從而達到懶加載的效果。前端

使用 React.lazy

在實際的使用中,首先是引入組件方式的變化:java

// 不使用 React.lazy
import OtherComponent from './OtherComponent';
// 使用 React.lazy
const OtherComponent = React.lazy(() => import('./OtherComponent'))
複製代碼

React.lazy 接受一個函數做爲參數,這個函數須要調用 import() 。它須要返回一個  Promise,該 Promise 須要 resolve 一個 defalut export 的 React 組件。react

圖片

// react/packages/shared/ReactLazyComponent.js
	export const Pending = 0;
	export const Resolved = 1;
	export const Rejected = 2;
複製代碼

在控制檯打印能夠看到,React.lazy 方法返回的是一個 lazy 組件的對象,類型是 react.lazy,而且 lazy 組件具備 _status 屬性,與 Promise 相似它具備 Pending、Resolved、Rejected 三個狀態,分別表明組件的加載中、已加載、和加載失敗三中狀態。webpack

須要注意的一點是,React.lazy 須要配合 Suspense 組件一塊兒使用,在 Suspense 組件中渲染 React.lazy 異步加載的組件。若是單獨使用 React.lazy,React 會給出錯誤提示。git

圖片

上面的錯誤指出組件渲染掛起時,沒有 fallback UI,須要加上 Suspense 組件一塊兒使用。github

其中在 Suspense 組件中,fallback 是一個必需的佔位屬性,若是沒有這個屬性的話也是會報錯的。web

接下來咱們能夠看看渲染效果,爲了更清晰的展現加載效果,咱們將網絡環境設置爲 Slow 3G。api

圖片

組件的加載效果:

圖片

能夠看到在組件未加載完成前,展現的是咱們所設置的 fallback 組件。

在動態加載的組件資源比較小的狀況下,會出現 fallback 組件一閃而過的的體驗問題,若是不須要使用能夠將 fallback 設置爲 null。

Suspense 能夠包裹多個動態加載的組件,這也意味着在加載這兩個組件的時候只會有一個 loading 層,由於 loading 的實現實際是 Suspense 這個父組件去完成的,當全部的子組件對象都 resolve 後,再去替換全部子組件。這樣也就避免了出現多個 loading 的體驗問題。因此 loading 通常不會針對某個子組件,而是針對總體的父組件作 loading 處理。

以上是 React.lazy 的一些使用介紹,下面咱們一塊兒來看看整個懶加載過程當中一些核心內容是怎麼實現的,首先是資源的動態加載。

Webpack 動態加載

上面使用了 import() 語法,webpack 檢測到這種語法會自動代碼分割。使用這種動態導入語法代替之前的靜態引入,可讓組件在渲染的時候,再去加載組件對應的資源,這個異步加載流程的實現機制是怎麼樣呢?

話很少說,直接看代碼:

__webpack_require__.e = function requireEnsure(chunkId) {
    // installedChunks 是在外層代碼中定義的對象,能夠用來緩存了已加載 chunk
  var installedChunkData = installedChunks[chunkId]
    // 判斷 installedChunkData 是否爲 0:表示已加載 
  if (installedChunkData === 0) {
    return new Promise(function(resolve) {
      resolve()
    })
  }
  if (installedChunkData) {
    return installedChunkData[2]
  } 
  // 若是 chunk 還未加載,則構造對應的 Promsie 並緩存在 installedChunks 對象中
  var promise = new Promise(function(resolve, reject) {
    installedChunkData = installedChunks[chunkId] = [resolve, reject]
  })
  installedChunkData[2] = promise
  // 構造 script 標籤
  var head = document.getElementsByTagName("head")[0]
  var script = document.createElement("script")
  script.type = "text/javascript"
  script.charset = "utf-8"
  script.async = true
  script.timeout = 120000
  if (__webpack_require__.nc) {
    script.setAttribute("nonce", __webpack_require__.nc)
  }
  script.src =
    __webpack_require__.p +
    "static/js/" +
    ({ "0": "alert" }[chunkId] || chunkId) +
    "." +
    { "0": "620d2495" }[chunkId] +
    ".chunk.js"
  var timeout = setTimeout(onScriptComplete, 120000)
  script.onerror = script.onload = onScriptComplete
  function onScriptComplete() {
    script.onerror = script.onload = null
    clearTimeout(timeout)
    var chunk = installedChunks[chunkId]
    // 若是 chunk !== 0 表示加載失敗
    if (chunk !== 0) {
        // 返回錯誤信息
      if (chunk) {
        chunk[1](new Error("Loading chunk " + chunkId + " failed."))
      }
      // 將此 chunk 的加載狀態重置爲未加載狀態
      installedChunks[chunkId] = undefined
    }
  }
  head.appendChild(script)
    // 返回 fullfilled 的 Promise
  return promise
}
複製代碼

結合上面的代碼來看,webpack 經過建立 script 標籤來實現動態加載的,找出依賴對應的 chunk 信息,而後生成 script 標籤來動態加載 chunk,每一個 chunk 都有對應的狀態:未加載 、 加載中、已加載 。

咱們能夠運行 React.lazy 代碼來具體看看 network 的變化,爲了方便辨認 chunk。咱們能夠在 import 裏面加入 webpackChunckName 的註釋,來指定包文件名稱。

const OtherComponent = React.lazy(() => import(/* webpackChunkName: "OtherComponent" */'./OtherComponent'));
const OtherComponentTwo = React.lazy(() => import(/* webpackChunkName: "OtherComponentTwo" */'./OtherComponentTwo'));
複製代碼

webpackChunckName 後面跟的就是打包後組件的名稱。

圖片

打包後的文件中多了動態引入的 OtherComponent、OtherComponentTwo 兩個 js 文件。

若是去除動態引入改成通常靜態引入:

圖片

能夠很直觀的看到兩者文件的數量以及大小的區別。

圖片

以上是資源的動態加載過程,當資源加載完成以後,進入到組件的渲染階段,下面咱們再來看看,Suspense 組件是如何接管 lazy 組件的。

Suspense 組件

一樣的,先看代碼,下面是 Suspense 所依賴的 react-cache 部分簡化源碼:

// react/packages/react-cache/src/ReactCache.js 
export function unstable_createResource<I, K: string | number, V>( fetch: I => Thenable<V>, maybeHashInput?: I => K, ): Resource<I, V> {
  const hashInput: I => K =
    maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
  const resource = {
    read(input: I): V {
      readContext(CacheContext);
      const key = hashInput(input);
      const result: Result<V> = accessResult(resource, fetch, input, key);
      // 狀態捕獲
      switch (result.status) { 
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
    preload(input: I): void {
      readContext(CacheContext);
      const key = hashInput(input);
      accessResult(resource, fetch, input, key);
    },
  };
  return resource;
}
複製代碼

從上面的源碼中看到,Suspense 內部主要經過捕獲組件的狀態去判斷如何加載,上面咱們提到 React.lazy 建立的動態加載組件具備 Pending、Resolved、Rejected 三種狀態,當這個組件的狀態爲 Pending 時顯示的是 Suspense 中 fallback 的內容,只有狀態變爲 resolve 後才顯示組件。

結合該部分源碼,它的流程以下所示:

圖片

Error Boundaries 處理資源加載失敗場景

若是遇到網絡問題或是組件內部錯誤,頁面的動態資源可能會加載失敗,爲了優雅降級,可使用 Error Boundaries 來解決這個問題。

Error Boundaries 是一種組件,若是你在組件中定義了 static getDerivedStateFromError() 或 componentDidCatch() 生命週期函數,它就會成爲一個 Error Boundaries 的組件。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
​
  static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以顯示降級後的 UI
      return { hasError: true };  
  }
  componentDidCatch(error, errorInfo) { // 你一樣能夠將錯誤日誌上報給服務器
      logErrorToMyService(error, errorInfo);
  }
  render() {
    if (this.state.hasError) { // 你能夠自定義降級後的 UI 並渲染      
        return <h1>對不起,發生異常,請刷新頁面重試</h1>;    
    }
    return this.props.children; 
  }
}
複製代碼

你能夠在 componentDidCatch  或者 getDerivedStateFromError 中打印錯誤日誌並定義顯示錯誤信息的條件,當捕獲到 error 時即可以渲染備用的組件元素,不至於致使頁面資源加載失敗而出現空白。

它的用法也很是的簡單,能夠直接看成一個組件去使用,以下:

<ErrorBoundary>
  <MyWidget /> </ErrorBoundary>
複製代碼

咱們能夠模擬動態加載資源失敗的場景。首先在本地啓動一個 http-server 服務器,而後去訪問打包好的 build 文件,手動修改下打包的子組件包名,讓其查找不到子組件包的路徑。而後看看頁面渲染效果。

圖片

能夠看到當資源加載失敗,頁面已經降級爲咱們在錯誤邊界組件中定義的展現內容。

流程圖例:

圖片

須要注意的是:錯誤邊界僅能夠捕獲其子組件的錯誤,它沒法捕獲其自身的錯誤。

總結

React.lazy()  和 React.Suspense 的提出爲現代 React 應用的性能優化和工程化提供了便捷之路。 React.lazy 可讓咱們像渲染常規組件同樣處理動態引入的組件,結合 Suspense 能夠更優雅地展示組件懶加載的過渡動畫以及處理加載異常的場景。

注意:React.lazy 和 Suspense 尚不可用於服務器端,若是須要服務端渲染,可聽從官方建議使用 Loadable Components

參考文檔

  1. Concurrent 模式
  2. 代碼分割
  3. webpack 優化之code splitting

推薦閱讀

圖解 HTTP 緩存

多是最全的 「文本溢出截斷省略」 方案合集

圖文並茂,爲你揭開「單點登陸「的神祕面紗

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「 5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索