はじめに
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
を卒業できた。
// 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/javascritps
と spec/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
と打つだけ。