Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

シングルページアプリを捨ててTurbolinksを使う?Rails開発者のためのPJAX活用ガイド

Turbolinks――おそらくRails界で最も忌み嫌われている言葉のひとつでしょう。

試したことがある方も多いはずです。新規プロジェクトや既存アプリにTurbolinksを組み込んだ途端、アプリは奇妙で不可解なバグを連発するようになりました。幸い、解決策は簡単でした。Turbolinksをオフにすればよかったのです。

……しかし、上手に使いこなしている企業も存在します。Honeybadgerでも実際に使いこなしています。そして、私たちは天才ではありません。

その答えはあまりにシンプルなので、あえて取り上げるのを躊躇するほどです。とはいえ、Ruby NationやMadison+Rubyでこのテーマについて講演したところ、多くの方が役立つと感じてくれました。それでは詳しく見ていきましょう。

シングルページアプリを捨ててTurbolinksを使う?Rails開発者のためのPJAX活用ガイド

TurbolinksとPJAXは、本質的にはほぼ同じ仕組みで動作します。あまりに似ているため、以降は単に「PJAX」と呼ぶことにします。

PJAXは「2つのページリクエスト」で理解できます。ユーザーが最初にページへアクセスしたときは、通常の「従来型」Railsページとまったく同じように表示されます。ところが、PJAX対応のリンクをクリックすると特別なことが起こります。ページ全体を再読み込みする代わりに、ページの一部だけが更新されるのです。これはAJAX経由で実現されます。

この仕組みにより、シングルページアプリ(SPA)の利点を多く享受しながら、SPA特有の難しさを回避できます。

  • リクエストごとにページ全体を再読み込みする必要がないため、SPAと同等のキビキビした操作性を実現できる
  • フロントエンドとバックエンドで同じ技術スタックを使える
  • ユーザーがJavaScriptを無効にしていても、デフォルトでグレースフルにフォールバックする
  • アクセシビリティやSEO対応もしやすい

PJAXの面倒な処理を担ってくれるライブラリは数多くありますが、中でも有名なのがTurbolinksです。セットアップは、Gemfileにgemを追加するだけ。

gem 'turbolinks'

続いて、app/assets/javascripts/application.jsにJSを読み込みます。

//= require turbolinks

これで完了です。アプリを再読み込みすると、すべての内部リンクがTurbolinkになります。リンクをクリックすると、新しいページがAJAX経由で取得され、現在のドキュメントに差し込まれます。

jquery-pjaxの実装

Honeybadgerでは、GitHubが開発したPJAXライブラリを使用しています。Turbolinksより設定の手間は少し増えますが、その分柔軟性も高いのが特徴です。

Turbolinksと違い、すべてのリンクが自動的にPJAX化されるわけではなく、どのリンクをPJAX対応にするかを自分で制御できます。さらに、PJAXコンテンツをページのどの部分に挿入するかも指定可能です。

まず、HTMLにコンテナを追加します。

<div class="container" id="pjax-container">
  <a href="/page/2">次のページ</a>へ移動。
</div>

次に、PJAXリンクを設定します。

$(document).pjax('a', '#pjax-container')

最後に、PJAXリクエストの場合はRails側でレイアウトを描画しないよう指定します。これを忘れるとヘッダーやフッターが二重に表示され、サイトがまるで映画『インセプション』のように入れ子状態になってしまいます。

def index
  if request.headers['X-PJAX']
    render :layout => false
  end
end

実は、そんなに簡単ではない!

ここまで話してきた内容より、実際はもう少し複雑です。最大の原因は、ほとんど語られることのないある巨大な落とし穴です。

ページ遷移のたびにDOMがクリアされないということは、従来のRailsアプリでは正常に動いていたJavaScriptが、突如として奇妙な形で壊れる可能性があるということです。

その理由は、私たちの多くが「意図せぬ名前の衝突」を引き起こしやすい書き方でJavaScriptを学んできたからです。中でも最も厄介な犯人のひとつが、シンプルなjQueryセレクタです。

// 無害に見えて、実はそうじゃない!
$(".something")

再読み込みされないページのためのJS設計

再読み込みされないページ向けにJavaScriptを書く際、最大の敵は「衝突(コンフリクト)」です。本来触れるはずのないHTMLをJSが操作してしまうと、原因究明が極めて困難な不具合が発生します。たとえば、ごく普通のjQueryイベントハンドラを見てみましょう。

$(document).on("click", ".hide-form", function() {
  $(".the-form").hide();
});

このコードが単一のページでしか動かないなら、まったく問題ありません。しかしDOMが再読み込みされない環境では、いつか誰かが.hide-formクラスを持つ別の要素を追加するのは時間の問題です。こうして衝突が生まれます。

この種の衝突は、グローバルな参照が多いほど起きやすくなります。では、グローバル参照を減らすにはどうすればいいのか? 答えは「名前空間(ネームスペース)」です。

セレクタの名前空間化

次のRubyクラスでは、グローバルな名前はクラス名ひとつだけで、多数のメソッド名をその内部に隠蔽しています。

# ひとつのグローバル名で2つのメソッド名を隠している
class MyClass
  def method1
  end

  def method2
  end
end

DOM要素には名前空間の組み込みサポートがありません(ES6のWeb Componentsが普及するまでは)。それでも、コーディング規約を使えば名前空間を擬似的に実現できます。

たとえば、タグ編集ウィジェットを実装したいとしましょう。名前空間化しない場合、次のようになります。グローバル参照が3つもあることに注目してください。

// <input class="tags" type="text" />
// <button class="tag-submit">Save</button>
// <span class="tag-status"></span>

$(".tag-submit").click(function(){
  save($(".tags").val());
  $(".tag-status").html("Tags were saved");
});

一方、「名前空間」となるコンテナを作り、すべての要素参照をその内部に限定すれば、グローバル参照は1つに減らせます。おまけに、いくつかのクラス自体が不要になり、マークアップもすっきりします。

// <div class="tags-component">
//   <input type="text" />
//   <button>Save</button>
//   <span></span>
// </div>

$container = $("#tags-component")
$container.on("click", "button", function(){
  save($container.find("input").val());
  $container.find("span").html("Tags were saved");
});

「簡単だと言ったでしょう?」というわけです。

上の例では、DOM要素をtags-componentというクラス名のコンテナの中に入れて名前空間化しました。この名前自体に特別な意味はありません。しかし、「すべての名前空間コンテナは末尾が-componentのクラスを持つ」という命名規則を採用すると、非常に興味深い効果が得られます。

誤ったグローバルセレクタを一目で判別できる

許可するグローバルセレクタを「コンポーネントへの参照のみ」に限定し、すべてのコンポーネントが-componentで終わるクラス名を持っていれば、不正なグローバルセレクタがひと目でわかるようになります。

// 悪い例
$(".foo").blah()

// 良い例
$(".foo-component").blah()

HTMLを操作するJSファイルが見つけやすくなる

先ほどのタグフォームに戻りましょう。フォームのHTMLは書きました。次にJSとCSSを追加する必要がありますが、どのファイルに置けばいいのでしょう? 幸い、名前空間の命名規則を決めておけば、そのままJS・CSSファイルの命名規則に転用できます。ディレクトリ構成は次のようになります。

.
├── javascripts
|   ├── application.coffee
│   └── components
│       └── tags.coffee
└── stylesheets
    ├── application.scss
    └── components
        └── tags.scss

自動初期化

従来のWebアプリでは、ページロード時にすべてのJSを初期化するのが一般的でした。しかしPJAXやTurbolinksの環境では、要素が常時DOMに追加・削除されます。そこで、新しいコンポーネントがDOMに現れたことを自動的に検出し、必要なJSをその場で初期化できる仕組みがあると非常に便利です。

一貫した命名規則があれば、この自動初期化は驚くほど簡単に実装できます。アプローチは無数にありますが、一例を紹介します。

Initializers = {
  tags: function(el){
    $(el).on("click", "button", function(){
      // コンポーネントのセットアップ
    });
  }

  // 他のイニシャライザもここに追加できる
}

// 通常ロード・pjaxロードの両方で呼ばれるハンドラ
$(document).on("ready pjax:end", function(){
  for(var key in Initializers){
    $("." + key + "-component").each(Initializers[key]);
  }
});

CSSの品質も向上する!

Webページで衝突を引き起こすのはJavaScriptだけではありません。CSSの方がむしろ深刻なこともあります。幸い、この名前空間の仕組みは、衝突のないCSSを書くことにも大いに役立ちます。SCSSならさらに美しく書けます。

.tags-component {
  input { ... }
  button { ... }
  span { ... }
}

  1. ソーシャルメディアプロフィール用の印刷可能なQRコードを生成する方法

    現代では、ソーシャルメディアを利用していない人を見つけるのは難しくなっています。私たちはSNSアカウントを使って自分自身をアピールしており、あなたについてもっと知りたいと思った人は、あなたのSNSアカウントを訪れて情報を得ることができます。 通常、メールでSNSのリンクを共有したり、ユーザーIDやユーザー名を直接伝えたりすることがあります。確かにリンクは相手をアカウントへ誘導する最も簡単な方法ですが、印刷物や履歴書からアクセスしてもらいたい場合はどうでしょうか。ユーザー名を伝えるだけでは、読み手に手間がかかるため、必ずしも良い方法とは言えません。そんなときこそ、QRコードの出番です。QRコード

  2. CCleaner for Android 徹底レビュー:スマホの動作が重いと感じたら試したい無料最適化アプリ

    スマートフォンは、私たちが所有する最も重要で価値のある道具です。人とのつながりだけでなく、財布、ナビゲーション、エンターテインメント、さらには仕事のツールとしても欠かせない存在になっています。新しいスマホを手にしたときのワクワク感は格別ですが、数ヶ月使用するとその感覚は薄れていきます。かつてのキビキビした操作感、瞬時の反応、一瞬で起動するアプリの速さは消え去り、代わりに動作のもっさり感やアプリ起動までの待ち時間の増加などに悩まされるようになるのです。 しかし、それはスマホを買い替えるタイミングではなく、最適化すべきサインです。あらゆる機械にはメンテナンスが必要であり、スマートフォンも例外ではあ