Hot Reloading 簡介
React Native 的目標是為您提供最佳的開發人員體驗。其中很重要的一部分是您儲存檔案到能夠看到變更之間所需的時間。我們的目標是讓這個回饋迴圈保持在 1 秒以下,即使您的應用程式不斷成長。
我們透過三個主要功能接近這個理想
- 使用 JavaScript 作為語言,因為它沒有冗長的編譯週期時間。
- 實作一個名為 Packager 的工具,將 es6/flow/jsx 檔案轉換為 VM 可以理解的正常 JavaScript。它被設計為伺服器,將中間狀態保存在記憶體中,以實現快速增量變更,並使用多個核心。
- 建構一個名為 Live Reload 的功能,在儲存時重新載入應用程式。
在這一點上,開發人員的瓶頸不再是重新載入應用程式所需的時間,而是遺失應用程式的狀態。常見的情況是處理距離啟動畫面多個螢幕的功能。每次重新載入時,您都必須一次又一次點擊相同的路徑才能回到您的功能,這使得週期長達數秒。
Hot Reloading
Hot Reloading 背後的想法是保持應用程式執行,並在執行階段注入您編輯的檔案的新版本。這樣,您就不會遺失任何狀態,如果您正在調整 UI,這尤其有用。
一個影片勝過千言萬語。看看 Live Reload(目前)和 Hot Reload(新)之間的差異。
如果您仔細觀察,您會注意到可以從紅色方塊中恢復,而且您也可以開始匯入以前不存在的模組,而無需完全重新載入。
警告:由於 JavaScript 是一種非常有狀態的語言,因此 Hot Reloading 無法完美實作。在實務上,我們發現目前的設定在大量常見用例中運作良好,而且在發生問題時,始終可以使用完全重新載入。
Hot Reloading 自 0.22 版起提供,您可以啟用它
- 開啟開發人員選單
- 點擊「啟用 Hot Reloading」
簡而言之的實作
現在我們已經了解了為什麼我們需要它以及如何使用它,有趣的部分開始了:它實際上是如何運作的。
Hot Reloading 建構於 Hot Module Replacement 或 HMR 功能之上。它最初由 webpack 引入,我們在 React Native Packager 內部實作了它。HMR 使 Packager 監看檔案變更,並將 HMR 更新傳送至應用程式上包含的精簡 HMR 執行階段。
簡而言之,HMR 更新包含已變更的 JS 模組的新程式碼。當執行階段接收到它們時,它會將舊模組的程式碼替換為新的程式碼
HMR 更新包含的不僅僅是我們想要變更的模組程式碼,因為僅僅替換它不足以讓執行階段接收變更。問題在於模組系統可能已經快取了我們想要更新的模組的匯出。例如,假設您有一個由這兩個模組組成的應用程式
// 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
});
此調用將每個模組的程式碼包裝到一個匿名函式中,我們通常將其稱為工廠函式。模組系統執行階段會追蹤每個模組的工廠函式、它是否已經執行以及此類執行的結果(匯出)。當需要模組時,模組系統會提供已快取的匯出,或首次執行模組的工廠函式並儲存結果。
假設您啟動應用程式並要求 log
。此時,log
和 time
的工廠函式都尚未執行,因此沒有快取任何匯出。然後,使用者修改 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
作為頂層要求
const time = require('./time'); // top level require
// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}
module.exports = log;
當需要 log
時,執行階段會快取其匯出和 time
的匯出。(步驟 1)。然後,當 time
被修改時,HMR 處理無法在替換 time
的程式碼後簡單地完成。如果這樣做,當 log
執行時,它將使用 time
的快取副本(舊程式碼)來執行。
為了讓 log
接收 time
的變更,我們需要清除其快取的匯出,因為它所依賴的模組之一已被熱交換(步驟 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。Hot Reloading 應該可以在大多數常見用例中開箱即用。
HMR 執行階段
正如我們之前所見,有時僅僅接受 HMR 更新是不夠的,因為使用正在熱交換的模組的模組可能已經執行,並且其匯入已快取。例如,假設電影應用程式範例的依賴樹有一個頂層 MovieRouter
,它依賴於 MovieSearch
和 MovieScreen
檢視,而這些檢視又依賴於先前範例中的 log
和 time
模組
如果使用者存取電影的搜尋檢視,但未存取另一個檢視,則除了 MovieScreen
之外的所有模組都將具有快取的匯出。如果對模組 time
進行變更,則執行階段必須清除 log
的匯出,以便它接收 time
的變更。此處理程序不會在那裡結束:執行階段將遞迴地重複此處理程序,直到所有父項都被接受。因此,它將抓取依賴於 log
的模組並嘗試接受它們。對於 MovieScreen
,它可以中止,因為它尚未被要求。對於 MovieSearch
,它將必須清除其匯出並遞迴地處理其父項。最後,它將對 MovieRouter
執行相同的操作,並在那裡結束,因為沒有模組依賴於它。
為了走訪依賴樹,執行階段會在 HMR 更新時從 Packager 接收反向依賴樹。對於此範例,執行階段將接收一個類似於此 JSON 物件
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React 組件
React 組件更難以使用 Hot Reloading 運作。問題在於我們不能簡單地用新程式碼替換舊程式碼,因為我們會遺失組件的狀態。對於 React Web 應用程式,Dan Abramov 實作了一個 Babel 轉換,它使用 webpack 的 HMR API 來解決此問題。簡而言之,他的解決方案透過在轉換時為每個 React 組件建立一個 Proxy 來運作。Proxy 保留組件的狀態,並將生命週期方法委派給實際組件,這些組件是我們熱重新載入的組件
除了建立 Proxy 組件之外,轉換還定義了 accept
函式,其中包含一段程式碼,以強制 React 重新渲染組件。這樣,我們就可以熱重新載入渲染程式碼,而不會遺失任何應用程式的狀態。
React Native 隨附的預設 轉換器 使用 babel-preset-react-native
,它 已配置 為以與在使用 webpack 的 React Web 專案中使用 react-transform
相同的方式使用它。
Redux 儲存區
若要在 Redux 儲存區上啟用 Hot Reloading,您只需要使用 HMR API,與您在使用 webpack 的 Web 專案中執行的操作類似
// 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 不知道如何接受自身,因此它將尋找所有引用它的模組並嘗試接受它們。最終,流程將到達單一儲存區,configureStore
模組,它將接受 HMR 更新。
結論
如果您有興趣協助使 Hot Reloading 變得更好,我鼓勵您閱讀 Dan Abramov 關於 Hot Reloading 未來的文章 並做出貢獻。例如,Johny Days 將 使其適用於多個連線的用戶端。我們仰賴大家來維護和改進此功能。
透過 React Native,我們有機會重新思考我們建構應用程式的方式,以便使其成為絕佳的開發人員體驗。Hot Reloading 只是拼圖中的一小塊,我們還可以做哪些瘋狂的技巧來使其變得更好?