はじめに
バックエンドエンジニアの星野です。
最近、節電を意識して自宅のUbuntuサーバーを停止しました。 夏真っ盛りですが、電気代の値上げで自宅サーバーにとっては冬の時代です。
LCLではレガシー脱却の取り組みの一環としてRuby on Railsで作成された社内向け管理画面のアップデートを実施しました。 その際にRails 7リリース前後で登場した、importmap-railsとdartsass-railsを利用してフロントエンドツールチェインを更新しました。
tl;dr
- サードパーティのJaveScriptライブラリをImport maps経由で読み込みように変更しました。
- SassのコンパイルをDart Sassに乗り換えました。
- importamap-railsとdartsass-railsはRails 6.0以上で利用できるので最新のRailsではなくても利用できます。
レガシーなRails製管理画面とは
この記事ではレガシーなRails製管理画面は以下の構成を指します。
- Ruby on Rails 6.0以上
- Bootstrap 3以上
- jQuery 2以上
2022年の技術ブログとは思えないライブラリが並んでいますが、これをお読みの皆さんのチームにも年代物の管理画面が1つや2つあることでしょう。*1
当時は管理画面を作ろうとした場合にBootstrap + jQueryが鉄板の組み合わせで、LCLも漏れなく該当していました。 レガシーと呼ぶ割にはRailsのバージョンが少し高いような気がしますが、5.2以前は既にEOLのため6.0以上としています。(伏線)
サードパーティのJaveScriptライブラリの読み込みをimportmap-railsに乗り換える
現在、RailsでサードパーティのJaveScriptライブラリを読み込もうとした場合、主に次のパターンが考えられます。
vendor/
以下にライブラリ本体を置いてアセットパイプラインで読み込む- jquery-rails、bootstrap等Gemを使う
- npm、yarnで管理して
node_modules/
をアセットパイプラインで読み込む - Webpacker、Shakapackerでバンドルする
- importmap-railsで管理する
- jsbundling-railsでバンドルする
今回の更新では2から5に移行しました。
importmap-railsはRails 7で紹介されている新しいJavaScriptの管理方法ですが、実はRails 6.0以上で利用できます。 npmを使わずにライブラリをバージョン管理できることが大きな特徴で、すでにWeb上に多くの解説記事がでているので詳細はそちらを参照してください。
jQueryとBootstrapで必要なJavaScriptを読み込むだけであれば、それぞれGemが提供されているためimportmap-railsを使う必要性は低いですが、 jQueryプラグインやちょっとしたライブラリを足したい場合に管理方法が分散してしまうのを防ぐため採用しました。
導入はREADMEの通りに行うだけで簡単です。Gemfileからjquery-railsやbootstrapやtherubyracerやmini_racerを削除して、必要に応じてapp/javascript/application.js
を調整します。
bundle add importmap-rails bundle exec rails importmap:install bin/importmap pin boostrap@4 # jQuery3はBootstrapの依存で追加される
terserやuglifierを利用している場合も、execjsが必要になってしまうのでコメントアウトしましょう。 管理画面程度あればJavaScriptを圧縮する必要性はないと判断しています。
# config/environments/production.rb # 前略 # 以下をコメントアウトか行を削除 # config.assets.js_compressor = :terser # config.assets.js_compressor = :uglifier
importmap-railsで気になるところ
お手軽に利用できるimportmap-railsですが、デメリットとしてDependabotなどの自動更新ツールに対応していない点が挙げられます。
outdatedやauditコマンドが存在するのでCIで定期実行してうまく調整することもできますが、rubygemsやnpmよりも手間がかかってしまいます。
config/importmap.rb
で読み込むライブラリと内容を揃えたpackage.json
を用意して更新の検知だけ行う方法もありますが一長一短です。*2
Sassのコンパイルをdartsass-railsに乗り換える
Sassコンパイラの変遷
dartsass-railsの前にSassコンパイラについて大雑把におさらいします。
CSSのスーパーセットであるSassはコンパイルしてCSSにする必要があります。 最初のSassコンパイラはRuby実装(Ruby Sass)でしたが、パフォーマンスや互換性の改善されたC++実装(LibSass)に置き換わっていきました。 現在は、さらに改善を試みたDart実装のDart Sassが登場して公式の推奨はこちらになっています。新機能の追加や非推奨な記法の廃止はDart Sassのみになるため今後の選択肢としてもDart Sass 1択です。
そのような経緯によるかはわかりませんが、同じ名前のsassというパッケージがnpmとrubygemsで全く異なるのでややこしいことになっています。
実装 | npm | rubygems |
---|---|---|
Ruby Sass | - | sass |
LibSass | node-sass | sassc |
Dart Sass | sass | - |
サードパーティのCSSライブラリの読み込みをdartsass-railsに乗り換える
RailsでSassをコンパイルする場合もDart Sassに置き換える必要があります。ここで問題になるのがDart Sassのインストールです。OSのパッケージマネージャーでインストールできるので導入は容易ですが、Railsの明示的な依存として宣言する場合はnpmでインストールする必要がありました。これではImport mapsでnpmを捨てることができたのに嬉しくありません。
そこで、RailsチームはDart Sassをラップしてアセットパイプラインに組み込むdartsass-railsをリリースしたと予想しています。こちらもRails 7前後のフロントエンドツールチェイン刷新に伴っての登場ですが、依存はRails 6.0以上なので最新のRailsでなくても利用できます。
dartsass-railsの導入もimportmap-railsと同様に、READMEの通りに実行すれば難しいことはありませんでした。Gemfileからsass-railsとsasscの削除も忘れないようにしましょう。
bundle add dartsass-rails bundle exec rails dartsass:install
importmap-railsと異なり、CSSライブラリのバージョン管理まで実施してくれないため必要なライブラリはvendor/assets
以下にダウンロードする必要があります。Railsの設定によりますが、必要に応じてassets.rb
でアセットパイプラインのパスに加えます。
# このディレクトリ構成の場合 % tree -d vendor/assets/ vendor/assets/ ├── bootstrap │ ├── mixins │ ├── utilities │ └── vendor └── font-awesome ├── fonts └── scss
# config/initializers/assets.rb # 前略 Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'bootstrap') Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'font-awesome', 'scss') Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'font-awesome', 'fonts')
Import mapsやnpmと異なりCSSライブラリを手作業で管理しなければいけないのはかなりイケてないのですが、DHHもそう答えているので頑なにnpmを避ける場合は妥協します。ここはレガシーな管理画面程度あれば問題にならないとして許容しました。*3
Dockerでファイル変更を検知する
READMEではrails dartsass:install
すると以下のProcfile.devが作成されるのでforemanで起動するように記載されていますが、LCLでは開発環境のデフォルトはDockerなのでdocker composeでSassをコンパイルできるようにします。
# Procfile.dev web: bin/rails server -p 8080 css: bin/rails dartsass:watch
細部はプロジェクトごとに異なると思いますが、リポジトリルートをバインドマウントしてdartsass:watch
すればOKです。
# docker-compose.yml version: '3.8' # 現在はcompose specificationで記述しても良い services: # 中略 sass: volumes: - .:/ restart: always command: bundle exec rails dartsass:watch
Font Awesomeのvariablesを上書き
Bootstrapが依存するFont Awesomeも利用する場合、Sassの中で指定するフォントのパスをアセットパイプラインに含めるために$fa-font-path
を変更する必要があります。こちらの対応はDart Sassから利用できる@use...with
の記法で解決できました。
# app/assets/stylesheets/application.scss # 前略 @use "font-awesome" with ( $fa-font-path: "." );
Propshaftについて
importmap-railsとdartsass-railsを使うことでSprocketsの仕事はアセットパスの解決とダイジェストハッシュの付与のみになります。Rails 7からSprocketsの精神的後継と位置付けられているPropshaftはまさにそのために作られているため採用を検討しました。
結論から言うとPropshaftは前述のGemとは異なりRails 7以上でないとインストールできないため利用できませんでした。Rails 6.1まではrequire 'rails/all'
はsprockets-railsを含み、Rails 7からは依存から外れたのが理由と考えています。
Ruby on Rails 7.0 Release Notes — Ruby on Rails Guides
まとめ
BootstrapとjQueryで構築されたレガシーなRails管理画面のフロントエンドツールチェインを、Import mapsとDart Sassに移行することで、npmをなくすことができました。
npmをなくすことで開発環境やデプロイの構築が簡略化され、たまに発生する保守作業の開発者体験の向上が期待できます。
参考資料
- Rails 7.0でアセットパイプラインはどう変わるか | Wantedly Engineer Blog
- さようならLibSass、こんにちはDart Sass。 | Atlas Developers Blog
- Rails 7: importmap-rails gem README(翻訳)|TechRacho by BPS株式会社
- Rails 7: dartsass-rails gemはNode.jsなしで使える|TechRacho by BPS株式会社
*1:業務上必要ではあるけど改修の優先度は低いのでモダンなフロントエンドスタックで更新するほどではないところがポイントです。
*2:社内からのみの利用であれば依存ライブラリのこまめな更新は無視したくなりますが、サイバー攻撃による脅威はどこからくるか分からない昨今の事情を鑑みると用心することに越したことはありません。
*3:Tailwind CSSのみTailwond CSSのCLIをラッパーしたtailwindcss-railsがあるので回避できます。bootstrap Gemのように既存のGemとして提供されているCSSライブラリはsassc依存なことが多くDart Sassと相性が悪いことが多いです。