react程式碼拆分之react loadable原始碼淺析

楊小事er發表於2019-03-01

在做個人網站的js拆分打包時,最終的解決方案是看著網上的教程手寫了Bundle高階元件來動態載入需要的元件。對於它的運用也僅僅是把路由拆開,訪問不同的頂級路由進行動態載入,並沒有對其原理進行深入的理解。直到看到了React 的載入 loading 庫——react-loadable

react-loadable是什麼?

Loadable提倡基於元件分割程式碼。

image

route-centric code splitting is shit, component-centric splitting is cool as shit.

網上的翻譯有很多,本文就不過多對readme操作了,簡單的介紹一下就是:
Loadable 是一個高階元件(簡單來說,就是把元件作為輸入的元件。高階函式就是把函式作為輸入的函式。在 React 裡,函式和元件有時是一回事),一個可以構建元件的函式(函式可以是元件),它可以很容易的在元件層面分割程式碼包。

react-loadable怎麼用?

下面是github庫裡readme給的例子。

import Loadable from `react-loadable`;
import Loading from `./my-loading-component`;

const LoadableComponent = Loadable({
  loader: () => import(`./my-component`), // 要按需載入的元件,用了import()函式
  loading: Loading,    // 一個無狀態元件,負責顯示"Loading中"
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}
複製程式碼

my-loading-component.js

import React from `react`;

export default function Loading(props) {
  if (props.isLoading) {
    if (props.timedOut) {
      return <div>Loader timed out!</div>;
    } else if (props.pastDelay) {
      return <div>Loading...</div>;
    } else {
      return null;
    }
  } else if (props.error) {
    return <div>Error! Component failed to load</div>;
  } else {
    return null;
  }
}
複製程式碼

原始碼分析

原始碼結構

這是loadable的原始碼結構

image

最後export出來的是一個Loadable函式,所以我們從Loadable開始分析。

module.exports = Loadable;

function Loadable(opts) {
  return createLoadableComponent(load, opts);
}

複製程式碼

Loadable接受一個引數opts,再呼叫了createLoadableComponent函式,傳入了load函式和opts。

load函式

function load(loader) {
    let promise = loader();

    let state = {
        loading: true,
        loaded: null,
        error: null
    };

    state.promise = promise.then(loaded => {
        state.loading = false;
        state.loaded = loaded;
        return loaded;
    }).catch(err => {
        state.loading = false;
        state.error = err;
        throw err;
    });

    return state;
}
複製程式碼

這裡load又是一個函式,接受一個loader引數。我們先放在這裡,一會在回來看這個loader

createLoadableComponent

createLoadableComponent

這個函式是整個庫的畢竟重要的函式了。(因為本文是淺析,所以先只分析主邏輯的函式,別的健壯性支援函式可能會之後再分析)

if(!options.loading){…}

options.loading就是上文提到的一個無狀態元件,負責顯示”Loading中”,如果不存在,會報錯,需要一個Loading中顯示的元件。

let opts = Object.assign(…,options)
let opts = Object.assign({
        loader: null,
        loading: null,
        delay: 200,
        timeout: null,
        render: render,
        webpack: null,
        modules: null,
    }, options);
複製程式碼

就是初始化一下opts的值,賦給未傳入引數初始值,防止接下來的判斷報錯。

function init() {…}
function init() {
    if (!res) {
        res = loadFn(opts.loader);
    }
    return res.promise;
}
複製程式碼

init之前宣告過一個resinit就是為res賦值。在init裡呼叫了loadFn,就是上文說過的load函式

load函式接收一個引數,就是之前() => import(`./my-component`),這裡的import()方法,返回一個Promise物件,[[PromiseValue]].default就是待load元件的function。

image

load函式裡初始化了一個state物件,並在import()方法返回的Promise中,對state的屬性賦值,loading代表是否載入完成, loaded為載入的物件,這裡的state.loaded已經是[[PromiseValue]]了。

return class LoadableComponent extends React.Component …

因為react loadable的描述終究是一個高階元件,如果對高階元件不瞭解的,可以去看
深入React高階元件(HOC)這篇文章。
所以createLoadableComponent最後返回的是一個React Component。

一開始的constructor裡,呼叫了init,將res的幾個屬性,分別賦值給this.state作為元件初始化。

this.state = {
    error: res.error,
    pastDelay: false,
    timedOut: false,
    loading: res.loading,
    loaded: res.loaded
};
複製程式碼

之後在componentWillMount中,做了這些操作

  • 設定了個標記位this._mounted,預設為true
  • 進行一些判斷,如果res.loading為true,說明之前的init執行出錯,直接return;
  • 如果opts.delayopts.timeout有值,且為number屬性的話,就加個定時器。
  • 宣告update函式,如果this._mounted為false,重新將res的屬性更新到state裡。

render中進行了判斷,

  • 如果this.state.loading或者this.state.error為true,就是整體狀態是正在載入ing或者出錯了,就用React.createElement生成出loading的過渡元件,
  • 如果this.state.loaded有值了,說明傳入的loader的promise非同步操作執行完成,就開始渲染真正的元件,呼叫opts.render方法.(此時的this.state.loaded就是[[PromiseValue]])
    image
function resolve(obj) {
    return obj && obj.__esModule ? obj.default : obj;
}
function render(loaded, props) {
    return React.createElement(resolve(loaded), props);
}
複製程式碼

render的時候進行判斷,當__esModule為true的時候,標識module為es模組,那麼obj預設返回obj.default,那麼obj預設返回obj。之後再呼叫React.createElement進行渲染。

至於props是什麼?在專案裡列印發現,就是原元件的props。所以render出的,就和正常在程式碼中寫的React Component是一樣的。

image

然後,整個loadable就完成了。

總結

這個庫的設計還是非常巧妙,利用import返回一個promise物件的性質,進行loading的非同步操作,有點像圖片懶載入的原理。下面是劃重點系列,

  • import() 可以返回一個promise物件
  • React.createElement() API
  • Promise物件原理。

本文只是對react loadable這個庫的核心原始碼的分析,還有一些別的程式碼沒有分析到,以及一些分析失誤的地方,都歡迎大家交流分享。可以發郵件給我:uiryzd@163.com

react loadable原始碼地址

相關文章