在做個人網站的js拆分打包時,最終的解決方案是看著網上的教程手寫了Bundle高階元件來動態載入需要的元件。對於它的運用也僅僅是把路由拆開,訪問不同的頂級路由進行動態載入,並沒有對其原理進行深入的理解。直到看到了React 的載入 loading 庫——react-loadable。
react-loadable是什麼?
Loadable提倡基於元件分割程式碼。
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的原始碼結構
最後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
這個函式是整個庫的畢竟重要的函式了。(因為本文是淺析,所以先只分析主邏輯的函式,別的健壯性支援函式可能會之後再分析)
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
之前宣告過一個res
,init
就是為res
賦值。在init
裡呼叫了loadFn
,就是上文說過的load函式
。
load函式接收一個引數,就是之前() => import(`./my-component`)
,這裡的import()
方法,返回一個Promise物件,[[PromiseValue]].default
就是待load元件的function。
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.delay
和opts.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]]
)
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是一樣的。
然後,整個loadable就完成了。
總結
這個庫的設計還是非常巧妙,利用import返回一個promise物件的性質,進行loading的非同步操作,有點像圖片懶載入的原理。下面是劃重點系列,
- import() 可以返回一個promise物件
- React.createElement() API
- Promise物件原理。
本文只是對react loadable
這個庫的核心原始碼的分析,還有一些別的程式碼沒有分析到,以及一些分析失誤的地方,都歡迎大家交流分享。可以發郵件給我:uiryzd@163.com