Rails 7.0 で Sprockets 代替として追加された Propshaft とは何か?
はじめに
Rails 7 で rails new -h
すると、Assets Pipeline としてこれまでの sprockets
の他に、 propshaft
を選べるようになっています。
$ bundle exec rails new -h
# (省略)
-A, [--skip-asset-pipeline], [--no-skip-asset-pipeline] # Indicates when to generate skip asset pipeline
-a, [--asset-pipeline=ASSET_PIPELINE] # Choose your asset pipeline [options: sprockets (default), propshaft]
# Default: sprockets
# (省略)
propshaft
とは何なのでしょう? 調べてみました。
Propshaft について
rails/propshaft: Deliver assets for Rails には次のようなことが書かれています。
- Railsのためのシンプルで高速なアセットパイプラインライブラリ
- webを取り巻く環境の変化により、Sprockets で実装されていた多くの機能を省略できるようになった
- アセットのbundlingによるHTTP接続数抑制が急務でない
- HTTP/2が普及したため
- JavaScriptやCSSは専用のNode.js Bundlerによってコンパイル、直接ブラウザに配信され
- Webpackなど他のツールを使うため、Railsでやる必要がなくなった
- ネットワーク帯域幅の増加によりミニマイズの必要性が低くなった
- アセットのbundlingによるHTTP接続数抑制が急務でない
できること
次の機能に絞って実装されているようです。
設定可能なロードパス
アプリやgemsの複数の場所からディレクトリを登録でき、それを1つのパスのように扱ってアセットを参照できます。
config.assets.paths = [
"first_path",
"second_path"
]
Digest付与
ロードパス中のすべてのアセットは、production環境用のプリコンパイルステップでコピー(またはコンパイル)され、すべてのアセットにDigest Hashが付与されます。このため、パフォーマンスを向上させるために、有効期限の長いキャッシュヘッダーを使用することができます。
処理によってパスをファイル名を置き換えるためのマニフェストファイルが提供れるため、Digest付与されたアセットは論理的なパスで参照できます。(たとえば image_tag "logo.svg"
のように、Digestを意識せずに扱える)
<%= image_tag "logo.svg" %>
<img src="/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg" />
開発用サーバー
開発時にアセットをプリコンパイルする必要はありません。同じ asset_path
ヘルパーを使って参照することができ、開発用サーバーから提供されます。
基本的なコンパイラ
シンプルな入力->出力コンパイラを提供しており、デフォルトではCSSのurl関数の呼び出しをurl(Digest付きアセット)に変換するようになっています。
header {
background-image: url("logo.svg");
}
header {
background-image: url("/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg");
}
使い方
今回作成したサンプルアプリ
rails-propshaft-demo: This is a sample application that uses propshaft.
インストール
rails new
の際に -a propshaft
オプションを渡します。
$ bundle exec rails new myapp -d postgresql -T -a propshaft
アセットの配置
標準では app/assets/
のディレクトリ以下 ファイル群をアセットとして参照できるようです。
- app/assets/images/**
- app/assets/javascripts/**
- app/assets/hoge/**
Propshaftは config.assets_path
を通じて設定されたすべてのパスからのすべてのアセットを提供可能にし、プリコンパイル時にそれらすべてを public/assets
にコピーします。これは 明示してないアセットをコピーしなかったSprocketsとは異なります 、とのことです。
なお、 hoge
ディレクトリを追加で作成して、そこに置いたファイルを参照するには開発サーバーを一度止めて起動し直す必要がありました( rails restart
ではなく)※記憶違いかも知れません
アセットの参照
Railsの一般的な asset_path
image_url
stylesheet_link_tag
などアセット用のヘルパーを使って、論理パスで参照することができます。
これらの論理パスでの参照は、 assets:precompile
が実行されると、production環境では自動的にDigestを考慮したパスに変換されます(public/assets/.manifest.jsonにあるJSONマッピングファイルを介して行われます)。
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<%= image_tag "logo.svg", width: 100 %>
<img src="<%= image_url "logo.svg" %>" />
<link rel="stylesheet" href="/assets/application-17b703ad60557b2bcf288e07fe219c5e7d97d806.css" data-turbo-track="reload" />
<script type="importmap" data-turbo-track="reload">{
"imports": {
"application": "/assets/application-0547340687405d8a7a67dc7233616d7effb7b849.js",
"@hotwired/turbo-rails": "/assets/turbo.min-2e103ccc37abc7e592e309b1e6fa6aab152c8c99.js",
"@hotwired/stimulus": "/assets/stimulus.min-18eaacc58b7827f3729d07fff0094620e22f9c20.js",
"@hotwired/stimulus-loading": "/assets/stimulus-loading-e6cc58d8195016dd774d85da7a37d34620facbfd.js",
"react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
"react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
"object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
"scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js",
"components/hello": "/assets/components/hello-a248303d1841779a67e126ae5b7d46f74d0d178f.js"
}
}</script>
<link rel="modulepreload" href="/assets/application-0547340687405d8a7a67dc7233616d7effb7b849.js">
<link rel="modulepreload" href="/assets/turbo.min-2e103ccc37abc7e592e309b1e6fa6aab152c8c99.js">
<link rel="modulepreload" href="/assets/stimulus.min-18eaacc58b7827f3729d07fff0094620e22f9c20.js">
<link rel="modulepreload" href="/assets/stimulus-loading-e6cc58d8195016dd774d85da7a37d34620facbfd.js">
<script src="/assets/es-module-shims.min-e2b12bbbb10c875738c2e170931855bd187b4b90.js" async="async" data-turbo-track="reload"></script>
<script type="module">import "application"</script>
<img width="100" src="/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg" />
<img src="http://localhost:3000/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg" />
追加のパス
たとえば lib/assets
や vendor/assets
のファイルを扱うには以下の設定が必要でした。(設定方法が正しいかはわかりません)
module PropshaftDemo
class Application < Rails::Application
# 省略
initializer "propshaft.append_assets_path" do
config.assets.paths += [
Rails.root.join("lib", "assets"),
Rails.root.join("vendor", "assets")
]
end
end
end
app/assets/images/logo.svg
<%= image_tag "logo.svg", width: 100 %>
lib/assets/images/logo-lib-path.svg
<%= image_tag "images/logo-lib-path.svg", width: 100 %>
vendor/assets/images/logo-vendor-path.svg
<%= image_tag "images/logo-vendor-path.svg", width: 100 %>
app/assets/images/logo.svg
<img width="100" src="/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg" />
lib/assets/images/logo-lib-path.svg
<img width="100" src="/assets/images/logo-lib-path-5a074187b1817710df435954fff96a9fdabc54aa.svg" />
vendor/assets/images/logo-vendor-path.svg
<img width="100" src="/assets/images/logo-vendor-path-5a074187b1817710df435954fff96a9fdabc54aa.svg" />
CSS中でアセットを使用する
CSSプリコンパイラにより url("アセット名.拡張子")
が url("/assets/アセット名-<digest>.拡張子")
に変換されます。
header {
background-image: url("logo.svg");
background-repeat: no-repeat;
padding-left: 6rem;
height: 5rem;
line-height: 5rem;
}
header {
background-image: url("/assets/logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg");
background-repeat: no-repeat;
padding-left: 6rem;
height: 5rem;
line-height: 5rem;
}
propshaft/lib/propshaft/compilers/css_asset_urls.rb
assets:precompile の成果物
assets:precompile
すると、ロードパス内のすべてのファイルがDigest付きの名前で public/assets
にコピーされます。
論理名(image_tag
などの引数として渡すアセット名)と実際のファイルパスとの変換表として public/assets/.manifest.json
が生成されます。
$ bundle exec rails assets:precompile
ruby@1b8ec592091a:/src$ ls -al public/
404.html apple-touch-icon-precomposed.png favicon.ico
422.html apple-touch-icon.png robots.txt
500.html assets/
$ ls -al public/assets
total 1380
-rw-r--r-- 1 ruby ruby 0 Jan 22 12:44 -da39a3ee5e6b4b0d3255bfef95601890afd80709.keep
drwxr-xr-x 4 ruby ruby 4096 Jan 22 12:44 .
drwxr-xr-x 3 ruby ruby 4096 Jan 22 12:44 ..
-rw-r--r-- 1 ruby ruby 2387 Jan 22 12:44 .manifest.json
-rw-r--r-- 1 ruby ruby 15831 Jan 22 12:44 action_cable-a21b4532ea1400611d408e8375c5c42a35c11199.js
-rw-r--r-- 1 ruby ruby 15701 Jan 22 12:44 actioncable-121775e39ad4dc481c68abee9955c60b0b1ff7d6.js
-rw-r--r-- 1 ruby ruby 14082 Jan 22 12:44 actioncable.esm-850276fc683d4c874d6c73f5ed0954f24f5bf389.js
-rw-r--r-- 1 ruby ruby 31357 Jan 22 12:44 actiontext-b8a3af3411472301d757607ad786cfd51554ca3b.js
-rw-r--r-- 1 ruby ruby 29679 Jan 22 12:44 activestorage-15505ec1ad2b2f6ff9b9e869bee3e9696fdf5873.js
-rw-r--r-- 1 ruby ruby 27602 Jan 22 12:44 activestorage.esm-5ee1008554161b62d5462c4b5e8714e6ed7617a4.js
-rw-r--r-- 1 ruby ruby 420 Jan 22 12:44 application-0547340687405d8a7a67dc7233616d7effb7b849.js
-rw-r--r-- 1 ruby ruby 214 Jan 22 12:44 application-17b703ad60557b2bcf288e07fe219c5e7d97d806.css
drwxr-xr-x 2 ruby ruby 4096 Jan 22 12:44 components
-rw-r--r-- 1 ruby ruby 48210 Jan 22 12:44 es-module-shims-eaec8b1da24ee4b2b4343336aa1806f218f483db.js
-rw-r--r-- 1 ruby ruby 100707 Jan 22 12:44 es-module-shims.js-e64f44f16a8b74b647ebe9ab305320d55391de89.map
-rw-r--r-- 1 ruby ruby 32134 Jan 22 12:44 es-module-shims.min-e2b12bbbb10c875738c2e170931855bd187b4b90.js
drwxr-xr-x 2 ruby ruby 4096 Jan 22 12:44 images
-rw-r--r-- 1 ruby ruby 2701 Jan 22 12:44 logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg
-rw-r--r-- 1 ruby ruby 2524 Jan 22 12:44 logo-lib-path-5a074187b1817710df435954fff96a9fdabc54aa.svg
-rw-r--r-- 1 ruby ruby 2524 Jan 22 12:44 logo-vendor-path-5a074187b1817710df435954fff96a9fdabc54aa.svg
-rw-r--r-- 1 ruby ruby 28377 Jan 22 12:44 rails-ujs-c3b690f9b8614cb5b5611b669cbf480235543f71.js
-rw-r--r-- 1 ruby ruby 64347 Jan 22 12:44 stimulus-33b5690d111cc0620be9dd1b12e236dacf03a024.js
-rw-r--r-- 1 ruby ruby 1745 Jan 22 12:44 stimulus-autoloader-5ac2058ccdb0f469e7813dac26b579a060698c4c.js
-rw-r--r-- 1 ruby ruby 987 Jan 22 12:44 stimulus-importmap-autoloader-f2bf94d759a35993da804aa44b40ad17874dbff4.js
-rw-r--r-- 1 ruby ruby 3141 Jan 22 12:44 stimulus-loading-e6cc58d8195016dd774d85da7a37d34620facbfd.js
-rw-r--r-- 1 ruby ruby 33270 Jan 22 12:44 stimulus.min-18eaacc58b7827f3729d07fff0094620e22f9c20.js
-rw-r--r-- 1 ruby ruby 121331 Jan 22 12:44 stimulus.min.js-787cb4d685ae9a10466252272464074f1215b5a9.map
-rw-r--r-- 1 ruby ruby 331822 Jan 22 12:44 trix-c9e1c067e4bfaad3fb2c0e4d65b5b11115fc3f6a.js
-rw-r--r-- 1 ruby ruby 16037 Jan 22 12:44 trix-f84c5ee27b3a44e58ee790d379e9bb11624891e2.css
-rw-r--r-- 1 ruby ruby 120750 Jan 22 12:44 turbo-584e11f34371796122a0100de30f21d9a7e4af20.js
-rw-r--r-- 1 ruby ruby 74133 Jan 22 12:44 turbo.min-2e103ccc37abc7e592e309b1e6fa6aab152c8c99.js
-rw-r--r-- 1 ruby ruby 222244 Jan 22 12:44 turbo.min.js-be482c12399498cf96dfffc770008c5c80f46f68.map
$ cat public/assets/.manifest.json
{"logo.svg":"logo-f0ec97e6e9d99193cd9593c9c7890084a6e6390f.svg",".keep":"-da39a3ee5e6b4b0d3255bfef95601890afd80709.keep","application.css":"application-17b703ad60557b2bcf288e07fe219c5e7d97d806.css","logo-lib-path.svg":"logo-lib-path-5a074187b1817710df435954fff96a9fdabc54aa.svg","logo-vendor-path.svg":"logo-vendor-path-5a074187b1817710df435954fff96a9fdabc54aa.svg","stimulus-loading.js":"stimulus-loading-e6cc58d8195016dd774d85da7a37d34620facbfd.js","stimulus.min.js":"stimulus.min-18eaacc58b7827f3729d07fff0094620e22f9c20.js","stimulus.js":"stimulus-33b5690d111cc0620be9dd1b12e236dacf03a024.js","stimulus-importmap-autoloader.js":"stimulus-importmap-autoloader-f2bf94d759a35993da804aa44b40ad17874dbff4.js","stimulus.min.js.map":"stimulus.min.js-787cb4d685ae9a10466252272464074f1215b5a9.map","stimulus-autoloader.js":"stimulus-autoloader-5ac2058ccdb0f469e7813dac26b579a060698c4c.js","turbo.js":"turbo-584e11f34371796122a0100de30f21d9a7e4af20.js","turbo.min.js.map":"turbo.min.js-be482c12399498cf96dfffc770008c5c80f46f68.map","turbo.min.js":"turbo.min-2e103ccc37abc7e592e309b1e6fa6aab152c8c99.js","es-module-shims.js":"es-module-shims-eaec8b1da24ee4b2b4343336aa1806f218f483db.js","es-module-shims.min.js":"es-module-shims.min-e2b12bbbb10c875738c2e170931855bd187b4b90.js","es-module-shims.js.map":"es-module-shims.js-e64f44f16a8b74b647ebe9ab305320d55391de89.map","actioncable.esm.js":"actioncable.esm-850276fc683d4c874d6c73f5ed0954f24f5bf389.js","actioncable.js":"actioncable-121775e39ad4dc481c68abee9955c60b0b1ff7d6.js","action_cable.js":"action_cable-a21b4532ea1400611d408e8375c5c42a35c11199.js","trix.js":"trix-c9e1c067e4bfaad3fb2c0e4d65b5b11115fc3f6a.js","actiontext.js":"actiontext-b8a3af3411472301d757607ad786cfd51554ca3b.js","trix.css":"trix-f84c5ee27b3a44e58ee790d379e9bb11624891e2.css","activestorage.js":"activestorage-15505ec1ad2b2f6ff9b9e869bee3e9696fdf5873.js","activestorage.esm.js":"activestorage.esm-5ee1008554161b62d5462c4b5e8714e6ed7617a4.js","rails-ujs.js":"rails-ujs-c3b690f9b8614cb5b5611b669cbf480235543f71.js","components/hello.js":"components/hello-a248303d1841779a67e126ae5b7d46f74d0d178f.js","application.js":"application-0547340687405d8a7a67dc7233616d7effb7b849.js","images/logo-lib-path.svg":"images/logo-lib-path-5a074187b1817710df435954fff96a9fdabc54aa.svg","images/logo-vendor-path.svg":"images/logo-vendor-path-5a074187b1817710df435954fff96a9fdabc54aa.svg"}
まとめ
現代のweb開発では Webpack など他のツールでプリコンパイルやバンドリングなどを行うことが多いので、 Railsのアセットパイプラインとしては配信に必要な最小限の機能だけ提供してシンプルに保とう いうというコンセプトのライブラリのようです。
READMEで述べられているように、現実に運用されているRailsアプリでは、Sprocketsの機能を利用したものがあるため Propshaft への移行は難しく、まだまだ置き換えられるものではい、ということです。
また、現時点ではアルファ版ということで、今後大きな変更などがあるかもしれません。業務で使うには時期尚早そうですね。
ですが 複雑で理解が難しかったSprocketsに代わり、シンプルな代替手段が登場するのは個人的には嬉しいと思っています。できれば使ってバグだしなどで貢献したいですね。
その他、他のツールなどですでにDigestが付けられているファイルの扱い方など、詳しくは rails/propshaft をご確認ください。
Discussion