介紹熱重載
React Native 的目標是為您提供最佳的開發者體驗。其中很重要的一部分是從您儲存檔案到能夠看到變更之間所需的時間。我們的目標是讓這個回饋迴圈保持在 1 秒以下,即使您的應用程式不斷成長。
我們透過三個主要功能接近這個理想
- 使用 JavaScript 作為語言,因為它沒有冗長的編譯週期時間。
- 實作一個名為 Packager 的工具,將 es6/flow/jsx 檔案轉換為 VM 可以理解的普通 JavaScript。它被設計為一個伺服器,將中間狀態保存在記憶體中,以實現快速的增量變更,並使用多核心。
- 建構一個名為 Live Reload 的功能,在儲存時重新載入應用程式。
在這一點上,開發人員的瓶頸不再是重新載入應用程式所需的時間,而是遺失應用程式的狀態。常見的場景是開發一個距離啟動畫面多個螢幕的功能。每次重新載入時,您都必須一次又一次地點擊相同的路徑才能回到您的功能,使週期長達數秒。
熱重載
熱重載背後的想法是保持應用程式運行,並在運行時注入您編輯的檔案的新版本。這樣,您就不會遺失任何狀態,這在您調整 UI 時尤其有用。
一圖勝千言。查看 Live Reload(目前)和 Hot Reload(新的)之間的差異。
如果您仔細觀察,您會注意到可以從紅色錯誤框中恢復,並且您也可以開始導入以前不存在的模組,而無需執行完整重新載入。
警告: 因為 JavaScript 是一種非常有狀態的語言,所以熱重載無法完美實作。在實務中,我們發現目前的設定在大量常見用例中運作良好,並且在出現問題時始終可以使用完整重新載入。
熱重載從 0.22 版本開始提供,您可以啟用它
- 開啟開發人員選單
- 點擊「啟用熱重載」
簡而言之的實作
現在我們已經了解了為什麼我們需要它以及如何使用它,有趣的部分開始了:它實際上是如何運作的。
熱重載是建立在一個功能之上 熱模組替換,或 HMR。它最初由 webpack 引入,我們在 React Native Packager 內部實作了它。HMR 使 Packager 監視檔案變更並將 HMR 更新發送到應用程式中包含的輕量級 HMR 運行時。
簡而言之,HMR 更新包含已變更的 JS 模組的新程式碼。當運行時接收到它們時,它會將舊模組的程式碼替換為新的程式碼
HMR 更新包含的不僅僅是我們要變更的模組的程式碼,因為僅替換它不足以讓運行時接收到變更。問題在於模組系統可能已經快取了我們要更新的模組的 *exports*。例如,假設您有一個由這兩個模組組成的應用程式
// log.js
function log(message) {
const time = require('./time');
console.log(`[${time()}] ${message}`);
}
module.exports = log;
// time.js
function time() {
return new Date().getTime();
}
module.exports = time;
模組 `log`,印出提供的訊息,包括模組 `time` 提供的目前日期。
當應用程式被捆綁時,React Native 使用 `__d` 函數在模組系統上註冊每個模組。對於這個應用程式,在許多 `__d` 定義中,將會有一個用於 `log` 的定義
__d('log', function() {
... // module's code
});
這個調用將每個模組的程式碼包裝成一個匿名函數,我們通常將其稱為工廠函數。模組系統運行時追蹤每個模組的工廠函數、它是否已經被執行,以及此類執行的結果(exports)。當需要一個模組時,模組系統要么提供已經快取的 exports,要么首次執行模組的工廠函數並儲存結果。
假設您啟動您的應用程式並需要 `log`。在這一點上,`log` 和 `time` 的工廠函數都尚未執行,因此沒有快取任何 exports。然後,使用者修改 `time` 以返回 `MM/DD` 格式的日期
// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}
module.exports = bar;
Packager 會將 time 的新程式碼發送到運行時(步驟 1),當 `log` 最終被需要時,導出的函數被執行時,它將使用 `time` 的變更來執行(步驟 2)
現在假設 `log` 的程式碼需要 `time` 作為頂層 require
const time = require('./time'); // top level require
// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}
module.exports = log;
當需要 `log` 時,運行時將快取其 exports 和 `time` 的 exports。(步驟 1)。然後,當 `time` 被修改時,HMR 流程不能僅在替換 `time` 的程式碼後就完成。如果這樣做,當執行 `log` 時,它將使用 `time` 的快取副本(舊程式碼)來執行。
為了讓 `log` 接收 `time` 的變更,我們需要清除其快取的 exports,因為它所依賴的模組之一被熱替換了(步驟 3)。最後,當再次需要 `log` 時,它的工廠函數將被執行,需要 `time` 並取得它的新程式碼。
HMR API
React Native 中的 HMR 透過引入 `hot` 物件來擴展模組系統。這個 API 基於 webpack 的 API。`hot` 物件公開一個名為 `accept` 的函數,它允許您定義一個回呼函數,該函數將在模組需要被熱替換時執行。例如,如果我們將 `time` 的程式碼更改如下,每次我們儲存 time 時,我們都會在控制台中看到「time changed」
// time.js
function time() {
... // new code
}
module.hot.accept(() => {
console.log('time changed');
});
module.exports = time;
請注意,只有在極少數情況下您才需要手動使用此 API。對於最常見的用例,熱重載應該可以開箱即用。
HMR 運行時
正如我們之前所見,有時僅接受 HMR 更新是不夠的,因為使用正在被熱替換的模組的模組可能已經被執行並且其 imports 已被快取。例如,假設電影應用程式範例的依賴樹有一個頂層 `MovieRouter`,它依賴於 `MovieSearch` 和 `MovieScreen` 視圖,而這些視圖又依賴於先前範例中的 `log` 和 `time` 模組
如果使用者訪問了電影的搜尋視圖,但沒有訪問另一個視圖,則除了 `MovieScreen` 之外的所有模組都會快取 exports。如果對模組 `time` 進行了更改,則運行時將必須清除 `log` 的 exports,以便它接收 `time` 的變更。這個過程不會在那裡結束:運行時將遞迴地重複這個過程,直到所有父模組都被接受。因此,它將抓取依賴於 `log` 的模組並嘗試接受它們。對於 `MovieScreen`,它可以跳過,因為它尚未被需要。對於 `MovieSearch`,它將必須清除其 exports 並遞迴地處理其父模組。最後,它將對 `MovieRouter` 執行相同的操作並在那裡結束,因為沒有模組依賴於它。
為了遍歷依賴樹,運行時從 Packager 接收 HMR 更新上的反向依賴樹。對於這個例子,運行時將收到像這樣的一個 JSON 物件
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React 組件
React 組件更難與熱重載一起使用。問題在於我們不能簡單地用新的程式碼替換舊的程式碼,因為我們會遺失組件的狀態。對於 React Web 應用程式,Dan Abramov 實作了一個 Babel 轉換,它使用 webpack 的 HMR API 來解決這個問題。簡而言之,他的解決方案的工作原理是在 *轉換時* 為每個 React 組件建立一個代理。代理保存組件的狀態,並將生命週期方法委派給實際的組件,這些組件是我們熱重載的組件
除了建立代理組件之外,轉換還定義了 `accept` 函數,其中包含一段程式碼,以強制 React 重新渲染組件。這樣,我們就可以熱重載渲染程式碼,而不會遺失應用程式的任何狀態。
React Native 附帶的預設 轉換器 使用 `babel-preset-react-native`,它被 配置 為以與在使用 webpack 的 React Web 專案中使用 `react-transform` 相同的方式使用它。
Redux Store
要在 Redux store 上啟用熱重載,您只需要像在使用 webpack 的 Web 專案中所做的那樣使用 HMR API
// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';
export default function configureStore(initialState) {
const store = createStore(
reducer,
initialState,
applyMiddleware(thunk),
);
if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}
return store;
};
當您變更 reducer 時,用於接受該 reducer 的程式碼將被發送到客戶端。然後客戶端將意識到 reducer 不知道如何接受自身,因此它將尋找所有引用它的模組並嘗試接受它們。最終,流程將到達單一 store,`configureStore` 模組,它將接受 HMR 更新。
結論
如果您有興趣協助改進熱重載,我鼓勵您閱讀 Dan Abramov 關於熱重載未來 的文章並做出貢獻。例如,Johny Days 將要 使其與多個連接的客戶端一起工作。我們依靠大家來維護和改進這個功能。
有了 React Native,我們有機會重新思考我們建構應用程式的方式,以使其成為出色的開發者體驗。熱重載只是拼圖的一塊,我們還可以做哪些瘋狂的 hack 來使其更好?