LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログ

レガシーなRails製管理画面開発をimportmap-railsとdartsass-railsで最適化する

はじめに

バックエンドエンジニアの星野です。

最近、節電を意識して自宅の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ライブラリを読み込もうとした場合、主に次のパターンが考えられます。

  1. vendor/以下にライブラリ本体を置いてアセットパイプラインで読み込む
  2. jquery-railsbootstrap等Gemを使う
  3. npm、yarnで管理してnode_modules/をアセットパイプラインで読み込む
  4. Webpacker、Shakapackerでバンドルする
  5. importmap-railsで管理する
  6. jsbundling-railsでバンドルする

今回の更新では2から5に移行しました。

importmap-railsはRails 7で紹介されている新しいJavaScriptの管理方法ですが、実はRails 6.0以上で利用できます。 npmを使わずにライブラリをバージョン管理できることが大きな特徴で、すでにWeb上に多くの解説記事がでているので詳細はそちらを参照してください。

github.com

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

github.com

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でなくても利用できます。

github.com

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

github.com

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はまさにそのために作られているため採用を検討しました。

github.com

結論から言うとPropshaftは前述のGemとは異なりRails 7以上でないとインストールできないため利用できませんでした。Rails 6.1まではrequire 'rails/all'はsprockets-railsを含み、Rails 7からは依存から外れたのが理由と考えています。

Make Sprockets more optional, offer Propshaft as alternative by dhh · Pull Request #43261 · rails/rails · GitHub

Ruby on Rails 7.0 Release Notes — Ruby on Rails Guides

まとめ

BootstrapとjQueryで構築されたレガシーなRails管理画面のフロントエンドツールチェインを、Import mapsとDart Sassに移行することで、npmをなくすことができました。

npmをなくすことで開発環境やデプロイの構築が簡略化され、たまに発生する保守作業の開発者体験の向上が期待できます。

参考資料

*1:業務上必要ではあるけど改修の優先度は低いのでモダンなフロントエンドスタックで更新するほどではないところがポイントです。

*2:社内からのみの利用であれば依存ライブラリのこまめな更新は無視したくなりますが、サイバー攻撃による脅威はどこからくるか分からない昨今の事情を鑑みると用心することに越したことはありません。

*3:Tailwind CSSのみTailwond CSSのCLIをラッパーしたtailwindcss-railsがあるので回避できます。bootstrap Gemのように既存のGemとして提供されているCSSライブラリはsassc依存なことが多くDart Sassと相性が悪いことが多いです。