Edited at
LivesenseDay 13

もし、僕らのRailsにSprocketsがなかったら

More than 1 year has passed since last update.

Livesense AdventCalendar その1 13日目の記事です。

今年は3枚ありますので、その2その3もご一緒にどうぞ。

フロントエンドのエコシステムの成長に伴って、Rails開発においてSprocketsを導入する、しないの議論が盛んに行われていました。

そこで、実際にRails上でフロントエンドエコシステムをSprocketsの代わりにできるのか、実際にやってみました。

自分で手を動かしてみて、得られた知見や感じたことを共有できればと思います。


再現するSprocketsのバージョン


  • Sprockets3系(4系については最後にちょろっと触れます。)


Sprocketsがやっていること


  • アセットのコンパイル

  • アセットファイル同士の依存性を管理する

  • アセットファイルにアクセスするためのパスを管理する


パーフェクトRuby on Rails 第3章より


ここまでは、殆どのフロントエンドビルドツールで再現できるでしょう。

やっかいなのはsprockets-railsが担っている部分です。こちらは


  • digestの付与

  • digestが付与されたアセットのパス解決

  • devlopment環境・production環境それぞれでのプリコンパイル

など、ビルド生成物を実際にRailsに乗せて、連携させる役割を担っています。

フロントエンドビルドツールをRailsに組み込むときに、開発プロセスやデプロイプロセスに合わせて、方法を選ぶ必要があります。


Sprocketsがやっていることをやってみる

では実際に始めていきましょう。

$ bundle exec rails new no-sprockets-rails --skip-sprockets

$ npm init

ビルドツールは今回はwebpack使います。

行うことは以下。


  • coffee-scriptコンパイル・sassコンパイル

  • 依存関係解決

  • 開発環境でのデバッグ

Sprocketsの再現が目的なので、色んな思いをグッとこらえてCoffeeScriptをコンパイルしていきます。

必要なライブラリをダダダッとインストール

npm install webpack coffee-loader coffee-script css-loader extract-text-webpack-plugin node-sass sass-loader style-loader --save-dev

ルートディレクトリにwebpackの設定ファイルwebpack.config.js用意し、ビルド設定を追加していきます。

webpackの設定については

が参考になります。


coffee-scriptコンパイル・sassコンパイル


entry

エントリーファイルはSprocketsに則って、以下のように。


webpack.config.js

entry: {

application: [
'./app/assets/javascripts/application.js',
'./app/assets/stylesheets/application.scss'
]
}


output

ビルド生成物はpublicディレクトリに出力します。


webpack.config.js

output: {

path: __dirname + '/public/',
publicPath: __dirname + '/public/',
filename: '[name].js'
}


loaders

.coffee拡張子の物はcoffeeコンパイル、.scss拡張子はsassコンパイルします。


webpack.config.js

module: {

loaders: [
{ test: /\.coffee$/, loader: 'coffee' },
{ test: /\.scss/, loader: ExtractTextPlugin.extract('style', 'css!sass') }
]
}


依存関係解決

Sprocketsの// requireの代わりに、JavaScript(Node.js)とsassのそれぞれの機能を使って、manifestファイルを作成していきます


JavaScript

babel-loaderは使っておらず、import構文は使えないので、普通にNode.jsのrequireでファイルを読み込みます。


app/assets/javascripts/application.js

require('./src/saySomethingInCoffee')

require('./src/saySomethingInJs')


Sass

こちらは@import機能を使います。Sprocketsのマニフェストファイルは.cssファイルですが、Sassの@import機能を利用したいため、拡張子を.scss変更します。


app/assets/stylesheets/application.scss

@import "src/bold.scss";


ここまでで、entry→コンパイル→outputまでができました。


開発環境と本番環境用の設定

続いて、NODE_ENVを利用して、開発環境用と本番環境用に処理を切り分けていきます。

本番用と開発用のnpm scriptを用意しましょう


package.json

"scripts": {

"build": "webpack", // 本番
"dev-watch": "NODE_ENV=development webpack -w" // 開発
}

今回は開発環境のみNODE_ENVを設定し、それ以外はprodctionと同じとみなします。

stagingなども必要であればここで切り分けておくと良いでしょう


webpack.config.js

const isDevMode = process.env.NODE_ENV === 'development'


こうすることで、isDevModeで開発環境と本番環境で処理を切り分けることができます。

開発環境・本番環境それぞれで必要な設定以下のように追加していきます。

開発環境
本番環境

ソースマップを出力する
Uglifyする

digestを付与する

webpack-manifest.jsonを出力する

ソースマップの出力

開発環境でデバッグできるようにwebpackのsource-map出力機能を使用します

webpack -configuration-

幾つか種類がありますが、開発環境ではinline-source-mapを指定します。本番ではevalを指定します。


webpack.config.js

const devtool = isDevMode ? 'inline-source-map' : 'eval'


ExtractTextPluginを使ってsassコンパイルを行っている場合、sourceMapがうまく出力されずにハマりました。

以下のように記述すればとりあえずは出力されました。


webpack.config.js

loaders: [

{ test: /\.scss/, loader: ExtractTextPlugin.extract('style', 'css?sourceMap!sass?sourceMap') }
]

Uglifyする

UglifyJsPluginを追加します


webpack.config.js

const webpack = require('webpack')

plugins:[ new webpack.optimize.UglifyJsPlugin() ]

digestを付与する

ビルド生成物にdigestを付与します。webpackが提供してくれている[hash]をファイル名の後ろに付与します。


webpack.config.js

const fileName = isDevMode ? '[name]' : '[name]_[hash]'


fileNameを出力されるファイル名に指定することで、開発環境以外ではhash付きのファイルを出力することができます。

ですが、ビルドの度に変更されるhash付きのファイル名をRails側で参照する手段が存在しないので、このままでは使えません。

そこで、assets-webpack-pluginを使って、webpack-manifest.jsonを出力し、Railsとビルド生成物のひも付けを行います。

webpack-manifest.jsonを出力する

ひも付けと言ってもやることはシンプルで、ビルドの度にファイル名が記述されたJSONファイルを出力し、それをFile.readしてRailsから参照できるようにします。


webpack.config.js

const AssetsPlugin = require('assets-webpack-plugin')

new AssetsPlugin({path: __dirname + '/app/views', fullPath: false})


app/views/webpack-manifest.json

{

"application": {
"js": "application_c3d726db6f5bd7b8dd3f.js",
"css": "application_c3d726db6f5bd7b8dd3f.css"
}
}


app/controller/application_controller.rb

class ApplicationController < ActionController::Base

protect_from_forgery with: :exception
helper_method :script_for, :css_for

def script_for(bundle)
return "#{Rails.application.config.action_controller.asset_host}/#{manifest_json[bundle]['js']}" if Rails.env.production?
"http://localhost:3000/#{bundle}.js"
end

def css_for(bundle)
return "#{Rails.application.config.action_controller.asset_host}/#{manifest_json[bundle]['css']}" if Rails.env.production?
"http://localhost:3000/#{bundle}.css"
end

private

def manifest_json
file = File.read('app/views/webpack-assets.json') # This is the file generated by the plug-in
json = JSON.parse(file)
end
end


今回はapplication_controllerにいろいろベタ書きしてしまいましたが、initializerでFile.readした結果を保持しておくほうが良さそうです。


webpack設定完了

ここまででとりあえずwebpackの設定は完了なので、webpack.config.jsの全体像を確認しましょう


webpack.config.js

const ExtractTextPlugin = require('extract-text-webpack-plugin')

const AssetsPlugin = require('assets-webpack-plugin')
const webpack = require('webpack')

const isDevMode = process.env.NODE_ENV === 'development'

const fileName = isDevMode ? '[name]' : '[name]_[hash]'
const devtool = isDevMode ? 'inline-source-map' : 'eval'
const plugins = [new ExtractTextPlugin(`${fileName}.css`)]

if(!isDevMode) {
plugins.push(new AssetsPlugin({path: __dirname + '/app/views', fullPath: false})) // manifest
plugins.push(new webpack.optimize.UglifyJsPlugin()) // minify
}

module.exports = {
entry: {
application: [
'./app/assets/javascripts/application.js',
'./app/assets/stylesheets/application.scss'
]
},
output: {
path: __dirname + '/public/',
publicPath: __dirname + '/public/',
filename: `${fileName}.js`
},
module: {
loaders: [
{ test: /\.coffee$/, loader: 'coffee' },
{ test: /\.scss/, loader: ExtractTextPlugin.extract('style', 'css?sourceMap!sass?sourceMap') }
]
},
resolve: {
root: __dirname + '/assets/',
extensions: ['', '.js', '.js.coffee', '.sass']
},
devtool,
plugins
}



ビルドしてデプロイする。

さて、いよいよデプロイプロセスです。

rake assets:precompileに相当する部分が、npm scriptで定義したnpm run buildに置き換わるので、Capistranoなどのデプロイツールを使用する際は、そのタイミングでnpm run buildを実行し、ビルド生成物を本番サーバーに配布する手順となります。

今回はCircleCi上でビルドして、ビルド生成物をherokuにpushする形を取りたいと思います。

CircleCiやherokuについての細かい説明は割愛しますが、CircleCiの環境変数にherokuのemailとユーザー名を登録しておきます

スクリーンショット 2016-12-12 17.00.55.png

そして、circle.ymlはこんな感じに。


circle.yml

machine:

timezone:
Asia/Tokyo
ruby:
version: 2.3.0
node:
version: 6.9.1

dependencies:
cache_directories:
- "vendor/bundle"
- "node_modules"

override:
- rm Gemfile.lock
- bundle install --path vendor/bundle
- npm install
- npm rebuild node-sass
- npm run build

test:
override:
- echo "Nothing to do here"
database:
override:
- echo "Nothing to do here"

deployment:
production:
branch: master
commands:
- "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow"
- git config --global user.email $HEROKU_EMAIL
- git config --global user.name $HEROKU_USER_NAME
- git add public/.
- git add app/views/webpack-assets.json
- git commit --amend --no-edit
- git push -f git@heroku.com:no-sprockets-rails.git master:refs/heads/master
- heroku run rake db:migrate --app no-sprockets-rails:
timeout: 400 # if your deploys take a long time


node-sassの警告が出るので、npm installした後にnpm rebuild node-sassを挟んでいます。

deployment以下でherokuへのdeploy方法をドキュメントを参考にしつつ設定

かなり強引なやり方なのは自覚しつつ、ビルド生成物を無理やりlast commitに含めて無理やりherokuにpushしています。これは絶対他にいい方法があるだろうなと思いつつ、タイムアップ。

はじめてちゃんと触るのにもかかわらず、初対面で不躾な無茶振りをかましまくってしまい、CircleCiさんには本当に申し訳ない気持ちでいっぱいです。

今後ちゃんと勉強させていただきます。


おまけ Sprockets4

Railsにwebpackなどのビルドツールを導入する理由として、ES2015シンタックスでJavaScriptを書きたいというモチベーションがありますが、Rails5.1から標準となるSprockets4にてサポートされます。

https://github.com/rails/sprockets

他にもライブラリ管理がyarnになったり、webpackの導入も議論中のようなので、ES2015が導入モチベーションかつ、Rails5.1に上げられそうなプロダクトは少し様子を見たほうが良いかもしれないです。


まとめ

ということで、すべての機能を再現できているわけではないですがSprocketsなしでSprocketsっぽいことをやってみました。

今回はRails newしたばかりのRailsかつ、簡易的なデプロイプロセスでしたが、実際のプロダクトでSprocketsを使わないとなると、考慮しなければならない点がいくつもあります。

フロントエンドビルドツールは自由度が非常に高い反面、それを安定して開発プロセスやデプロイプロセスに組み込むためにはツールへの理解と運用を組み立てるスキルが欠かせません。

一方で、SprocketsはRailsのフロントエンドビルドツールとして統一されていることは、Sprocketsの大きな価値のひとつです。

フロントエンドビルドツールが統一されることによって、開発プロセスを支えるツール郡の開発コストが低くなり、Railsが安定した開発環境を持つ一助となっているように感じます。

ですがそれはあくまで「Railsの開発プロセス」なので、「Sprocketsをなくしたい!」というモチベーションはRailsの開発プロセスに管理されず、よりフロントエンドに適したやり方でフロントエンドを管理したいモチベーションからくるもののように思います。

Sprocketsを剥がすには安定したRails wayを捨てるリスクと、代替手段を用意するコストがかかります。

それらを天秤にかけ、やる価値があるかどうかをよく考えて判断したいところですね。(自戒を込めて)


参考