Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Sprockets->Propshaftアップグレードガイド(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

rails/propshaft - GitHub

  • 2022/07/14: 初版公開
  • 2023/10/18: 更新
  • 2024/02/21: 更新

Rails: Sprockets->Propshaftアップグレードガイド(翻訳)

Propshaftの適用範囲はSprocketsよりも狭いので、SprocketsからPropshaftへの移行にはjsbundling-rails gemとcssbundling-rails gemも利用する必要があります。本ガイドでは、プロジェクトがRails 6.1以後の以下の規約に沿っていることを前提としています。

  • JavaScriptのバンドルにwebpackerを利用している
  • CSSのバンドルにsass-railsを利用している
  • アセットのダイジェスト化にsprocketsを利用している

最後に、npx 7.1.0以降をインストールする必要もあります。

PropshaftはRails 7に依存しているので、移行を開始する前にアプリケーションをRails 7にアップグレードしておく必要があります。

1. Webpackerをjsbundling-railsに移行する

最初に以下の手順を進めます。

  1. Gemfileのwebpackerjsbundling-railsに置き換える
  2. ./bin/bundle installを実行する
  3. ./bin/rails javascript:install:webpackを実行する
  4. config/initializers/assets.rbファイルを削除する
  5. bin/webpackファイルを削除する
  6. bin/webpack-dev-serverファイルを削除する
  7. config/webpackフォルダを削除する(注: カスタム設定はすべて新しいwebpack.config.jsに移行すること)
  8. config/webpacker.ymlファイルを削除する
  9. コード内のjavascript_pack_tagをすべてjavascript_include_tagに置き換え、defer: trueを追加する

上の手順が完了すると、プロジェクトにさまざまなファイルが追加され、既存ファイルの一部も更新されていることがわかります。

新規ファイル: bin/devとProcfile.dev

./bin/devは、foremanProcfile.devを用いて1つのターミナルから2つのプロセスを起動するシェルスクリプトです。後者のProcfile.devファイルは、バンドルおよびJavaScriptファイルの変更監視を行うwebpack-dev-serverを置き換えます。

package.jsonにbuild属性が追加される

これは、yarn buildでJavaScriptファイルをバンドルするのに使うコマンドです。

新規ファイル: webpack.config.js

このファイルはwebpackerではgem内に隠されていましたが、直接編集可能になりました。config/webpackディレクトリにカスタム設定を配置していた場合は、このファイルに設定を移動できます。エントリポイントを複数持つプロジェクトの場合は、以下のようにentry属性を調整する必要があります。

module.exports = {
  entry: {
    application: "./app/javascript/application.js",
    admin: "./app/javascript/admin.js"
  }
}

app/assets/manifest.jsにlink_treeディレクティブが追加される

これは、assets:precompileの実行中にapp/assets/buildsディレクトリ内のファイルをインクルードするようSprocketsに指示します。yarn buildでバンドルしたファイルはこのapp/assets/buildsディレクトリに置かれるので、これらもリポジトリにコミットしてください。アセットを整理するときに誤って削除しないようにしましょう。

babelの扱いについて

babelによるトランスパイルを引き続き利用したい場合は、手動で設定する必要があります。最初に、webpack.config.jsファイルを開いて以下を追加します。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(js)$/,
        exclude: /node_modules/,
        use: ['babel-loader']
      }
    ]
  }
}

次にpackage.jsonファイルを開いて以下を追加します。

"babel": {
  "presets": [
    "./webpack.babel.js"
  ]
}

最後に、webpackers babel presetファイルをダウンロードしてファイル名をwebpack.babel.jsに変更し、package.jsonと同じディレクトリに配置します。

モジュールの解決について

Webpackerは、モジュール解決の対象にsource_path(デフォルトはapp/javascript/)を含めていたので、import 'channels'などのステートメントによってapp/javascript/channels/がインポートされていました。jsbundling-railsへの移行後はこのように動作しません。この振る舞いを維持したい場合は、webpack.config.jsファイルを手動で更新して以下を含める必要があります。

module.exports = {
  // ...
  resolve: {
    modules: ["app/javascript", "node_modules"],
  },
  //...
}

または、以下のように相対インポートを使うようにモジュールを変更する方法もあります。

- import 'channels'
+ import './channels'

JavaScriptからSassやSCSSを抽出する

Webpackerでは、webpacker.ymlファイル内のextract_cssを有効にすることで、JavaScriptからSassやSCSSを抽出可能です。これによって、たとえばimport '../scss/application.scssのようにJavaScript内のソースファイルもインクルードできます。

この機能を残したい場合は、以下の手順を実行します。

  1. yarn add mini-css-extract-plugin sass sass-loader css-loaderを実行する
  2. webpack.config.jsファイルを更新してmini-css-extract-pluginrequireし、ローダーを設定する(以下の例を参照)。

webpack.config.jsファイルの例:

const path    = require("path")
const webpack = require("webpack")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
  mode: "production",
  devtool: "source-map",
  entry: {
    application: "./app/javascript/application.js"
  },
  resolve: {
    modules: ["app/javascript", "node_modules"],
  },
  output: {
    filename: "[name].js",
    sourceMapFilename: "[file].map",
    path: path.resolve(__dirname, "app/assets/builds"),
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  ],
  module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
}

2. sass-railsをcssbundling-railsに移行する

メモ: これまでWebpackerのextract_cssを用いてCSSをビルドしていて、sass-railsrequireしていなかった場合は、このセクションの手順を省略できます。

最初に以下の手順を実行します。

  1. Gemfileにcssbundling-railsを追加する
  2. ./bin/bundle installを実行する
  3. ./bin/rails css:install:sassを実行する

手順が完了すると、いくつかのファイルが更新されていることがわかります。

Procfile.devファイルに新しいプロセスが追加される

JavaScriptのプロセスの場合と同様に、この新しいプロセスもCSSファイルのバンドルと変更監視を行います。

package.jsonにbuild:css属性が追加される

これは、yarn buildでCSSファイルをバンドルするのに使うコマンドです。

app/assets/manifest.jsからlink_treeディレクティブが削除される

CSSファイルはapp/assets/buildディレクトリに配置されるようになったので、Sprocketsはapp/assets/stylesheetsフォルダのことを気にする必要がなくなりました。他にもCSSファイル用のlink_treeがある場合は、それらも削除してください。

エントリポイントを複数設定する

Sprocketsがコンパイルするのは、manifest.jsファイル内に記載されているrootディレクトリ内のファイルだけですが、yarn buildで使うsassパッケージもサブフォルダをチェックするので、scssファイルで@importや変数などの機能を使っている場合はコンパイルエラーが発生する可能性もあります。このため、アプリに複数のエントリポイントがある場合は追加の作業が必要です。

app/asset/stylesheetsフォルダ内が以下のような構成になっているとします。

stylesheets/admin.scss
stylesheets/admin/source_1.scss
stylesheets/admin/source_2.scss
stylesheets/application.scss
stylesheets/application/source_1.scss
stylesheets/application/source_2.scss

最初に、エントリポイントを以下のように他のファイルと別ディレクトリに配置し、新しい構成に合わせて@importを調整します。

stylesheets/entrypoints/admin.scss
stylesheets/entrypoints/application.scss
stylesheets/sources/admin/source_1.scss
stylesheets/sources/admin/source_2.scss
stylesheets/sources/application/source_1.scss
stylesheets/sources/application/source_2.scss

次に、package.jsonファイル内のbuild属性を調整します。

"build:css": "sass ./app/assets/stylesheets/entrypoints:./app/assets/builds --no-source-map --load-path=node_modules"

非推奨警告

Sassで除算などの機能を使っていると非推奨警告が表示される可能性もありますが、修正方法は警告メッセージに表示されます。修正方法がわからない場合はSassの公式ドキュメントで詳細をチェックしてください。

3. SprocketsをPropshaftに移行する

最初に以下の手順を実行します。

  1. Gemfileからsprocketssprockets-railssass-railsを削除し、propshaftを追加する
  2. ./bin/bundle installを実行する
  3. config/application.rbファイルを開いてconfig.assets.paths << Rails.root.join('app','assets')を削除する
  4. app/assets/config/manifest.jsファイルを削除する
  5. CSSファイル内のアセットヘルパー(image_urlfont_urlをすべて標準のurlsに置き換える
  6. Railsフレームワークのインポートにrails/allを使わずに個別の機能をインポートしている場合は、require "sprockets/railtie"を削除する

アセットのパス

Propshaftは、プロジェクトおよびGemfile内にあるすべてのgemについて、探索パス内にある以下のフォルダを自動的にインクルードします。

  • vendor/assets
  • lib/assets
  • app/assets

インクルードされているファイルをすべて表示するには、rakeタスクのrevealが利用できます。

 rake assets:reveal

アセットヘルパー

Sprocketsと異なり、Propshaftはasset_pathasset_urlimage_urlといったアセットヘルパーに依存していません。代わりにPropshaftは、CSSファイル内にあるすべての url関数を探索し、それらの関数が参照するアセットのダイジェストをインクルードするよう調整します。

以下のように、使っているCSSファイルをくまなく調べて必要な調整を行ってください。

- background: image_url('hero.jpg');
+ background: url('/hero.jpg');

ここで、Propshaftではパスが/で始まっているのに、Sprocketsの場合はパスが/で始まっていないことにご注意ください。これは、後者では絶対パスが使われ、前者では相対パスが使われるためです。この違いをよりよく理解するために、以下のディレクトリ構造で考えます。

assets/stylesheets/theme/main.scss
assets/images/hero.jpg

Sprocketsの場合、main.scssはhero.jpgを以下のように参照します。

background: image_url('hero.jpg')

しかしPropshaftのurlで同じパスを指定すると、theme/hero.jpgが見つからないというエラーになります。その理由は、Propshaftがすべてのパスを「処理中のファイルからの相対パス」として仮定するためです。ここではthemeフォルダ内にあるCSSファイルを処理していたので、hero.jpgファイルを同じフォルダ内で探索します。

パスの冒頭に/を追加することで、このパスが絶対パスであると解釈するようPropshaftに指定できます。この変更によってアップグレードの手間が増えますが、そのおかげでFontAwesomeなどの外部ライブラリやBootstrapテーマなどがすぐ使えるようになります。

アセットのコンテンツ

アプリの画像をできるだけ素早く表示したいときに、小さなSVGファイルや低解像度版の画像をインライン化しておくというパターンがよく使われます。Propshaftでは、以下のコード行があらゆる環境で動作します。

Rails.application.assets.load_path.find('logo.svg').content

デフォルトのRailsはビューのHTMLタグをエスケープするので、レンダリングしたsvgを出力するには、html_safeまたはrawで文字列をエスケープしないよう指定する必要があります。

Rails.application.assets.load_path.find('logo.svg').content.html_safe
raw Rails.application.assets.load_path.find('logo.svg').content

development環境でのプリコンパイル

Propshaftは、developmentモードで動的アセットリゾルバを使います。ただしローカルでassets:precompileを実行すると、Propshaftが静的アセットリゾルバを使うように切り替わります。そうなるとアセットの変更が反映されなくなるので、アセットを変更するたびにアセットのプリコンパイルを実行しなければならなくなります1。ここがSprocketsと異なる点です。

動的なアセットリゾルバを再度有効にしたい場合は、ターゲットフォルダ(通常はpublic/assets)を削除する必要があります。これにより、propshaftがソースからの動的コンテンツの配信を開始するようになります。

CSSやJSのアセットの変更を監視するには、rails serverの代わりにbin/devでサーバーを起動する方法も使えます。こうすると、単にサーバーが起動するだけでなく、アセットが変更されたかどうかも監視するようになり、変更が検出されるとサーバー実行中にアセットをコンパイルします。これはProcfile.devで設定されています。

関連記事

Propshaft gem README(翻訳)

Rails 7: importmap-rails gem README(翻訳)


  1. 訳注: ./bin/rails assets:clobberを実行することで元の動的アセットリゾルバに戻せます(#36)。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。