SPAのフロントエンド実装は、モデルの理解が重要

SPA実装とモデル

【告知】 10/14(土)新潟県上越市でIT勉強会を開催します!!

スポンサーリンク

どうも、イソップです。

ReactAngular、最近ではVue.jsが普及してきたおかげで、シングルページアプリケーション(以下SPA)の実装を多くの人ができるようになってきました。

宣言的なコンポーネント志向のアーキテクチャ、Fluxを用いたデータフロー。
コンポーネントの構築やデータの流れを意識することで、昔に比べて設計やデータ管理がラクになりました。

ただ注意したいのは、昔に比べてUIを実装しやすくなっただけで、アプリケーション実装のポイントを抑えておかないと次第にコードが散らかってしまうことです。
例えばReact + Reduxでは、始めは良くてもすぐにReducerやActionが膨れ上がります。
その結果コードの行数は増え、コードを追うのに時間がかかり、修正作業も困難。常にあれこれ考えながら実装を進めることになってしまっては元も子もありません。

そこで、重要になってくるのがモデルの概念です。
モデルを適用することで、データ管理をモデルに任せることができ、コードの見通しが良くなります。
今日はSPAにおけるフロントエンド実装とモデルの話をしたいと思います。

フロントエンドのモデルとは

始めにモデルに馴染みがない人もいると思うので簡単に説明をします。

モデルとは、サーバーサイドなどのMVCフレームワークにおける「Model(モデル)」「View(ビュー)」「Controller(コントローラー)」のモデル部分を指します。
Model View Controller – Wikipedia

モデルの役割は、アプリケーションで使用するデータの保持やビジネスロジックの処理(※注1)などが挙げられます。

※注1

ビジネスロジックは、データベース上のデータに対する処理手順といったようなものを指す、ソフトウェア工学的な用語である。

ビジネスロジック – Wikipedia

今回説明するフロントエンドのモデルは、MVCフレームワークのモデルと同じ役割のものを言います。
記事の冒頭で挙げた各ライブラリ・フレームワーク(React、Angular、Vue.js)では、テンプレートがMVCのビュー、ルーターがMVCのコントローラーと考えればわかりやすいでしょう。

しかし、それぞれのライブラリ・フレームワークにはモデルの機能がそれほど強力ではないのです。
ReactのstateやFluxのデータフローを持ち込んだだけでは複雑なアプリケーション実装には耐えられません。
(もしReact以外のライブラリでの解決策をご存じでしたら、教えて頂けると幸いです。)

昔人気があったBackbone.jsでは、MVCの概念取り入れられていて、Backbone.Modelというモデルの機能がありました。
これはAPI通信の機能はあれど、MVCのモデルそのままの機能を持ったものでした。(本来のモデルではAPIを叩きません)
当時は結構好きだったんですが、昨今の流れで行くとモデルは置いてけぼり感があります。

モデルを使うと何が嬉しいのか

ではモデルを利用することで、具体的にどんな利点があるのでしょうか。

モデルを使わない場合

引き続き、Reactを例に挙げます。
記事の冒頭でReact + Recuxのツライところとして、ReducerやActionが簡単に膨れ上がる、という話をしましたが、次の例を見てください。

APIから受け取るデータがあるとします。

APIレスポンス

const posts = [
  {
    "id": 1,
    "name": "foo"
    "description": "text1",
    "checked": false
  },
  {
    "id": 2,
    "name": "bar",
    "description": "text2",
    "checked": false
  },
  {
    "id": 3,
    "name": "baz",
    "description": "text3",
    "checked": false
  },
];

データを受け取って、テーブルで表示します。

コンポーネント(ビュー)

render() {
  return (
    <table>
      {this.props.posts.map((post, i) => (
        <tr key={i}>
          <td>{post.id}</td>
          <td>{post.name}</td>
          <td>{post.description}</td>
        </tr>
      ))}
    </table>
  ); 
}

データはactionとしてreducerに渡され、 switch 文でreduceします。

Reduxのreducer

const postsState = [];

function reducer(state = postsState, action) {
  switch (action.type) {
    // データの追加
    case ActionTypes.GET_POSTS:
      return Object.assign({}, state, {
        posts: state.concat(action.posts)
      });

    // データの削除
    case ActionTypes.REMOVE_POST:
      return Object.assign({}, state, {
        posts: state.filter((post, i) => i !== action.index)
      });

    // データの更新
    case ActionTypes.UPDATE_POST:
      return Object.assign({}, state, {
        posts: state.map(post => {
          if (post.id === action.data.id) {
            return action.data;
          }
          return post;
        });
      });

    // チェックボックスのトグル
    case ActionTypes.TOGGLE_CHECK:
      return Object.assign({}, state, {
        posts: state.map(post => {
          if (post.id === action.id) {
            post.checked = !post.checked;
          }
          return post;
        });
      });

    default:
      return state;
  }
}

reducer内のcase文でActionTypeを振り分けて、その中でビジネスロジックを定義しているのがお分かりでしょうか。
Reactではこれがツライのです。パッと見た感じ、コードは長く、見通しも悪いためストレスを感じると思います

case文ごとに見ていけばなんとかやっていることはわかりますが、reducer全体がごちゃごちゃしているので直感的にコードの内容がわかりません。
処理が増えればコードも増え、それと同時にストレスも増えます。
このまま開発を行っていくことで、開発効率は落ちてコード品質を維持するのが難しくなり、最終的に修正するのが苦しい技術的負債になりかねません。
また、丁寧にコメントを書いて内容がわかるようにしても、そもそもコード量が多いわけですから根本解決にはならないのです。
(注:コメントを書かないということではありません。自分や周りの人のためにコメントは極力書きましょう。)

モデルを使った場合

そこで、満を持してモデルの登場です。
先ほどのコードにモデルを利用してみます。

修正されたreducer

const postsState = new PostsModel();

function reducer(state = postsState, action) {
  switch (action.type) {
    // データの追加
    case ActionTypes.GET_POSTS:
      return state.add(action.posts);

    // データの削除
    case ActionTypes.REMOVE_POST:
      return state.remove(action.index);

    // データの更新
    case ActionTypes.UPDATE_POST:
      return state.update(action.data);

    // チェックボックスのトグル
    case ActionTypes.TOGGLE_CHECK:
      return state.toggleCheck(action.id);

    default:
      return state;
  }
}

配列データの操作処理(ビジネスロジック)がごっそり無くなって、スッキリしました。
actionで渡ってくるデータを、state(モデル)のメソッドへ受け渡すだけです。

Reducerはstoreの内容やactionで渡ってくるデータのことも考えないといけないため、全てを一度に考えることが複雑化する原因になります。
そこで、Reducerからデータ操作の役割を分離することで、モデルにデータの処理を丸投げすることができるのです。
Reducerでは、モデルのメソッドを実行するだけで済みます。

モデルを適用することで、いかにコードの見通しが良くなることがお分かりいただけたかと思います。

モデルの実体

モデルを利用した例では、stateの初期化時に postsState = new PostsModel() として、モデルを生成しています。
では、そのモデル内部を見てみましょう。

作成したPostsModel

import { Record } from 'immutable';

const PostsRecord = Record({
  posts: [], // 取得データ
});

class Posts extends PostsRecord {

  /**
   * データの追加
   * @param {array} data - 取得したデータ配列
   */
  add(data) {
    return this.set('entries', this.get('posts').concat(data));
  }

  /**
   * データの削除
   * @param {number} id - postのid
   */
  remove(id) {
    const _posts = this.get('posts').filter((post) => post.id !== id);
    return this.set('posts', _posts);
  }

  /**
   * データの更新
   * @param {object} data - postオブジェクト
   */
  update(data) {
    const _posts = this.get('posts').map((post) => {
      if (post.id === data.id) {
        return data;
      }
      return post;
    });
    return this.set('posts', _posts);
  }

  /**
   * チェックのトグル
   * @param {number} id - postのid
   */
  toggleCheck(id) {
    const _posts = this.get('posts').map((post) => {
      if (post.id === id) {
        post.checked = !post.checked;
      }
      return post;
    });
    return this.set('posts', _posts);
  }

}

export default Posts;

このモデルは、 Immutable.jsRecord を使用しています。

Immutable.js
Record – Immutable.js

Immutable.jsは、常にImmutableな値を返すライブラリです。
またその中の Immutable.Record はクラスとして機能するので、メソッドを定義することができます。
そこで、もともとReducer内で処理していたデータ操作をモデルへ移動しました。

そして、モデルの各メソッド実行時に自身を返すようにすることで、Reducerで特別な処理をしなくてもstoreの更新ができるようになります。(コードサンプルではわかりやすいように get, set を使っていますが、 update でも同じことができます)

データ処理はモデルに全て閉じ込めたほうが、実装をシンプルにすることができるわけです。
このようにモデルの考えかたが理解できると、データ処理の役割が明確に分離されているので頭の中がラクになりませんか?

また、モデルは自分でメソッドを定義できるので、イレギュラーなデータ取得やバリデーションなどもモデルに実装することで、よりシンプルな設計にすることが可能です。

モデルは自由にメソッドを定義できる

  /**
   * postsが空か
   * @return {boolean}
   */
  isEmpty() {
    return this.get('posts').length === 0;
  }

  /**
   * データの初期化
   */
  reset() {
    return this.set('posts', []);
  }

  /**
   * 入力値のバリデーションなども実装可能
   */
  validate() { ... }

ReactではImmutable.jsでモデルを実装するのが定石です。
今回はReactを例に紹介しましたが、AngularやVue.jsなど、どのライブラリ・フレームワークにも共通する概念ではないかと思います。

勘がいい方はお気づきかもしれませんが、何を隠そう、やっていることはサーバーのモデルと同じなわけです。

SPA実装ではMVCフレームワークを経験しておくと◎

今回はSPAのフロントエンド実装におけるモデルの例を紹介しました。
モデルの利便性を少しでも理解していただけたら幸いです。

日頃実装していてつくづく思いますが、SPA構築はライブラリ・フレームワークを利用したUI実装だけでは通用しないということです。

ぼくのオススメは、サーバーのMVCフレームワークを経験すること。
SPAはサーバーの負荷分散な面もあるので、MVCの理解は非常に強力で、フロントエンドの実装に必ず役立ちます。

個人的にはRailsがわかりやすく、手っ取り早く雰囲気を掴みたいのであれば、UdemyのRailsコースの受講をオススメします。

【短期間の効率的な学習に最適!】UdemyでRuby on RailsのWebアプリ開発を勉強してみた

2017.04.13

ぼくも実際に受講しましたが、心底やっておいてよかったと実感しています。
Railsのモデルを意識することで、フロントエンド実装の強力な武器になります。

そこまで時間無いよ、という方には、2014年の本でちょっと古いのですが、オライリーのSPA本にモデルの概念が書かれています。
UI実装はjQueryですが、モデルの他にもExpress・MongoDBでの構築方法も書かれているので、非サーバーサイドだったら必ず役に立つはずです。

ぜひモデルを理解してSPA実装に役立ててください。

SPA実装とモデル


イソップへのお悩み相談募集中

イソップに相談しませんか?

当ブログで紹介しているような、Web制作やフリーランスへの悩みをイソップに相談してみませんか?
回答できることがあれば記事の中でご紹介します。