ウーバーイーツのようなアプリケーションを作るプロジェクト
トップページには店舗一覧がある。この中から1つの店舗をクリックすると、その店舗が持っている商品一覧ページへと遷移。
商品一覧画面の中にはまず、商品が一覧で並んでいる。その中の1つ、ハンバーガーをクリックするとまず仮注文のモーダルが表示される。
このときその数量を選択できるようにすることを忘れない。そうしないと、ハンバーガーを3つ頼みたい時に、3回クリックするという面倒なUX(ユーザー体験)になってしまう。
つまり、仮注文モーダルの中では特定の商品をいくつ頼むか?ということを選択できるようにするUI(ユーザーインターフェース)が必要。
最後に、仮注文が確定したらそれらをまとめて確認し、また問題なければそのまま注文確定することができる画面を用意。もし仮注文が複数(上記の図でいえばハンバーガーとポテトが1つずつ)あるような場合には、それらの合計価格とさらに店舗の配送手数料が加算されます。そして最終的な注文内容に誤りがなければ、注文確定をするという流れになる。
店舗Aで仮注文を行ったあとも、店舗一覧画面に遷移することは可能である。そしてその中で店舗Bを選択し、店舗Bの商品一覧を選ぶこともできる。その場合に、店舗Aの商品の仮注文と店舗Bのそれが併存することを許容するかどうか?はとくにサーバーサイドの処理のなかでとても重要なビジネスロジックの一つになる。
ここでは店舗Aで仮注文がある状態で、別の店舗Bでさらに仮注文をしようとすると新規注文のモーダルが表示された。つまり、仮注文は同時に1つの店舗しか存在しえないという仕様のようである。
ここで、「新規注文」ボタンを押すと、店舗Aの仮注文は削除され、店舗Bで仮注文した内容が注文画面に表示されました。
本プロジェクトでもこの仕様を踏襲し、仮注文をPOSTする際には、別店舗の仮注文がないか?をチェックしたうえでAPIの挙動を変えるようにする。
今回はあくまでRailsとReactを使ったSPAの開発が目的のため、以下を前提として実装範囲を決める。
- 複雑なサーバーサイドの処理は書かない
- モデルの定義は最低限の動きにのみ対応する
- ページ遷移は全てReact Routerをつかったルーティング
- RailsではViewを用意せず、React側で画面を作っていく
- Reactの例外処理は最低限
今回はサーバーサイドから実装していく。つまり、Ruby on Rails側から作っていく。しかもAPIコントローラーを1つ作って画面を作って、とサーバー・フロントを行き来しない。サーバーサイドを全て作り終えてから、フロントエンドに移る。途中でコントローラーの確認は挟む。
こうすることでRailsとReactで頭の使い方をいちいち切り替えなくて良い。
サーバーサイド
- 最低限の動きに必要なデータのMigrationとModelの定義
- データを取得するためのAPI Controllerの実装
- APIのなかで例外パターンに一致する場合にはエラーを返却すること
- サーバーサイドはこれらがあれば、データを用意し、それを返却するだけというシンプルなAPIサーバーをつくることができます。もちろん後から複雑なロジックを追加することもできますし、別のデータが必要になった場合にも同様に拡充することができます。
フロントエンド
- ルーティング(画面遷移)はReactで行う
- レイアウトはなるべくUber eatsぽくするものの、細かい部分は省略する
本家Uber eatsとまったく同じレイアウトを目指すとCSSや画像などが大量に必要になる。今回はあくまでSPAの開発が主目的なので、最低限のレイアウトのみに留める。
サーバーサイド
- データを投入するAdmin画面(システム管理者用画面)
- データごとのユニークな画像
- テスト
- ユーザー登録/ログインの処理
- 本番デプロイ
今回はローカル開発環境でのみ動くものを目指す。本番にデプロイして動かすこともできるが、その場合に考慮しなければいけないことがたくさんあるため、実装スコープからは除外。
さらに、商品データ1つ1つにユニークな画像(商品Aは画像A、商品Bは画像B...)を用意せず、フロントエンドで共通の画像を表示させるようにする。プロジェクトとしてユニークな画像を用意しない、というだけでありUIを拡張するなかで商品ごとの画像を表示させるということも十分に可能。
フロントエンド
- エラーの場合の例外処理
- テスト
- HTML/CSS
SPAの場合、データはおもにAPIから非同期で取得。つまり、画面表示時には問題ないのに、あとからAPI側でデータが不整合だったり、リクエストに失敗することでエラーになるというケースである。この場合にReact側ではエラー文章を画面に表示したり、画面遷移をさせたりアウルが、今回はそうしたエッジケース(例外処理)は考慮しない。
アプリケーションの要件は決まったので次に、実装の手順を確認する。
今回はサーバーサイド/フロントエンドの2つをそれぞれ実装するが、その順番としては、まず初めにサーバーサイドを全て実装し、その後フロントエンド、という順番で進める。
なぜこの順番で作るか、というとサーバー/フロントのスイッチングコスト(作業切替にかかる手間)を省くためである。例えば、1ページごとにサーバー/フロントを一緒に作るという方法もあるが、そうすると都度作業プロジェクトを切り替える必要がでてくる。
また、ページごとに個別最適なコードになりがちで、コード全体の設計が"がたつく"ことがある。
ということで、今回はサーバーサイド単体をまずは完成させて、その後それに沿ってフロントの実装を進めていくことにする。
店舗の情報
- 店舗のID
- 店舗名
- 配送手数料
- 配送にかかる時間
商品の情報
- 商品名
- 商品価格
- 商品の説明文章
店舗1つにつき複数の商品データが紐づく1:n の関係。
続いて、店舗や商品などの目に見えるもの以外で必要なデータを定義する。ここでは、仮注文データと注文データの2つである。ここで図解をみてみる。
仮注文データはあくまで商品と1:1の関係にあり、その商品がいくつか?という情報しか持たない。一方、注文データは商品の個数は気にせず、紐づく仮注文の合計情報だけを保持する。ここで仮注文データと注文データはn:1の関係とする。
仮注文の情報
- 店舗ID
- 商品ID
- 商品の個数
- active/not activeの状態
- 注文ID
仮注文データは頻繁に作成されたり、消されたり、また注文が確定するとDisabledになるようにする。そのため、物理削除ではなく、論理削除できるようにactiveかどうか?というboolean型のフラグをもたせる。
こうすることで、本注文後に仮注文はすでに役目を終えたとしても、注文データから仮注文データを参照することができるようになる。
一般的にデータベースに作成されたレコードは論理削除(booleanなどで活性 -> 非活性に変更すること)できる設計が多い。例えばSNSのユーザーデータなどを考えても、退会したら削除するのではなく、論理削除できるようにすることで、退会したユーザーがどんな情報を持っていたか?を追えるようになる。
注文の情報
- 店舗ID
- 合計金額
注文自体はどの店舗に対して、いくらの金額を払うか?ということのみを保持するようにする。
続いては決まったデータをもとに、画面とコンポーネントの設計をしていく。特に画面に対応するコンポーネントはデータを扱うことから、最初の設計を誤ると保守性の低いフロントエンドのコードになってしまう。
ちなみに、ここでいうコンポーネントとはUI(ユーザーインターフェース)のパーツ・部品のようなもので、ボタンや画像などもコンポーネントの一部である。このようなコンポーネントを幾重にも重ねて複雑なページを構成していく。
まず初めに主にReactでよく推奨されるコンポーネントの種類分けについて。 まず1つ目が「データを扱うコンポーネント」であるContainer Componentというもの。 Container Componentはデータをもち、また親コンポーネントとして複数の子コンポーネントにデータを渡す役割を担当。
一方、「受け取ったデータをただ表示するだけ」のPresentational Componentには例えばボタンやモーダルなどが含まれる。
ではなぜこの2種類に分けるべきなのか?いくつかメリットが挙げられるが、一番大きいのは子コンポーネントの再利用性を高めるということである。Presentational Componentは子コンポーネントとしてUIを提供することだけに関心を持つ。そのため、どのようなデータが入ってくるか?によってその振る舞いを変えることができる。
もし子コンポーネントにデータを持たせてしまうと、複数ページで同じようなUIなのにデータが異なるという場合に再利用できず、結局コピペするということになってしまう。これが局所的なら問題ないが、ボタンやモーダルなど頻繁に利用されるものであれば尚更データを持たせるべきではない。
本教材のコンポーネント設計
小さめのアプリケーションであれば分ける必要もなかったりするが、今回は練習として明示的にContainer層/Component層をディレクトリから分けていく。また、1ページに1Containerとして以下のようなContainerを作成。
containers/Restaurants.jsx
containers/Foods.jsx
containers/Orders.jsx
これらは各ページの最も上部に存在するContainer層で、データを扱う。 これらの中にボタンや画像など子コンポーネントがレイアウトのみを提供する。その子コンポーネントは以下のディレクトリに置いていく。
components/Buttons/HogeButton.jsx
components/Modals/FugaModal.jsx
components/Wrapper.jsx
ボタンやモーダルなど複数種類存在し得るものについてはcomponents/Buttonsと1つ階層を掘っていくことで、コンポーネントを管理しやすくする。
続いて、フロントエンドからAPIを叩く部分を考える。一般的にファイルごとに関心は分離すべきです。そのため、Reactのコンポーネントはあくまでレイアウトやデータを保持することにする。その場合に、各種APIを呼ぶ部分は関数化して他のファイルに分けることで関心を分離することができます。
コンポーネントファイルの中にAPIを叩く関数を定義してしまうと、例えば他のページから同じAPIを叩きたいという場合に参照できずに、これもまたコピペする羽目になってしまう。それを避けるためにコンポーネントファイル/API関数ファイルを分ける。
このようなディレクトリで定義。
apis/restaurants.js
apis/foods.js
apis/orders.js
本教材ではReact Hooksを使います。世間一般ではreduxなどのデータライブラリを用いる場合もありるが、今回はReactの組み込みAPIであるReact Hooksでシンプルにデータを保持できるようにする。
React HooksのなかでもuseReducerはカプセル化の手段として最適。そのreducerもコンポーネント内で定義せず、外部ファイルに閉じ込めてしまう。そうすることで、複数コンポーネントで同じような状態を持ちたい場合に便利である。
reducers/restaurants.js
reducers/foods.js
reducers/orders.js
このようにreducersディレクトリを掘って、各種リソースごとのreducerファイルを作成します。
最後に、APIのURL管理も別ファイルに分けてしまう。これはRailsでURLがroutes.rb
にまとまっているように、フロントエンドでも「ここを参照すれば使われているURLが分かる」という状態を明確にするためである。少し冗長であるが、アプリケーションが大きくなるにつれて、呼ばれるAPIの数も増えていき、管理がしにくくなるという状態を少しでも防ぐために早めに作っておくと良い。
ここではurls/index.js
を作成して、ここにAPIのURL文字列を定義していく。
以上