Wprowadzenie do Hot Reloading
Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →
Celem React Native jest zapewnienie najlepszego możliwego doświadczenia deweloperskiego. Kluczowym elementem jest czas między zapisaniem pliku a możliwością zobaczenia zmian. Naszym celem jest skrócenie tej pętli sprzężenia zwrotnego do poniżej 1 sekundy, nawet w miarę rozrastania się aplikacji.
Zbliżyliśmy się do tego ideału dzięki trzem głównym funkcjom:
-
Wykorzystanie JavaScriptu jako języka, który nie wymaga długiego czasu kompilacji.
-
Wdrożenie narzędzia zwanego Packager, które przekształca pliki es6/flow/jsx w zwykły JavaScript zrozumiały dla maszyny wirtualnej. Zaprojektowano je jako serwer przechowujący stan pośredni w pamięci, co umożliwia szybkie zmiany przyrostowe i wykorzystuje wiele rdzeni.
-
Stworzenie funkcji Live Reload, która przeładowuje aplikację przy zapisie.
W tym momencie wąskim gardłem nie jest już czas przeładowania aplikacji, ale utrata jej stanu. Typowym scenariuszem jest praca nad funkcją znajdującą się kilka ekranów dalej od ekranu startowego. Przy każdym przeładowaniu musisz klikać tę samą ścieżkę wielokrotnie, by wrócić do swojej funkcji, co wydłuża cykl do kilku sekund.
Hot Reloading
Idea Hot Reloading polega na utrzymywaniu działania aplikacji i wstrzykiwaniu nowych wersji edytowanych plików podczas działania programu. Dzięki temu nie tracisz stanu aplikacji, co jest szczególnie przydatne przy dopracowywania interfejsu.
Jeden film wart jest tysiąca słów. Zobacz różnicę między Live Reload (obecne) a Hot Reload (nowe).
Jeśli przyjrzysz się uważnie, zauważysz że możliwe jest odzyskanie się po czerwonym boksie błędów oraz importowanie modułów, które wcześniej nie istniały, bez konieczności pełnego przeładowania.
Ostrzeżenie: ponieważ JavaScript jest językiem silnie stanowym, Hot Reloading nie może być zaimplementowany idealnie. W praktyce obecne rozwiązanie sprawdza się dobrze w większości typowych przypadków, a pełne przeładowanie zawsze jest dostępne na wypadek problemów.
Hot Reloading dostępny jest od wersji 0.22. Możesz go włączyć:
-
Otwórz menu deweloperskie
-
Dotknij "Włącz Hot Reloading"
Implementacja w pigułce
Skoro wiemy już po co to i jak używać, czas na najciekawszą część: jak to właściwie działa.
Hot Reloading bazuje na funkcji Hot Module Replacement (HMR). Została ona wprowadzona przez webpack, a my zaimplementowaliśmy ją w React Native Packager. HMR sprawia, że Packager obserwuje zmiany plików i wysyła aktualizacje HMR do lekkiego środowiska uruchomieniowego HMR w aplikacji.
W skrócie, aktualizacja HMR zawiera nowy kod zmienionych modułów JS. Gdy środowisko uruchomieniowe je otrzyma, zastępuje stary kod modułów nowym:

Aktualizacja HMR zawiera nieco więcej niż tylko kod modułu, który chcemy zmienić, ponieważ samo zastąpienie kodu nie wystarczy, by środowisko uruchomieniowe uwzględniło zmiany. Problem w tym, że system modułów mógł już zapisać w pamięci podręcznej eksporty modułu, który aktualizujemy. Przykładowo, rozważ aplikację z dwóch modułów:
// 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;
Moduł log wypisuje podaną wiadomość wraz z aktualną datą dostarczoną przez moduł time.
Gdy aplikacja jest bundle'owana, React Native rejestruje każdy moduł w systemie modułów za pomocą funkcji __d. Dla tej aplikacji, pośród wielu definicji __d, będzie jedna dla log:
__d('log', function() {
... // module's code
});
To wywołanie opakowuje kod każdego modułu w anonimową funkcję, którą powszechnie nazywamy funkcją fabryczną. System modułowy śledzi funkcję fabryczną każdego modułu, czy została już wykonana oraz wynik tego wykonania (eksporty). Gdy moduł jest wymagany, system modułowy albo dostarcza buforowane eksporty, albo wykonuje funkcję fabryczną modułu po raz pierwszy i zapisuje wynik.
Załóżmy, że uruchamiasz aplikację i wymagasz modułu log. W tym momencie żadna funkcja fabryczna log ani time nie została jeszcze wykonana, więc żadne eksporty nie są buforowane. Następnie użytkownik modyfikuje time, aby zwracał datę w formacie MM/DD:
// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}
module.exports = bar;
Packager wyśle nowy kod time do środowiska wykonawczego (krok 1). Gdy log zostanie ostatecznie wymagany, wyeksportowana funkcja zostanie wykonana z uwzględnieniem zmian w time (krok 2):

Teraz załóżmy, że kod log wymaga time jako zależności najwyższego poziomu:
const time = require('./time'); // top level require
// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}
module.exports = log;
Gdy log jest wymagany, środowisko wykonawcze zbuforuje jego eksporty oraz eksporty time (krok 1). Jeśli time zostanie zmodyfikowany, proces HMR nie może zakończyć się po prostu zastąpieniem kodu time. Gdyby tak zrobił, wykonanie log korzystałoby z buforowanej kopii time (starego kodu).
Aby log uwzględnił zmiany w time, musimy wyczyścić jego buforowane eksporty, ponieważ jeden z modułów od których zależy został "gorąco zamieniony" (krok 3). Gdy log zostanie ponownie wymagany, jego funkcja fabryczna zostanie wykonana, wymagając time i pobierając jego nowy kod.

API HMR
HMR w React Native rozszerza system modułów poprzez wprowadzenie obiektu hot. To API opiera się na webpackowym rozwiązaniu. Obiekt hot udostępnia funkcję accept pozwalającą zdefiniować callback wykonywany przy wymianie modułu. Np. jeśli zmienimy kod time jak poniżej, po każdej modyfikacji zobaczymy "time changed" w konsoli:
// time.js
function time() {
... // new code
}
module.hot.accept(() => {
console.log('time changed');
});
module.exports = time;
Uwaga: tylko w rzadkich przypadkach będziesz musiał ręcznie używać tego API. Gorące przeładowanie powinno działać "out of the box" w większości typowych scenariuszy.
Środowisko wykonawcze HMR
Jak widzieliśmy, czasem samo zaakceptowanie aktualizacji HMR nie wystarczy, ponieważ moduł używający zamienianego komponentu mógł zostać już wykonany z buforowanymi importami. Np. załóżmy, że drzewo zależności w aplikacji filmowej zawiera nadrzędny MovieRouter zależny od widoków MovieSearch i MovieScreen, które zależą od modułów log i time z poprzednich przykładów:

Jeśli użytkownik odwiedzi widok wyszukiwania filmów, ale nie drugi widok, wszystkie moduły oprócz MovieScreen będą miały buforowane eksporty. Przy zmianie w module time, środowisko wykonawcze musi wyczyścić eksporty log by uwzględniło zmiany w time. Proces się tu nie kończy: środowisko rekursywnie powtórzy tę operację aż do zaakceptowania wszystkich modułów nadrzędnych. Pobierze moduły zależne od log i spróbuje je zaakceptować. Dla MovieScreen może przerwać (nie był jeszcze wymagany). Dla MovieSearch wyczyści eksporty i rekursywnie przetworzy jego moduły nadrzędne. Na koniec zrobi to samo dla MovieRouter i zakończy proces (żaden moduł od niego nie zależy).
Aby przejść drzewo zależności, środowisko wykonawcze otrzymuje od Packagera odwrotne drzewo zależności w aktualizacji HMR. Dla tego przykładu środowisko otrzyma obiekt JSON:
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
Komponenty Reacta
Komponenty React są trudniejsze w obsłudze przy Hot Reloadingu. Problem polega na tym, że nie możemy po prostu zastąpić starego kodu nowym, ponieważ stracilibyśmy stan komponentu. W przypadku aplikacji webowych React, Dan Abramov zaimplementował transformację Babel, która wykorzystuje API HMR webpacka. W skrócie, jego rozwiązanie działa poprzez tworzenie proxy dla każdego komponentu React w czasie transformacji. Te proxy przechowują stan komponentu i delegują metody cyklu życia do właściwych komponentów, które podlegają hot reloadowaniu:
Oprócz tworzenia proxy, transformacja definiuje funkcję accept z kodem wymuszającym ponowne renderowanie komponentu przez React. Dzięki temu możemy aktualizować kod renderowania bez utraty stanu aplikacji.
Domyślny transformer w React Native używa babel-preset-react-native, który jest skonfigurowany do wykorzystania react-transform w taki sam sposób, jak w projektach webowych React z webpackiem.
Store Reduxa
Aby włączyć Hot Reloading w store'ach Reduxa, wystarczy użyć API HMR podobnie jak w projektach webowych z webpackiem:
// 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;
};
Gdy zmienisz reducer, kod akceptujący tę zmianę zostanie wysłany do klienta. Klient zorientuje się, że reducer nie wie jak sam siebie zaakceptować, więc poszczy moduły, które się do niego odwołują i spróbuje je zaakceptować. Ostatecznie proces dotrze do pojedynczego store'a - modułu configureStore, który zaakceptuje aktualizację HMR.
Podsumowanie
Jeśli chcesz pomóc w ulepszaniu Hot Reloadingu, zachęcam do przeczytania postu Dana Abramova o przyszłości tej funkcji oraz do kontrybucji. Przykładowo, Johny Days pracuje nad wsparciem wielu połączonych klientów. Polegamy na was wszystkich w utrzymaniu i rozwijaniu tej funkcji.
Dzięki React Native mamy okazję przemyśleć sposób budowania aplikacji, aby zapewnić świetne doświadczenia deweloperskie. Hot Reloading to tylko jeden element układanki - jakie inne kreatywne rozwiązania możemy wdrożyć, żeby było jeszcze lepiej?