これは kokoro.io Advent Calendar 2017 5日目の記事です。
皆さんはRailsにおけるassetsの管理をどうされていますでしょうか。私はsprocketsおよびwebpackerは要らない派なのでkokoro.ioにおいてはこれを使用せず、サーバーサイドとフロントエンドは完全に疎にして開発を行っています。今日はその辺りについて書きます。最近長文を書くモチベーションが皆無になっているので要点だけまとめて書きます。
TL;DR
- webpacker導入しなくてもちょっと工夫すればいい感じにwebpack使えるのでwebpacker依存はやめとこう
- sprockets使ってdigest付与する必要はなくて、RackミドルウェアかNginxのレイヤでETagベースのキャッシュ破棄戦略を使おう
脱webpacker編
webpackerを使用してもRailsが用意した無駄な抽象化層が増えるだけです。せっかくwebpackというフロントエンド側の世界でassetsの管理ができるのですから、Railsとは完全に切り離しましょう。特にkokoro.ioは将来的にサーバーサイドの実装をASP.NET Coreに置き換える予定なのでフロントエンド周りの設定についてバックエンド(というかrubygem)の実装が食い込んでいることは好ましくありません。その為できるだけ疎にしつつ簡単なヘルパを導入することでフロントエンドとバックエンドをプラガブルに保ちます。
基本的に開発環境ではwebpack-dev-serverを起動し、assetの参照をそこに向けます。これは単純なビューヘルパを定義してやれば一瞬で実現できます。
module ApplicationHelper
def assets_url(path)
if Rails.application.secrets.assets_base_url.present?
Rails.application.secrets.assets_base_url + path
else
path
end
end
end
secrets.yml
でassets_base_url
の値をdevelopment環境とproduction環境で切り替えます。development環境では'//localhost:8080'
を、production環境では''
(空文字)指定もしくは未指定(値がnilになります)にしておきます。
あとはview側で以下のようにassetを参照します。
<script src="<%= assets_url('/javascripts/application.js') %>" data-turbolinks-track="true" />
これで、development環境ではwebpack-dev-server、production環境ではRailsをホストしているRackにリクエストが飛ぶ様になります。
production環境においてはconfig.ru
の設定にてRackミドルウェアのレイヤでassetを配信してやるようにします。
require ::File.expand_path('../config/environment', __FILE__)
use Rack::ETag
use Rack::Deflater
use(Rack::Static, urls: ['/javascripts', '/stylesheets', '/images', '/fonts', '/sounds'],
root: 'assets/dist',
header_rules: [
[:all, {
'Cache-Control' => 'no-cache'
}]
]) if ENV['RACK_ENV'] == 'production'
run Rails.application
ここではRack::Static
を使用し、/javascript
や/stylesheet
などのディレクトリへのアクセスはルートディレクトリからのassets/dist
内のファイルを返却するようにしています。
あとは、デプロイ時にwebpackを使用しassets/dist
内にコンパイル済みassetsを配置する設定を書けば完了です。
これは各々自由にやってもらえればいいですが、herokuなどのPaaSにおいては規定でデプロイ時にassets:precompile
Rakeタスクが自動実行されるようになっていますので、/lib/tasks/assets.rake
ファイルを作りこれを上書きしてしまうのが手っ取り早いでしょう。
Rake::Task['assets:clean'].clear
Rake::Task['assets:precompile'].clear
namespace :assets do
desc "Precompile assets"
task precompile: :environment do
require 'open3'
Rails.logger.info "Install dependencies…"
stdin, stdout, stderr, status = Open3.popen3("cp -r ./node_modules ./assets/javascripts/")
stdin.close
stdout.each do |l|
Rails.logger.info l
end
stderr.each do |l|
Rails.logger.warn l
end
Rails.logger.info "Compiling…"
stdin, stdout, stderr, status = Open3.popen3("yarn font sound image build", chdir: "./assets/javascripts")
stdin.close
stdout.each do |l|
Rails.logger.info l
end
stderr.each do |l|
Rails.logger.warn l
end
if status.value.success?
Rails.logger.info "Compiled all assets."
else
Rails.logger.info "Compilation failed."
end
status.value.success?
end
desc "Clean assets"
task clean: :environment do
Rails.logger.info "Nothing to do with webpack."
end
end
依存するnpmパッケージをコンパイル実行ディレクトリにコピーした後、コンパイル用のコマンドを実行しています。この例ではyarn font sound image build
コマンドを実行していますが、ここはあなたのプロジェクトで定義してあるコンパイル用yarnタスクに置き換えてください。
以上です。結構やることが多く感じますが、やっていることは薄く明確なので、やっていきましょう。
脱sprockets編
上記の脱webpacker編の作業にて、既に脱sprocketsは完了しています。
皆さんが脱sprocketsできない一番の要因はasset urlへのdigest付与を使いたいためだと思われますが、上記のconfig.ru
の設定にてETagベースのキャッシュ破棄を実装していますので、assetsのurlを固定したまま、内容に変更があった場合のキャッシュ即時破棄が実現できます。
そもそもdigestを付与・変更することによるキャッシュ破棄戦略は好きではなく、この様なダーティハックを使うのではなく、ちゃんとHTTPの仕様として策定されているETagベースのキャッシュ破棄機構を使うべきと私は考えています。
上記の設定では、ページをロードする度に"必ず"各assetのURLに対してHTTPリクエストが飛んでしまいますが、assetの中身に変更がなかった場合、Rackミドルウェアは単に「お前が持ってるキャッシュ使えよな」という返答、つまりHTTPステータスコードで言うと304 Not Modifiedを返してくれるだけですので、httpリクエストの本数自体は削減できないものの、レスポンスのbodyは空なのでdigest付与によるキャッシュ破棄戦略に較べてそこまで通信コストが増えるとは思いません。
私は基本的によほど大規模であったりレイテンシを気にするサービスない限りはherokuなどを使いましょうというスタンスなのでRackのレイヤで処理してしまっていますが、Nginxのレイヤでも同様の事は可能ですので、Nginxが導入可能なのであればそのレイヤでEtagベースのキャッシュ破棄戦略を担当してしまえば無駄なhttpリクエストがRailsのプロセスやスレッドを専有することもないです。
CDNで配信しなければいけない規模の場合にdigestがないとキャッシュ破棄できなくて困るという声は聞きますが、恐らくFast.lyとか使えばいい感じにURL固定しつつキャッシュ破棄でいるんじゃないでしょうか。知らんけど。
要はバランス感覚で、せっかくHTTPプロトコルレベルでちゃんとキャッシュ破棄の仕様が策定されているのでそれに則ったほうが綺麗に物事を分離できるよねという話でした。終わり。