Modular React + Reduxアプリケーションアーキテクチャ





ほとんどの開発者は、 Todoリストプロジェクトで Reduxの導入を開始します。 このアプリケーションの構造は次のとおりです。



actions/ todos.js components/ todos/ TodoItem.js ... constants/ actionTypes.js reducers/ todos.js index.js rootReducer.js
      
      





一見したところ、このコードの編成は、多くのバックエンドMVCフレームワークの標準的な規則に似ているため、論理的に見えます。



 app/ controllers/ models/ views/
      
      





実際、これはMVCとReact + Reduxの両方のアプリケーションにとって、次の理由から悪い選択です。



  1. アプリケーションの成長に伴い、コンポーネント、アクション、レデューサー間の関係を監視することは非常に困難になります
  2. アクションまたはコンポーネントを変更する可能性が高い場合、リデューサーを変更する必要があります。 ファイルの数が多い場合、IDEを上下にスクロールするのは便利ではありません
  3. このような構造は、レデューサーのコピー&ペーストを容認します


当然のことながら、多くの著者(1、2、3)は、「機能」( 機能別によってアプリケーションを構造化することを勧めています。



かなり前に、バックエンド開発でも同じ結論に達したので、フロントエンドでも同じことをします。 ロシア語では、機能の単位としての「フィーチャー」という単語に適した翻訳はありません。 代わりに、「モジュール」という言葉を使用します。 ES6では、「モジュール」という用語の意味は異なります。 あいまいな場合にそれらを混同しないようにするには、「アプリケーションモジュール」というフレーズを使用できます。 日常の作業では、これを除いて困難はありませんでしたが、「モジュール」という用語はよく理解されており、ビジネスユーザーとのコミュニケーションに適しています。



モジュラー構造

モジュールは、プログラムの機能的に完全な断片です。



モジュラープログラミングは、モジュールと呼ばれる小さな独立したブロックの集まりとしてのプログラムの編成であり、その構造と動作は特定のルールに従います。



私の理解では、モジュラーアプリケーションは次の要件を満たしている必要があります。



  1. すべてのモジュールコードは1つのフォルダーにあります。 プログラムからモジュールを完全に削除するには、対応するフォルダーを削除するだけです。 モジュールを削除しても、他のモジュールの機能には影響しませんが、アプリケーションから一部の機能が奪われます。
  2. モジュールは互いに依存していません。 モジュールを変更しても、他のモジュールの動作には影響しません。 モジュールはシステムの「コア」に依存できます。
  3. システムのコアには、UIを作成するためのI / Oおよびコンポーネントのセットをモジュールに提供するパブリックAPIが含まれています。


次のアプリケーション構造が得られます。



 app/ modules/ Module1/ … index.js Module2/ … index.js … index.js core/ … index.js routes.js store.js
      
      





エントリポイントで、 Root



コンポーネントが添付されたreact-hot-reload



に必要なAppContainer



ます。 Root



は、 redux



およびreact-router



との通信を提供するProviderのみが含まれ、 indexRoute



を使用してアプリケーションへのエントリポイントを定義します。 コンポーネントをnpmパッケージに入れて、任意のアプリケーションに接続できます。 インフラストラクチャを初期化するだけで、オブジェクトモデルのロジックは含まれません。



index.js



 import 'isomorphic-fetch' import './styles/app.sass' import React from 'react' import ReactDOM from 'react-dom' import { AppContainer } from 'react-hot-loader' import browserHistory from './core/history' import Root from './core/containers/Root' import store from './store'; import routes from './routes'; ReactDOM.render( <AppContainer> <Root store={store} history={browserHistory} routes={routes}/> </AppContainer>, document.getElementById('root'));
      
      





Root.js



 import React from 'react' import PropTypes from 'prop-types' import {Provider} from "react-redux" import {Router} from "react-router" const Root = props => ( <Provider store={props.store}> <Router history={props.history} routes={props.routes} /> </Provider>) Root.propTypes = { history: PropTypes.object.isRequired, routes: PropTypes.array.isRequired, store: PropTypes.object.isRequired } export default Root
      
      





これまでのところ、すべてが非常に簡単です。 モジュラーシステムを状態(ストア)に接続し、ルーティングを構成することは残ります。



defineModule



小さな関数を書きましょう:



 export const defineModule = ( title, path, component, reducer = (state = {}) => state, onEnter = null) => { return {title, path, component, reducer, onEnter} }
      
      





modules



フォルダーで、ユーザーアカウントにモジュールを作成します。



 modules/ Profile/ Profile.js index.js
      
      





プロフィール/ Profile.js



 import React from 'react' import PropTypes from 'prop-types' const Profile = props => (<h2>, {props.name}</h2>) Profile.propTypes = { name: PropTypes.string.isRequired } export default Profile
      
      





プロフィール/ index.js



 const SET_NAME = 'Profile/SetName' const reducer (state = {name: ''}, action) => { switch(action.type){ case SET_NAME: {…state, name: action.name} } } export default defineModule(' ', '/profile, Profile')
      
      





そして、モジュールをmodules / index.jsファイルに登録します



 import Profile from './Profile' export default { Profile }
      
      





このステップは回避できますが、わかりやすくするために、モジュール構造の手動初期化はそのままにします。 インポート/エクスポートの2行を書くことはそれほど難しくありません。


アクションタイトルの読みやすさを向上させるために、 CamelCase



/



を使用しています。 組み立てを簡単にするために、次の関数を使用できます。



 export const combineName = (...parts) => parts .filter(x => x && toLowerCamelCase(x) != DATA) .map(x => toUpperCamelCase(x)) .reduce((c,n) => c ? c + '/' + n : n) const Module = 'Profile' const SET_NAME = combineName(Module, 'SetName')
      
      





個人アカウントをルーターに接続し、モジュールをレイアウトに挿入することは残ります。 レイアウトを使用すると、すべてが簡単になります。 core/components/App.js



作成します。 重複を避けるために、ルーターと同じ配列がNavigation



コンポーネントに渡されることに注意してください。



 import React from 'react' import PropTypes from 'prop-types' import Navigation from './Navigation' const App = props => ( <div> <h1>{props.title}</h1> <Navigation routes={props.routes}/> {props.children} </div>) App.propTypes = { title: PropTypes.string.isRequired, routes: PropTypes.array.isRequired } export default App
      
      





ルーター



また、ルーターを使用すると、もう少し複雑になります。 一般に、複数のURLをモジュールに関連付けることができるはずです。 たとえば、 /profile



/profile



に関する基本情報が含まれ、 /profile/transactions



にはユーザートランザクションのリストが含まれます。 アカウントのユーザー名を常に表示し、下に「一般情報」と「トランザクション」の2つのタブでコンポーネントを表示するとします。



次に、ルートの論理構造は次のようになります。



  <Router> <Route path="/profile" component={Profile}> <Route path="/info" component={Info}/> <Route path="/transactions" component={Transaction}/> </ Route > </Router>
      
      





Profile



コンポーネントにはユーザー名とタブが表示され、 Info



Transactions



はそれぞれプロファイルの詳細とトランザクションのリストが表示されます。 ただし、モジュールのコンポーネントが追加のグループ化モジュールを必要としない場合もオプションをサポートする必要があります(たとえば、注文リストと注文表示ウィンドウは独立したページです)。



協定を紹介します



モジュールから、 defineModule



関数から返された構造を持つオブジェクト、またはそのようなオブジェクトの配列をエクスポートできます。 すべてのコンポーネントは、追加のネストなしでルートのリストに追加されます。



モジュールには、 modules/index.js



類似した構造をmodules/index.js



キーが含まれる場合がmodules/index.js



。 この場合、そのうちの1つをIndex



と呼ぶ必要がありIndex



IndexRoute



として使用されます。 次に、「個人アカウント」に対応する構造を取得します。



リストのモノイドの性質を使用し、配列またはオブジェクトをエクスポートする機能を考慮して、モジュールのフラットな配列を取得します。



 export const flatModules = modules => Object.keys(modules) .map(x => { const res = Array.isArray(modules[x]) ? modules[x] : [modules[x]] res.forEach(y => y[MODULE] = x) return res }) .reduce((c,n) => c.concat(n))
      
      





ルーターでは、 Route



コンポーネントだけでなく、使用する通常のオブジェクトを含む配列だけを転送できます。



 export const getRoutes = (modules, store, App, Home, title = '') => [ { path: '/', title: title, component: App, indexRoute: { component: Home }, childRoutes: flatModules(modules) .map(x => { if (!x.component) { throw new Error('Component for module ' + x + ' is not defined') } const route = { path: x.path, title: x.title, component: x.component, onEnter: x.onEnter ? routeParams => { x.onEnter(routeParams, store.dispatch) } : null } if(x.children){ if(!x.children.Index || !typeof(x.children.Index.component)){ throw new Error('Component for index route of "' + x.title + '" is not defined') } route.indexRoute = { component: x.children.Index.component } route.childRoutes = Object.keys(x.children).map(y => { const cm = x.children[y] if (!cm.component) { throw new Error('Component for module ' + x + '/' + y + ' is not defined') } return { path: x.path + cm.path, title: cm.title, component: cm.component, onEnter: cm.onEnter ? routeParams => { cm.onEnter(routeParams, store.dispatch) } : null } }) } return route }) } ]
      
      





したがって、 modules/index.js



モジュールを追加すると、新しいルートが自動的に初期化されます。 開発者がルートの宣言を忘れたり、合意に混乱した場合、コンソールに明確なエラーメッセージが表示されます。



onEnter



モジュールはonEnter関数もエクスポートできることに注意してください。 対応するルートへの移行時に、パスパラメータとstore.dispatch関数が渡されます。 これにより、 componentDidMountを使用してコンポーネントを初期化する必要がなくなります。 代わりに、ストアでイベントをスローできます(または、私のようにredux-sagaをスローしてredux-thunkを終了することにした場合はPromise)、reducerで処理し、状態を変更して、コンポーネントを再描画します。



減速機をストアに接続する

DevToolsとサンクが必要になります。 ストアを初期化する小さな関数を宣言します。



 const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const createAppStore = (reducer, ...middleware) => { middleware.push(thunk) const store = createStore( reducer, composeEnhancers(applyMiddleware(...middleware))) return store } export default createAppStore
      
      





そして、すべてのモジュールのすべてのレデューサーの受け取りとレイアウトのためにもう1つ:



 export const combineModuleReducers = modules => { const reducers = {} const flat = flatModules(modules) for (let i = 0; i < flat.length; i++) { const red = flat[i].reducer if (typeof(red) !== 'function') { throw new Error('Module ' + i + ' does not define reducer!') } reducers[flat[i][MODULE]] = red if(typeof(flat[i].children) === 'object'){ for(let j in flat[i].children){ if(typeof(flat[i].children[j].reducer) !== 'function'){ throw new Error('Module ' + j + ' does not define reducer!') } reducers[j] = flat[i].children[j].reducer } } } return reducers }
      
      





レデューサーを含まないモジュールをスキップし、例外に該当しないようにすることで、より厳格で簡単にすることができますが、より厳密なアプローチが好きです。 モジュールにロジックがまったく含まれていない場合は、単にコンポーネントとして設計し、手動でルーターに追加する方が簡単です。



store.jsファイル内のすべてを結合します



 export default createAppStore(combineReducers(combineModuleReducers(modules)))
      
      







現在、各モジュールは、 modules/index.js



内のキーに一致する状態の一部に対応していmodules/index.js



。 個人アカウントの場合、 Profile



になります

モジュラーアプリケーションの構造については以上です。 「コア」の構成とモジュールへのパブリックAPIの提供は、別の記事のトピックです。



All Articles