JavaScript
Rails4

標準のRails4+jQueryでフロントエンドのアプリケーション設計考えてみました

More than 1 year has passed since last update.

はじめに

Railsの仕組みを通じてJSONを取得して、画面に結果を反映するような処理の場合にガイドライン・設計方針が無いと以下の様なコードになりがちです

hostname = window.location.hostname
protocol = window.location.protocol
port =  window.location.port
rootURL = "#{protocol}//#{hostname}:#{port}"

$('#tasks-list').on 'click', ->
  params = {
    url: this.rootURL + '/tasks.json',
    method: 'GET'
  }
  $.ajax(params)
    .done (items) ->
      elements = []
      items.forEach( (item, index) ->
        elements.push(
          "<tr><td>#{item.title}</td>" + 
          "<td><strong data-task-id=#{item.id} class='task__detail'>" + 
          "詳細表示</strong></td></tr>)"
      )
      $('#tasks').append(elements)
      $('#tasks').show()

JavaScriptの処理が1つしかないなら上記のような書き方でも問題ないと思うのですが実際の開発の場面だと

  • Rails側と連携する処理はそれなりに数が増えてきて、$.ajaxな処理が色々な箇所に登場する
  • 仕様変更などで表示結果を修正する

というのがそれなりに発生しその場しのぎの対応をしていくことでだんだんと大変なことになっていくかと思います。

JavaScriptによるアプリケーションの設計で重要なのはこの二つ

ModelとViewを明確に分ける
Viewを疎結合にする
フロントエンドJavaScriptにおける設計とテスト
より引用

というスライドでもアプリケーションの設計で重要な事について言及されていますが、こういう点を意識した設計をしておくことで機能追加・改修の作業がやりやすくなるのでその辺りの情報まとめておきます

想定してる読者イメージ

  • Railsはそれなりに経験積んできたけどJavaScriptはほとんど書く機会が無いような人をイメージしてまとめてます
  • 最近流行りのフレームワークやライブラリはチーム内のノウハウがないため当面はjQueryベースで実装をしていく割合が多そうな人

目指す実装イメージ

デザインパターンの1つであるPublish/Subscribeパターン(observerパターンの別名)を意識した設計にしてます。

rails-js-01.png

サンプルアプリのコードはGitHub上にあります
https://github.com/h5y1m141/todo_rails

事前準備

適切な責務を意識しながらいくつかのファイルに分割して実装していきますが、素のRailsアプリケーションの状態で開発する時にいくつか事前に準備して置いたほうが良いことがあるのでそれについて説明します

名前空間の定義

CoffeeScript で名前空間を定義を参考にしてnamespace.coffeeというファイルを作成して以下内容を記述しました

@Todo = (fn) ->
  klass = fn()
  @Todo[klass.name] = klass

Publish/Subscribeパターンを利用するためにライブラリ配置

Publish/Subscribeパターンで実装してるというのを伝えやすくするために、JavaScriptデザインパターンでも紹介されてるjQuery Tiny Pub/Subを利用します。

コード見てもらうとわかりますが、jQueryのon/off/triggerをsubscribe/unsubscribe/publishという名前で利用できるようにしてる簡易なラッパーになってます。

JavaScriptの読み込み順を考慮するようにapplication.jsを修正

上記で名前空間を定義したりPublish/Subscribeパターンを利用するためにライブラリ配置してる関係で読み込みの順番を考慮しておく必要があります。

app/assets/javascripts/配下のディレクトリ・ファイルを意図した順番に読み込ませるためにapplication.jsを以下のように修正します。

//= require jquery
//= require jquery_ujs
//= require tinyPubSub
//= require namespace
//= require_directory ./validators
//= require_directory ./models
//= require_directory ./views
//= require_directory ./controllers
//= require main

実際の処理内容の解説

これで事前準備は完了したので、先ほどのコードを意味のある単位で分割していきます

Railsと連携する$ajax処理を切り出す

ここが一番手を付けやすい&理解しやすい箇所かと思うのでまずはここの解説を行います。

models/taskModel.coffeeを作成して以下内容を記述します。

@Todo -> class TaskModel
  constructor: () ->
    hostname = window.location.hostname
    protocol = window.location.protocol
    port =  window.location.port
    this.rootURL = "#{protocol}//#{hostname}:#{port}"
  index: () ->
    params = {
      url: this.rootURL + '/tasks.json',
      method: 'GET'
    }
    @_request(params)
      .done((response) =>
        $.publish('tasks.loaded', [response]);
      )
  _request: (params) ->
    return $.ajax(params)

ポイントは以下2つかと思います。

  • Rails側と連携することを意識してるのでそれぞれのアクションに対応するメソッドを作成
    • 個々のメソッドはパラメーターの組み立てを行い、最終的には_requestを通じて処理を行う
  • ajaxの返り値を得たら$.publish()を通じてtasks.loadedというイベントの発行を行う
    • tasks.loadedという名前のイベントを購読してる側でその通知を受取必要な処理が行われる

Viewの処理

従来のコードdoneの中で処理されていたViewの処理は以下の様な状態でした

$.ajax(params)
    .done (items) ->
      items.forEach( (item, index) ->
        elements.push(# 省略)
        $('#tasks').append(elements)
        $('#tasks').show()

このViewの実装は以下のような方針で実装することで、比較的処理内容が明白な状態になるかと思います

  • 該当するイベントを購読しておく
    • 具体的にはModelで定義したtasks.loaded
  • 該当するイベントが発行されたらそれを検知して表示が変わる
    • Model変更→Viewの描画という状態がこれで行われる

コードは以下のようになります。

@Todo -> class TasksView
  constructor: () ->
    @$tasksView = $('#tasks')
    $.subscribe 'tasks.loaded', (event, items) =>
      @show(items)
  show: (items) ->
    elements = []
    items.forEach( (item, index) ->
      elements.push("<tr><td>#{item.title}</td>" + "<td><strong data-task-id=#{item.id} class='task__detail'>詳細表示</strong></td></tr>")
    )
    @$tasksView.append(elements)
    @$tasksView.show()
  hide: () ->
    @$tasksView.hide()

イベントリスナーのバインディング処理

app/views/tasks/new.html.erbが以下のようになってるとします。

<h1>New Task</h1>
<button id="tasks-list">タスクの確認</button>
<ul id="tasks">
</ul>
<%= render 'form' %>

タスクの確認ボタンをクリックしてRails側のエンドポイントを通じてタスク一覧が表示されるようにしたいのですが、そのためにtasks-listのID属性に対してイベントリスナーを設定する必要があります

assets/javascripts/controllers/taskController.coffeeというファイルを作って以下のようにします。

@Todo -> class TaskController
  constructor: () ->
    @tasksView = new Todo.TasksView()
    @model = new Todo.TaskModel()
    @bindEvent()

  bindEvent: () ->
    $('#tasks-list').on 'click', =>
      @model.index()

上記のようにすることでViewの描画が行われます。

  1. $('#tasks-list')クリック
  2. TaskModelのインスタンスメソッドを通じてRails側から必要な情報を修得
  3. 情報取得した後にModelのindexメソッド内でtasks.loadedイベントがpublishされる
  4. TaskController内であらかじめTodo.TasksView()を初期化&そのViewの中でtasks.loadedイベントが購読されるようにしてあるのでViewの描画が行われる

この実装をベースに拡張したい場合

例えば

  • フォームのtitle入力時に文字列の長さをチェックしたい
  • 折角なのでシングルページアプリケーションのような画面にしたい
    • タスク一覧だけではなく詳細表示もしたい
    • 画面遷移せずにタスクの作成もしたい

という要望があったとします。

頑張って絵にまとめてみました

rails-js-02.png

  • Modelに必要なメソッドを追加
  • 要素単位でViewを生成し必要なイベントを購読しておく
    • 例えばフラッシュメッセージを表示する役割のFlashViewみたいな場合には複数のイベントを購読するようにしておけば割りと使い回ししやすくなるかも
  • イベントリスナーのバインディングを適宜行う

という作業を行うことである程度拡張性&見通しの良いコードになるのかなと思います。

こういう形の実装にしておくことで良さそうなこと

実は最近感じていたこととして、JavaScriptに苦手意識がある場合にその人がJavaScriptの世界をどう見てるのかというのは結構重要なのかと思ってます。

経験豊富な人が多いチームならそれほど気にしなくても良いのかもしれませんが、フロントエンドの設計としてどういう形が良いのか模索してるチームなどで「最近流行りのライブラリ・フレームワークを使いたい」となっても個々のJavaScriptの世界観にバラツキがあったり、そういうのを利用する上で知っておいたほうが良い概念が欠けていたりすると、かなり大変なのかなと思ってます。

今回紹介したようなPublish/Subscribeパターン(Observerパターン)を使ったサンプルアプリが何かの役に立てればと思ってます。