静的なサイトに JavaScript のルーターを導入する

ホームページビルダーで生成された静的な HTML で構成されるサイトをリニューアルする際に JavaScript のルーターを導入して、検討したことや学んだことについて述べます。

ルーターとは
----------

このドキュメントでは、ルーターとは URL を解析して、URL のパターンごとに開発者が割り当てた関数もしくはメソッドを実行する機能をもつライブラリのことを指します。ルーターの代わりにルーティングが同じ意味の用語として使われます。

ねらい
------

 ルーターを導入することで次の項目の改善が期待されます。
 
 * 保守性の向上
 * ページの切り替え速度の向上

### 保守性の向上

ルーターを導入する場合、レイアウトとコンテンツを分離することになります。HTML ファイルはただ1つの `index.html` に限定されます。コンテンツは別のテキストファイルとして保存して、Ajax 通信で取得することになります。

複数の HTML ファイルを提供する場合と比べて、レイアウトを修正する場合の作業量が減ります。コンテンツを追加、修正する際に、誤って別の箇所を編集してしまう可能性が減ります。

結果として、テンプレートエンジンを導入する場合と同じ効果を得ることになります。

### ページの切り替え速度の向上

ページを切り替える際にコンテンツの部分だけ更新すればすむので、毎回ページをまるごと読み込む方法と比べて、通信量が減るので、結果として、ページの表示速度が向上します。

通信量をさらに減らすために localStorage や IndexedDB にコンテンツを保存する選択肢があります。

検討課題
-------

ルーターを採用するにあたり、次の項目を検討しました。
 
 * URL の形式
 * コンテンツの保存形式
 * リンクの対象範囲
 * ルーターとフレームワークの組み合わせ

並行して、次の項目を検討しました。

 * 通信量を減らすためのビルドツールの併用
 * Ajax 通信ライブラリ

### URL の形式

サーバーの設定とブラウザーが History API をもとに利用できるかで決めます。

#### History API 対応

History API をサポートするブラウザーを対象とするのであれば,、`example.com/about`、`example.com/articles/123` のような形式の URL を採用することができます。

この URL の形式を選ぶ場合、検索エンジンやブックマークなどからトップページではないページに最初にアクセスしたときも正常にコンテンツが表示されるように、.htaccess などでサーバーの URL 書き換えルールを変更する必要があります。

サーバーの設定が変更できない場合、レガシーブラウザー対応のやり方も検討する必要があります。

##### .htaccess の例

Apache HTTP サーバーの `.htaccess` の例を示します。

```bash
<ifModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule (.*) index.html [L,QSA]
</ifModule>
```

コンテンツを保存する content ディレクトリを除外するために、content ディレクトリに別の .htaccess を設置します。

```bash
<ifModule mod_rewrite.c>
    RewriteEngine Off
</ifModule>
```

#### レガシーブラウザー対応

History API をサポートしない古いバージョンのブラウザー対応を考える場合、もしくはサーバーの URL 書き換えルールを使えない場合、`example.com/#!about` や `example.com/#!articles/123` のようなハッシュバン (hashbang) をつけた URL を選ぶことになります。ただし、2015年10月に Google が Ajax クロールを廃止することを[アナウンス](http://googlewebmastercentral-ja.blogspot.jp/2015/10/deprecating-our-ajax-crawling-scheme.html)しており、引き続きインデックスはサポートするものの、サイトをつくりかえる際には新しいベストプラクティスを適用することを推奨しています。

### コンテンツの保存形式

コンテンツは専用のディレクトリでテキストファイルとして保存します。形式は HTML、Markdown、JSON が挙げられます。

コンテンツに加えて、メタ情報も一緒に保存するかどうかを考える必要があります。

フォーマットのシンプルな例としてメタ情報を JSON、コンテンツを HTML、区切り文字を - (ハイフン)とする場合を示します。

```html
{
    "title": "タイトル"
}
---------------------
コンテンツ
```

メタ情報の内容や形式に関して、Jekyll をはじめとする HTML 生成ツールが参考になります。近年 golang で書かれた [Hugo](https://gohugo.io/) が登場し、HTML ファイルをより速く生成することが求められるようになりました。

### リンクの対象範囲

ルーターの対応範囲をナビゲーションやメニューのリンクに限定するか、それともすべてのリンクを対象にするか、そして、相対 URL に限定するか、絶対 URL も対象に入れるかで考える必要があります。すべての相対 URL を対象にする場合、次のようなコードを書くことができます。


```js
// http://stackoverflow.com/a/32375108/531320
$(document.body).on('click', 'a[href]', function(evt) {

  var target = evt.currentTarget;
  var href = target.getAttribute('href');

  if (!href.match(/^https?:\/\//)) {
    navigate(href);
    evt.preventDefault();
  }
});
```

### ルーターとフレームワークの組み合わせ

jQuery だけで、フレームワークを使わない場合、[Microjs](http://microjs.com/#rout) から探すとよいでしょう。History API をサポートしない古いブラウザーを考慮するのであれば、それぞれのルーターごとの対応策を知る必要があります。

#### Page.js

筆者が試したなかでは一番かんたんでした。JavaScript フレームワークを使わないケースによいと思います。試したコードは[こちら](https://github.com/masakielastic/pagejs-example)をご参照ください。

```js
page.base('/pagejs-example');
page('/', controller);
page('/:name', controller);
page.start();
```

#### Vue Router

記事の執筆時点で、0.6.0 が動かなかったので、0.5.2 で試しました。試したコードは[こちら](https://github.com/masakielastic/vue-example/tree/master/router)をご参照ください。

テンプレートで扱うためにトランジションフックを使いました。

```js
var router = new VueRouter({
  history: true,
  root: '/vue-example/router'
})

router.map({
  '/:name': {
    component: {
      route: {
        data: function (transition) {
          transition.next({name: this.$route.params.name})
        }
      },
      template: '<p>{{name}}</p>'
    }
  }
})

router.start(App, '#app')
```

マニュアルの例では transition.next の呼び出しに setTimeout もしくは Promise が使われていますが、コードをかんたんにするために省略しました。

#### Backbone.Router

ES6 で試しました。コードは[こちら](https://github.com/masakielastic/backbone-es6-example/tree/master/route)をご参照ください。

```js
class MyRouter extends Backbone.Router {

  get routes() {
    return {
      '(:name)(/)': 'dispatch'
    };
  }

  initialize(options) {
    this.model = options.model;
  }

  dispatch(name) {
    if (name === null) {
      name = 'index';
    }

    this.model.set({
      title: name + ' のタイトル',
      body: name + ' の内容'
    });
  }

}

new MyRouter(options);
Backbone.history.start({ pushState: true, root: '/backbone-es6-example/route' });
```

#### React Router

React Router 1.0RC1 を対象とします。試したコードは[こちら](https://github.com/masakielastic/react-example)をご参照ください。React 0.14RC では render メソッドが ReactDOM の所属になりました。

```js
import { Router, Route, IndexRoute, Link } from 'react-router'
import createBrowserHistory from 'history/lib/createBrowserHistory'

ReactDOM.render((
  <Router history={createBrowserHistory()}>
    <Route path="react-example/router" component={App}>
      <IndexRoute component={Message}/>
      <Route path=":name" component={Message} />
    </Route>
  </Router>
), document.getElementById('container'))
```

#### Cycle.js

Cycle.js は新しいプロジェクトなので、ルーターの開発ははじまったばかりです。Cycle.js 作者の staltz 氏は [switchPath](https://github.com/staltz/switch-path) を開発しています。ほかに [cycle-router5](https://github.com/axefrog/cycle-router5) があります。

### その他
#### 通信量を減らすためのビルドツールの併用
##### ソースコードの圧縮

通信量を減らすために、HTML、JavaScript、CSS のソースコードから空白スペースやコメントなどを削除して、ファイルサイズを圧縮する選択肢があります。

圧縮ツールを採用する場合、JavaScript と CSS の将来のバージョン対応や拡張構文の利用も考える必要もあるでしょう。

WebPack と Babel を組み合わせれば、EcmaScript 6 を EcmaScript 5 で変換して、1つのファイルに圧縮することができます。同時に React で採用される拡張構文の JSX も変換されます。

#####　コンテンツの圧縮

Ajax 通信の回数を減らすために、コンテンツを1つの JSON ファイルにまとめて、読み込む選択肢があります。localStorage や IndexedDB に保存する方法も合わせて考える必要があります。

将来、サーバーサイドのプログラミング言語を使うために、REST API を想定したフォルダーを用意して、JSON を生成することも考えられます。

#### Ajax 通信ライブラリ

特別な要求がなければ、標準の Fetch API ([polyfill](https://github.com/github/fetch)) を使うとよいでしょう。jQuery を使わないもしくは jQuery の依存度を減らしたい場合にもよい選択肢です。

2015年以降のライブラリ選びの基準として。ES6 (ES2015) で導入された Promise に対応しているかどうかが挙げられます。上述の Fetch API は Promise に対応しています。

Promise に対応していれば、ES7 (ES2016) で提案されている Async/Await を使うことができます。2015年時点ではサポートするブラウザーはかぎられているので、Babel や TypeScript などを使う必要があります。

Babel のセットアップや Async/Await を使ったコードの例についてはこちらの[記事](http://qiita.com/masakielastic/items/96e5142f1326b6998533)にまとめました。

ファイルサイズを小さくすることが目的であれば、XMLHttpRequest の薄いラッパー([xhr](https://github.com/Raynos/xhr)、[atomic](https://github.com/toddmotto/atomic))などが検討候補になるでしょう。

#### 仮想 DOM とテンプレートエンジン

コンテンツの表示をカスタマイズする場合、テンプレートエンジンを使うかどうかを考える必要があります。小規模のコンテンツであれば、ES6 で導入された Template strings や [mustache.js](https://github.com/janl/mustache.js) や [pure.js](http://beebole.com/pure/) が挙げられます。

Backbone.js の underscore.js や vue.js のようにフレームワークに付属していることがあります。

近年、仮想 DOM が注目されています。React は拡張構文の JSX を採用し、HTML、JavaScript、CSS をコンポーネントという単位でまとめて管理します。JSX 以外の選択肢として [react-hyperscript](https://github.com/mlmorg/react-hyperscript) が挙げられます。

運用上の課題
----------

筆者は SPA をはじめたばかりなので、何が問題なのかよくわかっていないのですが、Backbone.js を使ったこともあり、次の記事が参考になりました。

 * [スマホのWeb、ハイブリッドアプリを同時開発する上で今一番理にかなっているJSフレームワークは?](http://qiita.com/takeshy/items/6fee3f2d0fbb3102dc7b)
 * [SinglePageApplicationにおける問題点と対応](http://qiita.com/takeshy/items/b7e1b6bd30d12ecb02c6)
 * [スマホ用のSingle Page Applicationサイトを作る上での問題点と対応](http://qiita.com/takeshy/items/892c0b0c5b5dc3885570)