はじめに
私は、RailsでWebアプリを開発しているプログラマーです。
Webアプリで、インタラクティブな使用感を出したい時は、Hotwireを使っています。
Hotwireとは、レスポンスのHTMLでインタラクティブな使用感を実現するアプローチです。
このアプローチの実現は、TurboとStimulusによりできます。
Turboは、HTTPリクエストを自然に呼ぶだけで画面をインタラクティブに変更できるように設計されたライブラリです。
Stimulusは、HTMLを拡張するように設計されたJavaScriptフレームワークです。
どちらも違和感なく使えています。
これは開発者の設計が良いからでしょう。
このような使い勝手が良いものは、どのように作られているのだろうか?
気になったので、コードを1から読み解いていくことにしました。
RailsでStimulusを使いやすいようにするgemがあります。
手始めに、そのgemのコードを1から読み解きます。
stimulus-rails gem
stimulus-rails は、Stimulusを気軽に使えるようにするgemです。
stimulus-rails gemは以下の機能を持ちます。
- Stimulus インストーラー
- Stimulus ジェネレーター
このそれぞれの機能がどのようにして成り立っているか読み解きます。
Stimulus インストーラー
Stimulus インストーラーは、RailsアプリのJavaScriptの管理方法に適したStimulus環境を整えてくれるものです。
Railsアプリに stimulus-rails gemをインストールして、bin/rails stimulus:install
を実行すると、インストーラーは以下のことをします。
- JavaScriptの管理方法を判別する
- その結果に応じたStimulus環境を構築する
これらのことは、以下ファイルにより行われています。
https://github.com/hotwired/stimulus-rails/blob/main/lib/tasks/stimulus_tasks.rake
上記それぞれの仕組みを読み解きます。
JavaScriiptの管理方法を判別
stimulus-rails gemでは、JavaScriptの管理方法として以下を想定しています。
- import maps
- JavaScript bundler
管理方法の判別は、以下のコードで行なっています。
namespace :stimulus do
desc "Install Stimulus into the app"
task :install do
# import maps
if Rails.root.join("config/importmap.rb").exist?
Rake::Task["stimulus:install:importmap"].invoke
# JavaScript bundler on bun
elsif Rails.root.join("package.json").exist? && Stimulus::Tasks.using_bun?
Rake::Task["stimulus:install:bun"].invoke
# JavaScript bundler on yarn
elsif Rails.root.join("package.json").exist?
Rake::Task["stimulus:install:node"].invoke
else
puts "You must either be running with node (package.json) or importmap-rails (config/importmap.rb) to use this gem."
end
end
このコードから明らかなように、ファイルの有無によってJavaScriptの管理方法を判別しています。
config/importmap.rb
があったら、import mapsを使っていると判別しています。
Railsでimport mapsを使う際、importmap-rails gemが使用されます。
config/importmap.rb
は、このgemで使われるファイルです。
なので、このファイルがあれば、import mapsを使っていると判別しています。
JavaScript bundlerを使っているかは、package.json
の有無により判別しています。
JavaScript bundlerに関しては、JavaScript パッケージマネージャーとして何を使っているかも判別しています。
Stimulus::Tasks.using_bun? == true
の時に、パッケージマネージャーとしてbunを使っていると判別しています。
bun.config.js
があったら、using_bun? == true
になります。
bun.config.js
がない場合は、パッケージマネージャーとしてyarnを使っていると判別しています。
Stimulus環境を構築
上記の判別結果に則したRakeタスクを実行し、Stimulus環境を構築します。
例えば、import mapsの場合は、Rake::Task["stimulus:install:importmap"].invoke
を実行します。
これにより実行されるタスクは以下で、run_stimulus_install_template
メソッドを実行しているだけです。
namespace :install do
desc "Install Stimulus on an app running importmap-rails"
task :importmap do
Stimulus::Tasks.run_stimulus_install_template "stimulus_with_importmap"
end
run_stimulus_install_template
メソッドは、Rails アプリケーションテンプレートを適用します。
module Stimulus
module Tasks
extend self
def run_stimulus_install_template(path)
system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}"
end
例えば、import mapsの場合は以下のテンプレートが実行され、import mapsに適したStimulus環境を構築します。
https://github.com/hotwired/stimulus-rails/blob/main/lib/install/stimulus_with_importmap.rb
Stimulus ジェネレーター
Railsといえば、ジェネレーターです。
Stimulusにも、当然ジェネレーターがあります。
例えば、bin/rails g stimulus hello
を実行すれば、以下のファイルが作られます。
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="hello"
export default class extends Controller {
connect() {
}
}
ジェネレーターを定義しているファイルは以下です。
https://github.com/hotwired/stimulus-rails/blob/main/lib/generators/stimulus/stimulus_generator.rb
ジェネレーターの肝となっているのは、copy_view_files
メソッドです。
class StimulusGenerator < Rails::Generators::NamedBase # :nodoc:
source_root File.expand_path("templates", __dir__)
class_option :skip_manifest, type: :boolean, default: false, desc: "Don't update the stimulus manifest"
def copy_view_files
@attribute = stimulus_attribute_value(controller_name)
template "controller.js", "app/javascript/controllers/#{controller_name}_controller.js"
rails_command "stimulus:manifest:update" unless Rails.root.join("config/importmap.rb").exist? || options[:skip_manifest]
end
下記テンプレートをもとにして、ファイルを作成します。
https://github.com/hotwired/stimulus-rails/blob/main/lib/generators/stimulus/templates/controller.js.tt
JavaScriptの管理方法による違いは1つだけあります。
JavaScript bundlerを使っている時だけ、stimulus:manifest:update
を実行しています。
これにより、app/javascript/application.js
が更新されます。
Stimulusジェネレーターを使って作成したもの使えるようにするためです。
おわりに
コードを読むことで、JavaScriptの管理方法に適した環境をいい感じに整えてくれていることがわかりました。
環境の違いで色々とやらないといけないことが増えると、使うのが嫌になってきます。
Railsが重んじる「設定より規約」を体現している良いgemです。