俺の最近のRailsのJS開発環境を教えてやる

## はじめに

railsのsprocketsがキツイ。特にjsファイルが多くなると開発がとっってもキツイ。

layoutに

```
= javascript_include_tag "application"
```

こうやってるだけでも出力されたhtmlにはscriptタグが30個ぐらいならんでて、ページの読み込みに10sec以上かかる。

だけど、単に```app/assets/javascripts```を```gulp watch```とかはしたくない。
なぜならビルドはブラウザのリロード時に変更がある場合だけして欲しかった。

あと```window.AppNamespace```以下にモジュール追加していくのも辛い。
モジュール同士の依存関係もよくわかんないし、何より```window.AppNamespace.Modules.UserList.ItemView```とか長すぎ！

## browserify-railsってやつ使ってみた

browserifyがrailsの仕組みの中で動くようになる。

> browserify-rails/browserify-rails
> https://github.com/browserify-rails/browserify-rails

これが超便利。基本的には```require("hoge")```がjsコード内に存在するとbrowserifyしてくれる。
あと```//= require hoge```も今までどおり動くっぽいけど同時に使うことはあんまりないと思う。

これをつかって```window.AppNamespace```を卒業できた。

```js
// hello.js
module.exports = {
  hello: function() {
    return "Hello World"
  }
};

// application.js
var Hello = require("hello");

document.addEventListener("DOMContentLoaded", function() {
  document.querySelector("h1").textContent = Hello.hello();
});
```

副作用的にjqueryもあんまり使わなくなったし、なによりscriptタグが１個で済むのでブラウザのリロードが快適。
jsビルドがあるとちょっと待つけどね。

## npmでlodashとかインストールしてる

browserifyは当然nodeのツールなのでpackage.jsonで依存ライブラリを管理できるようになる。

```js
// $ npm install --save lodash 
// npm でインストール

// foo.js
var _ = require("lodash");

module.export = function(array) {
  return _.map(array, function(item) {
    return "foo!" + item;
  });
};
```

## babelifyでES6が書ける

browserify-railsの設定を変えるとbabelifyできるようになる（先に```npm install --save babelify```しておく必要はある）

```rb
# config/application.rb

config.browserify_rails.commandline_options = '--transform babelify'
```

```js
// constで宣言できたり
const _ = require("lodash");

module.exports = function(array) {
  // fat arrow のfunctionが書けたり
  return _.map(array, item => "foo" + item);
};

// もちろんclassも
class Person {
  constructor(name) {
    this.name = name;
  }
}
```

## babelifyするともちろんJSXが書けるのでReactが捗る

ただしコツがあって、requireするときに```.jsx```拡張子まで含める必要がある

```js
// jsxファイルはこれでは Error: Cannot find module と怒られる
const Item = require("components/item");

// これなら大丈夫
const Item = require("components/item.jsx");
```

config/application.rbのbrowserify_rails設定に--extensionsを追加してみたんだけど変わらなかった

```rb
config.browserify_rails.commandline_options = '--transform babelify --extensions .jsx'
```

## jsの世界にrailsのroutesが無い問題

jsでXHRするコードを書いてる時によくある問題だと思うのだけど、```GET /users.json```をリクエストしたいけど、パスをjsにハードコードするのは嫌だなーって時ある。

今まではdata attributeに渡してた。

```slim
span( data-fetch-users-path=users_path(format: :json)
      data-react-component="item" )
```

これも辛い。

## js-routesで解決

js-routesというgemは前から知ってたんだけど、```window.AppRoutes```にモジュールが追加されるので嫌だなぁと思ってた。
けど生成するコードがcommonjsとかのモジュール形式にも対応してるので普通にrequireで取得できた。
ただし、動的にrequireするのはbrowserify-railsが動かないので、config/routes.rbを更新したら生成しておく必要がある。

自分はrakeタスクを作って適宜叩くようにした

```rb
# lib/tasks/js.rake

namespace :js do
  desc 'Create rails-routes.js'
  task routes: :environment do
    path = Rails.root.join('app/assets/javascripts/rails-routes.js')
    options = {
      namespace: 'RailsRoutes',
      exclude: [/^admin_/]
    }
    JsRoutes.generate!(path, options)
  end
end

# bin/rake js:routes で app/assets/javascripts/rails-routes.js が生成される
```

これでdata attributeに渡さなくてもいい感じにjsからrailsのroutesを取得できる

```js
const RailsRoutes = require("rails-routes").RailsRoutes;

RailsRoutes.users_path({ format: "json" }); // => /users.json
```

## Jestでテスト

jestはfacebookが作ったjasmineの拡張版で、jsdomってゆうDOMのシミュレータを使ってるのでphantomjsを起動しなくてもDOM絡みのテストが書けてしまう。
ものすごく早く実行できるのに、jqueryが普通に使えてびっくりする。

使い方は```npm install --save-dev jest-cli```してから```package.json```に設定を書く

```json
{
  "before": "省略",

  "scripts": {
    "test": "node ./node_modules/jest-cli/bin/jest.js"
  },
  "jest": {
    "rootDir": "./spec",
    "testDirectoryName": "javascripts",
    "scriptPreprocessor": "../node_modules/babel-jest",
    "moduleFileExtensions": [
      "js",
      "jsx"
    ],
    "unmockedModulePathPatterns": [
      "react",
      "test-helper"
    ],
    "testFileExtensions": [
      "js",
      "jsx"
    ],
    "testPathIgnorePatterns": [
      "test-helper.js"
    ],
    "globals": {
      "appRootDirname": "rails-root-name"
    }
  },

  "after": "省略"
}
```

scriptsとjestのセクションを追加してる。
デフォルトでjestは```__tests__```ディレクトリをテスト用として認識するんだけど、```spec/javascrips```を見て欲しいのでその辺りを変更してる。

あとテスト実行前にトランスパイルして欲しいのでbabel-jestを使ってる。

## テスト内でrequireのパスが長くなりすぎる問題

jestは基本jsのディレクトリのすぐ近くでテストを書くのを推奨してるっぽい
```javascripts/hoge.js``` のテストは ```javascripts/__tests__/hoge.js``` に書く
だけどrailsだと ```app/assets/javascritps``` と ```spec/javascripts``` に分けるほうが精神衛生上いい

というわけでテスト内のrequireが超絶長くなる

```js
jest.dontMock("../../../../app/assets/javascripts/foo/bar/hoge.js");
const Hoge = require("../../../../app/assets/javascripts/foo/bar/hoge.js");
```

これを解消するために```test-helper.js```を作った。

```js

module.exports = {
  appPath(filePath) {
    let splitDir = __dirname.split("/");
    splitDir.shift();

    let appRootIndex;
    splitDir.map((dir, i) => {
      // ※package.jsonの```jest.globals.appRootDirname```にrailsのルートフォルダの名前をセットしておく
      if (dir === appRootDirname) {
        appRootIndex = i + 1;
      }
    });

    splitDir.splice(appRootIndex, splitDir.length - appRootIndex);
    splitDir = splitDir.concat("app/assets/javascripts".split("/"))
    splitDir = splitDir.concat(filePath.split("/"))

    return splitDir.join("/");
  }
};
```

これを```spec/javascripts/test-helper.js```に置いて、テスト内でrequireして使う

```js
// spec/javascripts/foo/bar-spec.js
const testHelper = require("../test-helper");
const modulePath = testHelper.appPath("foo/bar.js");
jest.dontMock(modulePath);
const Bar = require(modulePath);
```

jqueryを使ってるモジュールのテストはこんな感じ

```js
// app/assets/javascripts/commonjs-modules/global-header.js
const $ = require("jquery");

module.exports = class GlobalHeader {
  constructor() {
    this.$el = $(".global-header");
  }
}

// spec/javascripts/commonjs-modules/global-header-spec.js
const testHelper = require("../test-helper");
const modulePath = testHelper.appPath("commonjs-modules/global-header.js");
jest.dontMock(modulePath);

describe("GlobalHeader", function() {
  const GlobalHeader = require(modulepath);

  beforeEach(function() {
    document.body.innerHTML = '<div class="global-header"></div>';
  });

  afterEach(function() {
    document.body.innerHTML = "";
  });

  describe("constructor", function() {
    it("should set $el", function() {
      const globalHeader = new GlobalHeader();
      expect(globalHeader.$el).not.toBeNull();
      expect(globalHeader.$el.length).toEqual(1);
    })
  });
});
```

あとは```npm test```と打つだけ。