技術選定
私たちのプロジェクトは、主に以下の技術といくつかの補助ツールを使用して開発されています。
- react
- react-router
- redux
- react-redux
開発およびオンライン環境
開発環境
プロジェクトはフロントエンドとバックエンドが分離されているため、以下の機能を含む完全な開発環境が必要です。
- データモック
- Webpackによるコンパイルとホットリロード
- フロントエンドとバックエンドの連携デバッグの容易さ
これらの要件に基づき、Express、Webpack、Webpack-dev-middleware を使用して、この完全な開発環境を構築しました。
ご覧の通り、ブラウザからのすべてのリクエストはローカルのNode.jsサービスによってインターセプトされます。静的リソースのリクエストは webpack-dev-middleware
に委譲され、APIリクエストは異なる環境に応じて処理が決定されます。
ローカル開発
ENV = 'development'
の場合、つまり開発環境では、ローカルのモックデータを直接読み込んでページをレンダリングします。
フロントエンドとバックエンドの連携デバッグ
ENV = 'api'
の場合、つまり連携デバッグ環境では、APIリクエストはNode.jsによって連携デバッグが必要な実際のバックエンドサービスアドレスに転送され、直接呼び出しによって発生するクロスオリジン問題を回避します。
これにより、ローカルの開発コードで直接バックエンドと連携デバッグを行うことができ、効率が大幅に向上し、毎回サーバーにビルドしてデプロイする手間が省けます。
オンライン環境
フロントエンドとバックエンドは別々にデプロイされており、すべての静的リソースはCDN (example.cdn.com)上に配置されています。
つまり、私たちのページは example.cdn/index.html
にありますが、リクエストされるAPIは example.163.com/api/xxx
にあります。ユーザーに直接 example.cdn.com/index.html
にアクセスさせることは不合理であり、クロスオリジン問題も存在します。
では、example.dai.163.com
にアクセスした際に、どのようにしてHTMLページを取得するのでしょうか?
以下の図をご覧ください。
クライアントとバックエンドサービス間にNginxを設置します。example.dai.163.com
へのアクセスには2種類のリクエストがあります。
- HTMLページリソース
- APIリクエスト
これらの2種類のリクエストはどちらもまずNginxを経由し、ここで判断が行われます。ページリクエストであればNginxがCDNに転送し、そうでなければバックエンドサービスがAPIリクエストに応答します。
ページを取得した後、その他のすべてのCSS、JSなどの静的リソースは直接CDNにリクエストされます。ここについては特に説明することはありません。
データフロー
Reduxを活用してデータフローを管理します。この図をご覧ください。
まず、middleware
と reducer
を通じて store
が生成され、プロジェクトの初期 state
が取得されます。この初期 state
を使用してページの初期状態がレンダリングされます。
Home
ページを例にとると、まず Home
は react-redux
が提供する connect
メソッドを通じて初期 state
を取得し、それを Home
の prop
として Home
に渡します。Home
は複数の異なる子コンポーネントで構成されており、これらのコンポーネントが必要とするデータは、さらに Home
から props
を介して自身の子コンポーネントに渡されます。
Home
の初期状態のロードが完了した後、ユーザーデータを取得するためにバックエンドにリクエストを送信する必要があります。このとき、以下の形式の action
をディスパッチします。
{
types: ['home/start','home/success','home/failure'],
payload: {
api:
...
},
meta: {
isApi: true
}
}
すべての action
は、私たちが定めた順序で一つ一つの middleware
を通過します。
ここで、私たちの action
は callApiMiddleware
によって meta
内の isApi
フラグで識別され、それに応じた処理が行われます。
例えば、このミドルウェアでは、実際のAPIリクエストを行い、リクエストの成功または失敗時に対応する action
をディスパッチし、統一されたビジネスロジックも処理します。例えば、バックエンドから返されるAPIの code
値について統一された規約があり、1が成功、2が失敗、3が未ログインであると仮定します。そうすれば、これらのビジネスロジックをミドルウェア内で処理できます。
リクエストが成功し、ページがレンダリングされた後、ユーザーがボタンをクリックしたとします。このボタンは、例えば写真撮影のような native
の機能を呼び出す必要があります。このとき、写真撮影機能を呼び出す camera/start
という action
をディスパッチします。
{
types: ['sdk/start','sdk/success','sdk/failure'],
payload: {
command:
...
},
meta: {
isSDK: true
}
}
同様に、この action
は EpaySDKMiddleware
によって認識され処理されます。ネイティブ機能を呼び出す際、セキュリティを確保するために、署名を取得するためのリクエストをバックエンドに送信する必要があります。このとき、EpaySDKMiddleware
内でAPIリクエストの action
をディスパッチできます。この action
もすべての middleware
を通過する必要があります。すると、このAPIリクエストの action
は、上記のフローと同様に callApiMiddleware
を通じて処理されます。
ミドルウェアの存在により、全体のフローが非常に明確になります。APIリクエストのミドルウェアはAPIリクエストのみを行い、ネイティブAPI呼び出しのミドルウェアはネイティブ呼び出しのみを行います。ネイティブAPI呼び出しがバックエンドAPIリクエストを必要とする場合、APIリクエストのミドルウェアを通過する action
をディスパッチします。
各ミドルウェアは自身の役割にのみ集中するため、その後のメンテナンスが容易になり、優れた拡張方法も提供します。View
層が行う必要があるのは、action
をディスパッチし、データを受け取ってページをレンダリングすることだけであり、他のロジックについて心配する必要はありません。
例
以下のようなルーティング設定があると仮定します。
{
component: App,
path: '/',
onEnter: initLogin(store),
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], require => {
cb(null, require('../views/Home').default)
}, 'Home')
},
onEnter: initHome(store)
},
childRoutes: [
createActivateRoute(store),
{
path: 'test',
indexRoute: {
getComponent(nextState, cb) {
require.ensure([], require => {
cb(null, require('../views/Test').default)
}, 'Test')
}
}
},
...
]
}
では、react-router
と組み合わせて完全なフローを見てみましょう。ブラウザに example.dai.163.com/index.html/#/
と入力したときです。
まず、冒頭の「オンライン環境」セクションで述べた内容を通じて、ページに必要なHTML、CSS、JSを取得します。
次に、Provide
コンポーネントと Router
コンポーネントをレンダリングし、それぞれ store
の注入とルーティングの制御を提供します。
このとき、ルートパスのルーティングマッチがトリガーされ、ルートコンポーネント APP
がロードされます。その後、ルーティングマッチルールに基づいて IndexRouter
がマッチし、Home
コンポーネントがロードされます。
その後の処理は、前述の「データフロー」セクションで説明した内容と同じです。
まとめ
フロントエンドとバックエンドが完全に分離されていることを前提に、完全な開発環境を活用することで、開発効率を大幅に向上させ、フロントエンドとバックエンドの連携デバッグのコストを削減できます。
同時に、Reduxの思想を活用して単方向データフローを実現することで、非常に明確なデータフローを構築できます。さらに、ミドルウェアを活用することで、データフローのプロセスをより効果的に制御できます。これにより、将来のプロジェクト拡張に無限の可能性を提供します。
この記事は 2017年5月12日 に公開され、2017年5月12日 に最終更新されました。3068 日が経過しており、内容が古くなっている可能性があります。