React Nativeの目標は、可能な限り最高の開発者エクスペリエンスを提供することです。その大きな部分は、ファイルを保存してから変更を確認できるようになるまでの時間です。私たちの目標は、アプリが成長しても、このフィードバックループを1秒未満にすることです。
この理想に近づくことができたのは、主に3つの機能によるものです。
言語としてJavaScriptを使用すると、コンパイルサイクル時間が長くなりません。
es6/flow/jsxファイルをVMが理解できる通常のJavaScriptに変換するPackagerと呼ばれるツールを実装します。これは、高速な増分変更を可能にするためにメモリに中間状態を保持し、複数のコアを使用するサーバーとして設計されました。
保存時にアプリをリロードするライブリロードと呼ばれる機能を構築します。
この時点で、開発者にとってのボトルネックは、アプリをリロードするのにかかる時間ではなく、アプリの状態が失われることです。一般的なシナリオは、起動画面から複数の画面離れた場所にある機能に取り組むことです。リロードするたびに、同じパスを何度もクリックして機能に戻る必要があり、サイクルが数秒長くなります。
ホットリロード
ホットリロードの背後にあるアイデアは、アプリを実行したまま、実行時に編集したファイルの新しいバージョンを挿入することです。これにより、特にUIを調整している場合に非常に便利な状態を失うことはありません。
百聞は一見にしかずです。ライブリロード(現在)とホットリロード(新規)の違いをご覧ください。
VIDEO
よく見ると、レッドボックスから回復できること、以前になかったモジュールをフルリロードせずにインポートを開始できることに気付くはずです。
警告: JavaScriptは非常にステートフルな言語であるため、ホットリロードを完全に実装することはできません。実際には、現在のセットアップは、通常のユースケースの大部分でうまく機能しており、何かが混乱した場合に備えて、フルリロードが常に利用可能であることがわかりました。
ホットリロードは0.22から利用可能になり、有効にすることができます
開発者メニューを開きます
「ホットリロードを有効にする」をタップします
要するに実装
これで、必要な理由と使用方法がわかったので、楽しい部分が始まります:実際にどのように機能するのか。
ホットリロードは、Hot Module Replacement またはHMRと呼ばれる機能に基づいて構築されています。これは最初にwebpackによって導入され、React Native Packager内に実装しました。 HMRにより、Packagerはファイルの変更を監視し、アプリに含まれるシンHMRランタイムにHMRアップデートを送信します。
要するに、HMRアップデートには、変更されたJSモジュールの新しいコードが含まれています。ランタイムがそれらを受け取ると、古いモジュールのコードを新しいコードに置き換えます
HMRアップデートには、変更したいモジュールのコードだけよりも少し多くの情報が含まれています。これは、ランタイムが変更を検出するには、それを置き換えるだけでは不十分なためです。問題は、モジュールシステムが更新したいモジュールのエクスポート をすでにキャッシュしている可能性があることです。たとえば、次の2つのモジュールで構成されるアプリがあるとします。
function log ( message ) { const time = require ( './time' ) ; console . log ( ` [ ${ time ( ) } ] ${ message } ` ) ; } module . exports = log ;
function time ( ) { return new Date ( ) . getTime ( ) ; } module . exports = time ;
モジュールlog
は、モジュールtime
によって提供された現在の日付を含む、提供されたメッセージを出力します。
アプリがバンドルされると、React Nativeは__d
関数を使用して、各モジュールをモジュールシステムに登録します。このアプリの場合、多くの__d
定義の中で、log
の定義が1つあります。
__d ( 'log' , function ( ) { ... } ) ;
この呼び出しは、各モジュールのコードを、通常はファクトリ関数と呼ばれる匿名関数にラップします。モジュールシステムランタイムは、各モジュールのファクトリ関数、それがすでに実行されているかどうか、およびそのような実行の結果(エクスポート)を追跡します。モジュールが必要な場合、モジュールシステムはすでにキャッシュされているエクスポートを提供するか、モジュールのファクトリ関数を初めて実行して結果を保存します。
そこで、アプリを起動してlog
を必要とするとします。この時点で、log
とtime
のファクトリ関数のどちらも実行されていないため、エクスポートはキャッシュされていません。次に、ユーザーはtime
を変更して、MM/DD
で日付を返すようにします。
function bar ( ) { const date = new Date ( ) ; return ` ${ date . getMonth ( ) + 1 } / ${ date . getDate ( ) } ` ; } module . exports = bar ;
Packagerは、`time`の新しいコードをランタイムに送信し(ステップ1)、`log`が最終的にrequireされたときに、エクスポートされた関数が実行され、その際`time`の変更が反映されます(ステップ2)。
ここで、`log`のコードがトップレベルのrequireとして`time`を必要とする場合を考えます。
const time = require ( './time' ) ; function log ( message ) { console . log ( ` [ ${ time ( ) } ] ${ message } ` ) ; } module . exports = log ;
`log`がrequireされると、ランタイムはそのエクスポートと`time`のエクスポートをキャッシュします(ステップ1)。その後、`time`が変更された場合、HMRプロセスは、単に`time`のコードを置き換えただけで完了することはできません。もしそうしてしまうと、`log`が実行されるときに、キャッシュされた`time`のコピー(古いコード)で実行されることになります。
`log`が`time`の変更を反映するためには、依存関係にあるモジュールがホットスワップされたため、キャッシュされたエクスポートをクリアする必要があります(ステップ3)。最後に、`log`が再度requireされると、そのファクトリ関数が実行され、`time`をrequireし、新しいコードを取得します。
HMR API
React NativeにおけるHMRは、`hot`オブジェクトを導入することでモジュールシステムを拡張します。このAPIは、webpack のAPIに基づいています。`hot`オブジェクトは、モジュールをホットスワップする必要があるときに実行されるコールバックを定義できる`accept`という関数を公開します。たとえば、`time`のコードを次のように変更すると、`time`を保存するたびに、コンソールに「time changed」と表示されます。
function time ( ) { ... } module . hot . accept ( ( ) => { console . log ( 'time changed' ) ; } ) ; module . exports = time ;
このAPIを手動で使用する必要があるのはまれなケースのみです。ほとんどの一般的なユースケースでは、ホットリローディングはすぐに機能するはずです。
HMRランタイム
前述したように、ホットスワップされるモジュールを使用するモジュールが既に実行されてインポートがキャッシュされている場合、HMR更新を受け入れるだけでは不十分な場合があります。たとえば、映画アプリの例の依存関係ツリーに、前の例の`log`と`time`モジュールに依存する`MovieSearch`と`MovieScreen`ビューに依存するトップレベルの`MovieRouter`があるとします。
ユーザーが映画の検索ビューにアクセスしたが、もう一方のビューにはアクセスしなかった場合、`MovieScreen`を除くすべてのモジュールはエクスポートをキャッシュしていることになります。モジュール`time`に変更を加えると、ランタイムは`time`の変更を反映するために`log`のエクスポートをクリアする必要があります。プロセスはそこで終わりません。ランタイムは、すべての親が受け入れられるまで、このプロセスを再帰的に繰り返します。そのため、`log`に依存するモジュールを取得し、それらを受け入れようとします。`MovieScreen`については、まだrequireされていないため、そこで中断できます。`MovieSearch`については、エクスポートをクリアし、親を再帰的に処理する必要があります。最後に、`MovieRouter`についても同じ処理を行い、依存するモジュールがないため、そこで完了します。
依存関係ツリーをたどるために、ランタイムは、HMR更新時にPackagerから逆依存関係ツリーを受け取ります。この例では、ランタイムは次のようなJSONオブジェクトを受け取ります。
{ modules : [ { name : 'time' , code : } ] , inverseDependencies : { MovieRouter : [ ] , MovieScreen : [ 'MovieRouter' ] , MovieSearch : [ 'MovieRouter' ] , log : [ 'MovieScreen' , 'MovieSearch' ] , time : [ 'log' ] , } }
Reactコンポーネント
Reactコンポーネントは、ホットリローディングを機能させるのが少し困難です。問題は、コンポーネントの状態が失われるため、古いコードを新しいコードに単純に置き換えることができないことです。ReactのWebアプリケーションの場合、Dan Abramov 氏は、この問題を解決するためにwebpackのHMR APIを使用するbabelのトランスフォーム を実装しました。簡単に言うと、彼のソリューションは、*トランスフォーム時*にすべてのReactコンポーネントのプロキシを作成することによって機能します。プロキシはコンポーネントの状態を保持し、ライフサイクルメソッドを実際のコンポーネントに委任します。これはホットリロードするものです。
プロキシコンポーネントの作成に加えて、トランスフォームは、Reactにコンポーネントを再レンダリングさせるコードを含む`accept`関数も定義します。このようにして、アプリの状態を失うことなく、レンダリングコードをホットリロードできます。
React Nativeに付属しているデフォルトのトランスフォーマー は、`babel-preset-react-native`を使用します。これは、webpackを使用するReact Webプロジェクトで使用する場合と同じ方法で、`react-transform`を使用するように構成 されています。
Reduxストア
Redux ストアでホットリローディングを有効にするには、webpackを使用するWebプロジェクトで行うのと同様に、HMR APIを使用するだけです。
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 ; } ;
リデューサーを変更すると、そのリデューサーを受け入れるコードがクライアントに送信されます。次に、クライアントは、リデューサーが自分自身を受け入れる方法を知らないことに気付くため、それを参照するすべてのモジュールを検索して受け入れようとします。最終的に、フローは単一のストアである`configureStore`モジュールに到達し、HMR更新を受け入れます。
ホットリローディングの改善に協力したい場合は、Dan Abramovのホットリローディングの将来に関する投稿 を読んで、貢献することをお勧めします。たとえば、Johny Daysは複数の接続されたクライアントで動作するようにする 予定です。この機能を維持し、改善するために、皆様のご協力をお願いいたします。
React Nativeでは、優れた開発者エクスペリエンスを実現するために、アプリの構築方法を再考する機会があります。ホットリローディングはそのパズルの一部にすぎません。さらに改善するために、他にどのような斬新なハックが可能でしょうか。