クレードルからのReact-reduxプロジェクトの看護

イントロ

今年の初めに、HeadHunterでは、クライアント企業でのさまざまなHRプロセスの自動化を目的としたプロジェクトを開始しました。 前面のこのプロジェクトのアーキテクチャは、React-Reduxスタックでした。



9か月間、彼は従業員をテストするための小さなアプリケーションから、今日では「才能評価」と呼ばれるマルチモジュールプロジェクトに成長しました。 成長するにつれて、次の質問に直面しました。





これは、コンポーネント、レデューサーのアーキテクチャへのアプローチの変更に現れました。



プロジェクトをどのように開発し、どのような決定を下したかについて話しましょう。 それらのいくつかは、「holivny」であることが判明するかもしれませんが、他の人は、反対に、reduxで大規模なプロジェクトを構築する際の「クラシック」です。 以下に説明するプラクティスが、react-reduxアプリケーションを構築する際に役立つことを望み、実際の例が、このアプローチまたはそのアプローチの仕組みを理解するのに役立つことを願っています。



一般的な情報。 イントロ。



WebアプリケーションはSPAです。 クライアントでのみアプリケーションをレンダリングする(以下で理由を説明します)。 開発では、React-reduxスタックをさまざまなミドルウェア、たとえばredux-thunkとともに使用しました。 このプロジェクトでは、アセンブリ中にbabelでコンパイルされたes6を使用します。 開発は、es7を使用せずに実行されます。これは、標準では受け入れられないソリューションを採用したくないためです。

プロジェクトはtest.hh.ruで部分的に利用可能ですが、hhに登録された会社でのみ使用できます。



プロジェクト構造



プロジェクトの構造として、最初にアプリケーションをいくつかの部分に分けました。 結果は、このスタックの「古典的な」構造になりました。









ここに:





この構造により、プロジェクトの開発を開始しました。 システムのすべての要件と、開発プロセス中に発生する可能性のあるすべての困難を事前に予測することは不可能です。 したがって、プロジェクトの漸進的な近代化の道を選択しました。 次のように機能します。



  1. 特定のアプリケーションモデルを選択します。
  2. 一部のアプローチでは問題を希望どおりに解決できないという事実に直面しています。
  3. リファクタリングを実行し、アプリケーションの構造を変更します。


時間の経過に伴うアプリケーションの構造の変化を分析してみましょう。



構造の構築。 幼少期


プロジェクトが若く、多数のファイルで大きくなりすぎていない場合は、単純な構築モデルを選択することをお勧めします。 それはまさに私たちがやったことです。



すべてのアクションクリエーターは、原則に従って格納されていました。つまり、1つのグループの操作、つまり1つのファイルにまとめられています。 すべてのファイルは、フォルダー内の1つのリストに保存されていました。



このように見えた:









「1つの操作グループ」とは何ですか? 1つのアクション作成者ファイルが、1つのレデューサー内のアクションを担当していると言えます。 この場合、アクションの作成者とリデューサーの名前は同じですが、100%のケースでは発生しません。 意味では、これらは1つのタイプのオブジェクトに対する操作です。 たとえば、employee.jsには、特定のオブジェクト(「従業員」)を操作するためのさまざまなメソッドが含まれています。 これは彼に関する情報の取得、データの読み込みエラーの処理、データの変更、新しい従業員の追加です。



そのような操作の典型的なコード:



 export function loadEmployee(id) { return dispatch => { //  action,   ,      employee dispatch(fetchEmployee()); fetch(`/employees/${id}`) //    .then(checkStatus) //  .then(result => result.json()) .then(result => { //  ,      (,       ,     ) dispatch(receiveEmployee(result)); }) .catch(() => { //  ,     : 1)   ; 2)    ; 3)     ; 4)    react . dispatch(releaseEmployee()); dispatch(errorRecord(t('employee.error'))); }); }; }
      
      





このコードでは、サーバー側とレデューサーの動作とコンポーネントの反応の両方で、一度のキャッチでエラーのグループを一度に処理することに注意することが重要です。 より詳細には、このようなアーキテクチャを作成することにした理由は、記事の最後にあるあいまいなソリューションのセクションにあります。



同様のビルドモデルがレデューサー、コンテナー、およびコンポーネントに採用されました。 コンポーネントの「慣習」はごくわずかです。 各コンポーネントは個別のフォルダーにあります。 これにより、.jsモジュールとスタイルの両方を配置できます。また、場合によっては、フォルダー内に画像とその他のデータを配置することもできます。









思春期


この構造は、最初のモジュールの作業が終了するまで正確に存続しました。 新しいモジュールに移る前に、結果のコンポーネントのセットを冷静に見て、システムの増加に伴い、プロジェクト構造に対するこのアプローチは混乱を招き、1つのシステムレベルに膨大な数のファイルが存在することになります。 さらに、すべてのjsを1つのファイルではなく前面に送信することにした場合(たとえば、jsバンドルのサイズがメガバイト単位のminifaen-agliphene情報で増加する場合)、しかしいくつかのバンドルで、すべてのモジュールの依存関係をかなり長い間解決する必要があります。



そのため、次の決定を下しました(2つのモジュールAとBについて説明しますが、任意の数にスケーリングできます)。



  1. すべてのアクションクリエーター、レデューサー、およびコンテナーは、3つのタイプ(common、moduleA、moduleB)に分類されます。



  2. アクションタイプを記述しない定数はすべて、constantsフォルダー内にあります。 同じフォルダーには、必要なアクションの種類を説明するアクションディレクトリが含まれています。 さらに、それらは2つのタイプに分けられます。



  3. コンポーネントは4つのタイプに分けられます。



    common-一般的な反応モジュールが含まれます。 それらはダミーの反応コンポーネントを表します(つまり、UIを記述するコンポーネントのみがそれを制御せず、アプリケーションの状態に直接依存せず、アクションを呼び出しません)、実際には、これらはアプリケーションのどこでも使用されるコンポーネントです。



    ブロック-アプリケーションの全体的な状態に依存するコンポーネント。 たとえば、ブロック「通知」または「通知」。

    moduleA、moduleB-モジュールの特定のコンポーネント。



    ブロックとモジュールはどちらもスマートコンポーネントである可能性があり、何らかのアクションを引き起こすなど


受け入れられた規則により、アプリケーション構造は次のようになり始めました。









したがって、プロジェクトのモジュールの本質を説明する明確な構造を得ました。そこでは、jsファイルと1つまたは別のモジュールの比率を簡単に決定できます。つまり、モジュールに異なるバンドルを作成することを決定した場合、これは難しくありません(ちょうど「必要な部分へのWebpack」。



アプリケーション状態のモジュールへの分離



さて、私たちはファイルを構造的に分割しましたが、目は喜ぶでしょうが、ロジック、レデューサーのレベルでのマルチモジュール構造のサポートについてはどうでしょうか?



小規模なアプリケーションの場合、ルートレデューサーの説明は通常次のとおりです。



 export const createReducer = () => { return combineReducers({ account, location, records, managers, tooltip, validation, progress, restrictedData, command, translations, status, features, module, settings, menu, routing: routeReducer, }); };
      
      





小規模なアプリケーションの場合、すべてのデータが単純なコレクションにあるため、これは便利です。 しかし、アプリケーションのサイズが増加すると、転送されるデータの数が増加するため、レデューサーを粉砕してモジュールに分割する必要があります。 これをどのように行うことができますか? 結果に移る前に、2つの状況を検討します。



最初の状況:「オブジェクトを個々の減速機に粉砕する」


従業員エンティティがあるとします。 このエンティティは、さまざまな状況を分析し、意思決定を記述するのに最適です。 エンティティ「従業員」を使用して、会社のマネージャーはさまざまなアクションを実行できます。アップロード、編集、作成、テストへの招待、テスト結果の表示。 このフィールドは、ステータスとデータの2つのフィールドを持つオブジェクトです。 statusは、フィールドの現在の状態(FETCH \ COMPLETE \ FAIL)、およびデータ-有用なデータ、従業員の配列を決定します。 これは、サーバーからデータを取得し、従業員のリストを表示するのに十分です。

次に、従業員を選択する機能を追加する必要があります。









この問題を解決するには、次の3つの方法があります。



方法1:


employees.data配列内の要素を変更して、id、name、surname、positionに加えて、各要素に選択したフィールドが含まれるようにします。 チェックボックスをレンダリングするとき、このフィールドを見て、たとえば次のように合計金額を考慮します。



 employees.data.reduce((employee, memo) => employee.selected ? memo + 1 : memo, 0);
      
      





将来、選択したIDを送信する必要がある場合、それらはすべて同じフィルターを介して検出されます。 選択したものの追加/削除は、マップを介して同様に行われます。 この方法の利点は、データが一元的に保存されることです。 欠点は、「くしゃみ」ごとにオブジェクト全体に触れることです(選択したフラグを追加/削除します)。 これにより、かなりの数のアクションが発生します。 選択した従業員と連携するためのロジックが、従業員削減者、アクション作成者に追加されます。 これらの部分を分離したい場合(選択した従業員と連携しても従業員削減者のメインタスクに影響しないため、従業員を削除してページネーションを行う)、他の2つの方法を検討する必要があります。



2番目の方法:


selectedEmployees



などの別のレデューサーを作成します。 これは不変のマップであり(必要に応じて、オブジェクトのみを配列できます)、選択したIDのみを格納します。 この構造にidを追加および削除すると、はるかに簡単に見えます。 チェックボックスのレンダリングは、パスstore.selectedEmployees.has(id)



に従う必要があるという事実によってのみ複雑になります。 したがって、選択した従業員とのすべての作業は、別個のレデューサー、別個のアクショングループに送られます。



このソリューションは、状態の他の部分を変更およびロードせず、近くに別の部分を追加するという点で優れています。 したがって、アプリケーションがこれら2つのレデューサーで構成されている場合、次の構造が得られます。



 employees {state: COMPLETE, data: [{id: 1, ...}, {id: 2, ...}]} selectedEmployees Map({1 => true})
      
      





3番目の方法:


時間が経つにつれて、毎回2番目のメソッドを使用すると、従業員、selectedEmployees、従業員、employeeTestなどで構成される肥大化した状態になります。リデューサーは相互に関連していることに注意してください:selectedEmployeesは従業員を指し、employeeTestは従業員を指します。 したがって、複合減速機を作成してアプリケーションを構成します。 これにより、より明確で便利な構造が得られます。



 employees: - list [{id: 1, ...}, {id: 2, ...}] - selected Map({1 => true}) - status COMPLETE employee: - data - test
      
      





このような構造は、レデューサーの階層を構築することで実現できます。



 export const createReducer = () => { return combineReducers({ employees: combineReducers({ list: employees, selected: selectedEmployees, status: employeesStatus }) routing: routeReducer, }); };
      
      





注:このレベルではステータスは重要ではありません。以前のように、従業員のリストを担当するリデューサー内に残すことができます。



これが、使用することにした方法です。 オブジェクトのグループ作業が必要なさまざまな状況に最適です。



状況2:「データの正規化」


SPAの開発では、データの正規化が大きな役割を果たします。 今後も従業員と協力していきます。 適切な従業員を選択し、テストに送りました。 その後、従業員は合格し、会社はテスト結果を受け取ります。 アプリケーションにはデータを保存する必要があります-従業員のテスト結果。 1人の従業員が複数の結果を持つ場合があります。

同様の問題は、いくつかの方法でも解決できます。



悪い道(男バンド)


この方法は、テストに関する完全なデータを内部に保存するような方法で従業員構造を改良することを提案します。 それは:



 employees: { status: COMPLETE, list: [ { id: 1, name: '', testResults: { id: 123, score: 5 speed: 146, description: '...' ... } } ] }
      
      





ストアにオーケストラオブジェクトを取得しました。 これは不便で、余分なネストがあります。 正規化されたデータを使用したソリューションは、ずっときれいに見えます。



いい方法


従業員とテストにはidがあることに注意してください。 データベースには、従業員IDだけでテストと従業員の間に接続があります。 バックエンドから同じアプローチを取り、次の構造を取得します。



 employees: { status: COMPLETE, list: [ { id: 1, name: '', testResults: [123] } ] }, tests: { 123: { id: 123, score: 5 speed: 146, description: '...' ... } }
      
      





ルートレデューサーでは次のようになります。



 export const createReducer = () => { return combineReducers({ employees: combineReducers({ list: employees, selected: selectedEmployees, status: employeesStatus }), tests, routing: routeReducer, }); };
      
      





ストアを構築し、すべてを棚に置き、アプリケーションの機能を明確に理解しました。



モジュールを追加する


モジュールを追加するとき、状態ビューを共通のグループと異なるモジュールに属するグループに分割します。



 export const createReducer = () => { return combineReducers({ moduleA: combineReducers({ employees: combineReducers({ list: employees, selected: selectedEmployees, status: employeesStatus }), tests, }), moduleB: combineReducers({...}), // common     ,    notifications, routing: routeReducer, }); };
      
      





このメソッドを使用すると、ストアの構造内でファイル構造を繰り返すことができます。 したがって、アプリケーションの構造は、ファイルレベルと論理レベルの両方で繰り返されます。 これは、アプリケーション全体の作業全体の理解が簡素化され、新しい開発者がプロ​​ジェクトに参加するためのしきい値が低くなることを意味します。



React Component Buildingの原則



アプリケーション構造の構築に成功し、コードをモジュールに分割しました。アクションとレデューサーは階層的であり、スケーラブルなコードを記述できます。 UIを担当するコンポーネントをどのように構築するかという質問に戻りましょう。



私たちはリデュースの原則を順守します。つまり、グローバルな側面が唯一の真実の源です。 私たちの目標は、ストアからのデータに応じて、いつでもサイトの表示を完全に復元できるようにアプリケーションを構築することです。 許容されるエラーのうち、アニメーション/ドロップダウンの状態、ツールチップ-オープン/クローズに関するデータを復元しないようにします。



これらの条件に基づいて、アプリケーションモデルを構築します。



スマートコンポーネントとダムコンポーネント


以下のリソースでこれらの特性を構築および分離するための十分に説明されたアプローチ:



» Medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.aszvx1fh1

» Jaketrent.com/post/smart-dumb-components-react



同様のアプローチを取ります。 すべての共通コンポーネントは、プロパティなどのみを受け取るダミーコンポーネントです。特定のモジュールに関連するコンポーネントは、スマートまたはダムのいずれかです。 構造内でそれらを明確に区別するのではなく、近くに保存されます。



きれいな部品


ほぼすべてのコンポーネントに独自の状態はありません。 そのようなコンポーネントは、接続デコレータを介して、または上位コンポーネントからの「ウォーターフォール」の助けを借りて、状態、アクション作成者を受け取ります。



しかし、独自の状態を持つコンポーネントの約5%があります。 これらのコンポーネントは何ですか? 彼らのアートには何が保存されていますか? このアプリケーションの同様のコンポーネントは、2つのグループに分けることができます。



ポップアップステータス:








別の例:









このグループには、情報を保存するコンポーネントが含まれます-ポップアップ要素(ドロップダウン、ツールチップ)を表示するかどうか。



そのようなコンポーネントの典型的なコード:



 import React, { Component } from 'react'; import PseudoLink from '../pseudoLink/pseudoLink'; import Dropdown from '../../common/dropdown/dropdown'; import './dropdownHint-styles.less'; class DropdownHint extends Component { constructor(props, context) { super(props, context); this.state = { dropdown: false, }; } renderDropdown() { if (!this.state.dropdown) { return ''; } return ( <Dropdown onClose={() => { this.setState({ dropdown: false, }); }}> <div className='dropdown-hint'>{this.props.children}</div> </Dropdown> ); } render() { return ( <PseudoLink onClick={() => { this.setState({ dropdown: true, }); }}> {this.renderDropdown()} {this.props.text} </PseudoLink> ); } } export default DropdownHint;
      
      







内部状態の2番目のタスク:一時的なキャッシュまたは追加。




次の例を考えてみましょう。









画面上部の通知は、表示または非表示になるとアニメーション化されます。 通知の本質はグローバルアプリケーション状態に保存されますが、通知を非表示にする(その後の削除を伴う)プロセスでは、アニメーションなどの重要でない情報でグローバル状態を汚染することなく、いくつかのアプローチのいずれかを使用できます。



最初の方法はキャッシュです。

かなり簡単な方法。 NotificationsManagerコンポーネントを作成します。これは、Notificationコンポーネントのレンダリングを担当します。 NotificationsManagerが次の通知をレンダリングした後、タイマーを開始し、その後、通知を非表示にするアクションが呼び出されます。 呼び出す前に、NotificationsManagerは通知をキャッシュします。 これにより、通知自体をストアから削除できます。また、コンポーネントのローカル状態内のキャッシュされたデータにより、消失をアニメーション化できます。

この方法は、私たちの側を欺くという点で不便です-彼は通知がないと信じていますが、実際にはコンポーネントのローカルバージョンに保存されています。 「誠実な」パーティが必要なので、この方法は私たちには適していません。



2番目の方法-ストアからの情報をローカルに補完します。

この方法は、ストアからデータを復元するための高精度を妨げないため、より正直で魅力的です。 NotificationsManagerは、ストアの側から通知への変更を受け取ると、通知で実行する必要のある情報(外観の表示、消失、または何もしない)に関する情報をその状態に追加するという事実から成ります。 この場合、NotificationManagerは、通知の消失のアニメーションが完了した場合にのみ、CLOSE_NOTIFICATIONアクションを介してパーティに通知します。 このアプローチにより、ストア内の不必要な情報(通知アニメーションステータス)を破棄することができ、同時にストアは「唯一の真実の源」であり、アプリケーション全体の表示を正確に復元できます。



このアプローチがどのように機能するかをおおよそ説明します。 ストアでは次のものを取得します。



 notifications: [ { id: 1, text: '  ,   ' }, { id: 2, text: '  ,   ' }, { id: 3, text: '  , ?' }, ]
      
      





ローカルコンポーネントのスタイル:



 notifications: { 1: 'out', 3: 'in' }
      
      





例を見てみましょう。 アプリケーション全体のレベルでは、3つの通知があることがわかります。 コンポーネントのローカルレベルでは、アプリケーションにとって意味を持たない情報を保存しますが、ローカルレンダリングには重要です。id=== 1が消え、id == 3が残り、id === 2は静的です。



, . , . . “- ”.



. , , , . . , . .



.



. react-router, redux react-router-redux.

.

, json, : , , ( ). .



:



  1. js ;
  2. , ;
  3. node.js .


json-. acceptType-.



:









employees (employees) (account).



, html-, json:



 window.pageData = { account: //    employees: //    }
      
      





, , .



:



 const initialData = window.pageData.employees; export default function employees(state = initialData, actions) { // reducer }
      
      





employees , -, .



— . action creator, acceptType: application/json ( ). . “ ”.







action creator':



 export function loadEmployee(id) { return dispatch => { //  action,   ,      employee dispatch(fetchEmployee()); fetch(`/employees/${id}`) //    .then(checkStatus) //  .then(result => result.json()) .then(result => { //  ,      (,       ,     ) dispatch(receiveEmployee(result)); }) .catch(() => { dispatch(releaseEmployee()); dispatch(errorRecord(t('employee.error'))); }); }; }
      
      





receiveEmployee. , , , . . — catch :



  1. ;
  2. ;
  3. ;
  4. react .


- “” . , , t('employee.error'). t — , ().



“” . , , , . . , : . . , :



 export function loadEmployee(id) { return async dispatch => { //  action,   ,      employee dispatch(fetchEmployee()); let json; try { const res = await fetch(`/employees/${id}`); checkStatus(res); json = await getJson(res); } catch () { dispatch(releaseEmployee()); dispatch(errorRecord(t('employee.error'))); } dispatch(receiveEmployee(result)); }; }
      
      





try-catch dispatch:



dispatch(receiveEmployee(result));



, catch:



 export function loadEmployee(id) { return async dispatch => { //  action,   ,      employee dispatch(fetchEmployee()); let json; try { const res = await fetch(`/employees/${id}`); checkStatus(res); json = await getJson(res); } catch () { dispatch(releaseEmployee()); dispatch(errorRecord(t('employee.error'))); } try { dispatch(receiveEmployee(result)); } catch() { dispatch(releaseEmployee()); dispatch(errorRecord(t('employee.error'))); } }; }
      
      





, .



, , , , .





, , , “ ”. (, 5—10 , 40—50 ) . ? , . , . , “ ” , . , , , -, , -, (, , ). , , . . , .



:

» github.com/reactjs/redux/issues/37#issue-85098222

» gist.github.com/gaearon/0a2213881b5d53973514

» stackoverflow.com/questions/34095804/replacereducer-causing-unexpected-key-error



:



ステップ1

, . , . ( ).



ステップ2

. :



 export const createReducer = () => { return combineReducers({ moduleA: combineReducers({ employee: combineReducers({ list: employees, selected: selectedEmployees, status: employeesStatus }), tests, }), moduleB: combineReducers({...}), // common     ,    notifications, routing: routeReducer, }); }
      
      





:



 export const createReducer = (dynamicReducers = {}) => { return combineReducers(Object.assign({}, { notifications, routing: routeReducer, // ,    module, }, dynamicReducers)); };
      
      





, :



 export const aReducers = { moduleA: combineReducers({ employee: combineReducers({ list: employees, selected: selectedEmployees, status: employeesStatus }), tests, }), }
      
      





ステップ3

, :



 export function configureStore() { const store = createStoreWithMiddleware(createReducer()); store.dynamicReducers = {}; storeInstance = store; switch (store.getState().module) { case A: injectAsyncReducer(aReducers); break case B: injectAsyncReducer(bReducers); break; //   } return store; } export function injectAsyncReducer(reducers) { if (reducers !== storeInstance.dynamicReducers) { storeInstance.dynamicReducers = reducers; storeInstance.replaceReducer(createReducer(storeInstance.dynamicReducers)); } }
      
      





injectAsyncReducer, .



ステップ4

action creator. action . :



 export const checkoutAModule = () => { //    injectAsyncReducer(aReducers); return { type: CHECKOUT_MODULE, module: A, }; };
      
      





, , .



ステップ5

, . :



 import React, { Component } from 'react'; import { connect } from 'react-redux'; import { checkoutModuleA } from '../../actions/module'; import { A } from '../../constants/module'; export default function checkoutA() { return Container => { class AContainer extends Component { componentWillMount() { if (this.props.module !== A) { this.props.checkoutModuleA(); } } render() { return ( <Container {...this.props} /> ); } } return connect( state => { return { module: state.module, }; }, { checkoutModuleA, } )(AContainer); }; }
      
      





, , .



6.

:



 import React, { Component } from 'react'; import { connect } from 'react-redux'; import { compose } from 'redux'; import Content from '../components/content/content'; import Managers from '../components/managers/managers'; import composeRestricted from './restricted/restricted'; import { loadManagers } from '../actions/managers'; import title from './title/title'; class ManagersPage extends Component { componentWillMount() { if (!this.props.firstLoad) { this.props.loadManagers(this.props.location); } } render() { return ( <div className='page'> <Content> <Managers /> </Content> </div> ); } } export default compose( connect( state => state.location, {loadManagers} ), checkoutA(), title('managers', 'title.base'), composeRestricted({user: true}) )(ManagersPage);
      
      





できた! . , . , “” . (, , ).





. , . , , , , . , ( ), .



All Articles