📝

Rails 7.0 で Sprockets 代替として追加された Propshaft とは何か?

2022/01/22に公開約12,800字

はじめに

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でやる必要がなくなった
    • ネットワーク帯域幅の増加によりミニマイズの必要性が低くなった

できること

次の機能に絞って実装されているようです。

設定可能なロードパス

アプリや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/assetsvendor/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

ログインするとコメントできます