Rails7でViteを使う - 開発環境での Hot Module Replacement 編

2025/01/04に公開

概要

  • Rails を採用しているプロジェクトは2024年末時点ではまだまだ多い
  • 中でも数年間運用してきたものは言語やフレームワーク自体のバージョンアップなどが必要
  • 今回は担当している案件で、Rails のフロントエンドのビルドツールに Vite を使用するようになったのでその素振りとまとめをここでする

目的

  • Vite と Rails のインテグレーションのハンズオンを実施し、理解を深めること

目標

  • ローカル開発環境で HMR(Hot Module Replacement) を有効にすること

環境

技術 バージョン
Ruby 3.4.1
Rails 7.2
MySQL 8.0
Vite 5.4
  • ローカル開発環境はMacを想定しており、Docker Desktop for Mac を利用
  • ホストOS側に Ruby の環境は整備しない

事前知識

アセットプリコンパイルの復習
  • Railsはフルスタックフレームワークを標榜しているのでフロントエンドも担える
    • RailsがAPIモードで動作していない時はフロントエンドの開発環境が有効
  • フロントエンドでの実装は、JavaScriptやCSSなどが絡む
  • アセットとは、JavaScript、CSS、画像、フォントなどの外部リソースのこと
  • アセットプリコンパイルとは、Rails がアセットを最適化して、本番環境で効率的に配信するためのプロセス
    • 最適化とは具体的に以下を意味する
      • 必要に応じて複数のファイルを 1 つにまとめる「統合」
      • ファイルサイズを削減するため、空白や改行を削除する「圧縮」
      • キャッシュ対策として、ファイル名に一意のハッシュを付加する「ハッシュ化」
    • 最適化によりリクエスト数の削減(10 個の JavaScript ファイル → 1 ファイルに結合して配信)、キャッシュ効率化、レスポンス速度の向上などの福利がある
    • 但し、前項は本番環境などの話で開発時は分けて管理した方が、実装の利便性が上がる
      • ファイルが分割されていることで保守性が上がる
        • 例えばViteは分割されたファイルの個々が編集された時にそれだけを読み込んでくれるホットリロードが効く
        • 圧縮されたコードなんてもちろんそのままじゃ読めないし・・・
    • したがって、開発時はファイルを分割し、プリコンパイルなどはスキップする、デプロイ時などではアセットプリコンパイルでまとめるということになる
Railsにおけるモジュールバンドリングツールやビルドツールとの統合
  • webpack[1] や vite[2] などのビルドツールがある
  • これらをRailsのアセットプリコンパイルと統合するために、webpacker や vite_rails などのラッパーを用いる
  • 統合とは具体的には以下
    • vite_javascript_tagvite_stylesheet_tag のようなビューでアセットをレンダリングするためのヘルパーメソッドが利用できるする
    • Rakeタスクとのインテグレーションもあり、 assets:precompile を拡張してVite アプリのビルドもこのタスクで実行されるようにする

Railsアプリの初期化

`rails new` 実行までの手順

リポジトリ初期化

$ cd /path/to/workdir
$ mkdir rails_vite_example
$ cd $_
$ git init
$ git commit -m 'first commit' --allow-empty

Docker関連のファイルの追加

以下のファイルを各々追加する

Dockerfile.dev
FROM ruby:3.4

RUN mkdir -p /app
WORKDIR /app

RUN gem install rails -v 7.2
RUN apt-get update -qq && apt-get install -y nodejs npm
docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    networks:
      - rails-vite-network
    ports:
      # 自由に利用できるとされる動的・プライベート ポート番号は 49152–65535 らしいので、これらのうち5万番を利用する
      # 以下、ウェルノウンポート番号 + 50000 という規約で設定していく
      - "53000:3000"
    volumes:
      - .:/app
      - gem-cache:/usr/local/bundle
      - node-modules-cache:/app/node_modules
    working_dir: /app
    stdin_open: true
    tty: true
    command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0"

volumes:
  gem-cache: {}
  node-modules-cache: {}

networks:
  rails-vite-network:
    driver: bridge

rails new 実行

$ docker compose run --rm app rails new . --skip-javascript --no-deps --skip-active-record
$ git add .
$ git commit -m 'rails new'
  • Rails7系から導入された importmap-rails などは、Vite で用いる ESBuild や Rollup と競合してしまうので、 --skip-javascript オプションを付加してインストールされないようにしている
  • ActiveRecord がない Rails アプリには多大な違和感があるが、今回は簡便のため --skip-active-record を付与した

※ 以降、 git commit の記載は省略する

アプリケーションサーバーの起動

docker compose up を実行するだけで http://localhost:53000/ にアクセスするとRailsの初期画面が表示されるようになる

$ docker compose up

vite_rails のインストールと初期化

  • 今回は vite_rails を採用する
  • 理由は、このようなラッパーを利用することで Vite に関する設定をスクラッチで行う必要がなくなるため

vite_rails provides similar functionality as webpacker does for webpack, without all the configuration overhead and dependencies.
cite: https://vite-ruby.netlify.app/guide/introduction.html#introduction

以下のコマンドで gem のインストールと初期化処理を行う

$ docker compose exec app bundle add vite_rails
$ docker compose exec app bundle exec vite install
Creating binstub
Check that your vite.json configuration file is available in the load path:

	No such file or directory @ rb_sysopen - /app/config/vite.json

Creating configuration files
Installing sample files
Installing js dependencies

changed 2 packages, and audited 33 packages in 1s

6 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Adding files to .gitignore

Vite ⚡️ Ruby successfully installed! 🎉

上記を実行すると、設定ファイルが生成されたり、レイアウト(app/views/layouts/application.html.erb) にヘルパーメソッドが追加されたりする

$ git status                               
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   .gitignore
	modified:   Gemfile
	modified:   Gemfile.lock
	modified:   app/views/layouts/application.html.erb
	modified:   config/initializers/content_security_policy.rb
	modified:   node_modules/.package-lock.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	Procfile.dev
	app/frontend/
	bin/vite
	config/vite.json
	package-lock.json
	package.json
	vite.config.ts

no changes added to commit (use "git add" and/or "git commit -a")

vite.json にはソースコードディレクトリがどこかや、エントリーポイントをどのサブディレクトリに配置するのかなどを設定する

デフォルトではソースコードディレクトリは app/frontend で、エントリーポイントはソースコードディレクトリからの相対パスで entrypoints となる

https://vite-ruby.netlify.app/guide/development.html#entrypoints-⤵️

実際に初期化後は以下のようなディレクトリレイアウトになっている

$ tree app/frontend            
app/frontend
└── entrypoints
    └── application.js

レイアウトの差分は以下

app/views/layouts/application.html.erb
     <link rel="icon" href="/icon.svg" type="image/svg+xml">
     <link rel="apple-touch-icon" href="/icon.png">
     <%= stylesheet_link_tag "application" %>
+    <%= vite_client_tag %>
+    <%= vite_javascript_tag 'application' %>
+    <!--
+      If using a TypeScript entrypoint file:
+        vite_typescript_tag 'application'
+
+      If using a .jsx or .tsx entrypoint, add the extension:
+        vite_javascript_tag 'application.jsx'
+
+      Visit the guide for more information: https://vite-ruby.netlify.app/guide/rails
+    -->
+
   </head>
 
   <body>

vite_client_tag はHMR(Hot Module Replacement)を有効にするためのヘルパー

vite_client_tag: Renders the Vite client to enable Hot Module Reload
cite: https://vite-ruby.netlify.app/guide/rails.html#enabling-hot-module-reload-🔥

vite_javascript_tag はエントリーポイントをロードするためのヘルパーで、今回だと app/frontend/entrypoints/application.js が読み込まれる

HMR(Hot Module Replacement)の有効化

Vite 開発サーバー用のコンテナを追加

前節で見たように、HMRを有効にするためのヘルパーは追加されている
ただ、これだけではHMRが作用しないので、Viteの開発サーバーを起動してアプリのフロントエンド(ブラウザ)がVite側に疎通できるように設定する必要がある

まず、docker-compose.yml を以下の内容に置き換える

docker-compose.yml
x-app-base: &app-base
  build:
    context: .
    dockerfile: Dockerfile.dev
  networks:
    - rails-vite-network
  volumes:
    - .:/app
    - gem-cache:/usr/local/bundle
    - node-modules-cache:/app/node_modules
  working_dir: /app
  stdin_open: true
  tty: true

services:
  app:
    <<: *app-base
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      # 自由に利用できるとされる動的・プライベート ポート番号は 49152–65535 らしいので、これらのうち5万番を利用する
      # 以下、ウェルノウンポート番号 + 50000 という規約で設定していく
      - "53000:3000"
    command: bash -c "rm -f tmp/pids/server.pid && rails s -b 0.0.0.0"
    environment:
      VITE_RUBY_HOST: "vite"

  vite:
    <<: *app-base
    ports:
      - "3036:3036"
    environment:
      VITE_RUBY_HOST: 0.0.0.0
    depends_on:
      - app
    command: bin/vite dev

volumes:
  gem-cache: {}
  node-modules-cache: {}

networks:
  rails-vite-network:
    driver: bridge

これで docker compose down/up を実行する

$ docker compose down
$ docker compose up

これで Vite の開発サーバーが起動する

今回は設定ファイル等は初期化処理実行後から編集していないので http://localhost:3036/vite-dev/ にブラウザからアクセスすれば以下のような画面が表示される

動作確認

適当にアクションを作る

$ docker compose exec app bin/rails g controller home index

これをルートに割り当てる

config/routes.rb
 Rails.application.routes.draw do
+  root "home#index"
   # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
 
   # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.

エントリーポイントを適当に以下のように編集する[3]

app/frontend/entrypoints/application.js
document.addEventListener('DOMContentLoaded', () => {
  document.body.innerHTML = '<h1>Vite ⚡️ Rails</h1>'
})

こうすると、トップページのレンダリングは以下のようになる

文字が小さくて恐縮だが、以下の画像のようにVSCodeでJavaScriptを編集して保存したら、自動的にブラウザの表示内容も更新されるようになる(速い⚡️)

脚注
  1. webpackはビルド時に全ての依存環境を解消しなければならないのでアプリケーション全体を走査してバンドルするからオーバヘッドが大きい ↩︎

  2. Viteはノーバンドルツール(全てをバンドルするのではなく、ESModulesのimportでソースコードを読み込む)だから早いとされている ↩︎

  3. 通常はエントリーポイントでこのようなコーディングをすることはなく、ソースコードディレクトリに配置したモジュールに対するimport 文の羅列になると思うが、ここでは動作確認の簡略化のためこのようにした ↩︎

Discussion