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

  • 440
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

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

layoutに

= javascript_include_tag "application"

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

だけど、単にapp/assets/javascriptsgulp 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を卒業できた。

// 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タグが1個で済むのでブラウザのリロードが快適。
jsビルドがあるとちょっと待つけどね。

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

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

// $ 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しておく必要はある)

# config/application.rb

config.browserify_rails.commandline_options = '--transform babelify'
// 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拡張子まで含める必要がある

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

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

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

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

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

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

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

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タスクを作って適宜叩くようにした

# 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を取得できる

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に設定を書く

{
  "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/javascritpsspec/javascripts に分けるほうが精神衛生上いい

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

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

これを解消するためにtest-helper.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して使う

// 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を使ってるモジュールのテストはこんな感じ

// 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と打つだけ。