Rail7の推奨になったimportmap+ dartsassを使ってサービスを構築してみました

Ruby on Rails

概要

  • Ruby on Rails Advent Calendar 2022の17日目の記事です。
  • Ruby on Rails 7.0からアセット管理にimport mapsが使われるようになりました。Rails6のときに使われていたwebpackerとは大きく仕組みが異なります。
  • import mapsの学習のためにrails newから始めて、1つサービスを構築して公開してみました。
  • 個人的には日本や世界のDXをに役立ったり、生活を便利にするサービスを作ることを片手間におここなっています。今回もその一環です。

今回作ったサイト

「元号くん」という名称で、西暦から和暦を表示したり、和暦から西暦を表示できます。また、各年ごとの祝日の情報を表示しています。

ただ表示するだけではRailsを使う意味があまりないのでREST APIでも同様の変換を提供できるようにしました。

サイトのURLは以下になります。

トップページのスクリーンショットは以下です。

importmapとは

すでに解説記事が多数あるのでそちらを参照するのが良いです。こちらの記事がとても理解しやすかったです。

JSのmoduleをimportするときに、今まではブラウザ上では相対パスでしか指定できなかったけど、importするmoduleとしてnode_moduleやURLなど色々指定できるようにする機能というかんじです。なるほど。

importmapの対応ブラウザ

2022年12月14日時点でグローバルで72.9%です。古めのsafariだと対応してい無さそうです。

https://caniuse.com/import-maps

サービス構築

要件定義

  • importmapを使う。
  • REST APIからも利用できるようにする。
  • すでに動いている自宅サーバで公開できるようにすることでサーバ費用はほぼ0円で運用する。

外部設計

大したサービスではないのでページ階層を書いておきます。

  • トップページ
    • 西暦→和暦 一覧
      • 各西暦のページ
    • 和暦→西暦 一覧
      • 和暦のページ
    • 年ごとの休日一覧
      • 年ごとのページ

内部設計

  • 西暦と和暦を変換するgemであるwarekiを使えばかんたんに実装できそうです。
  • 休日のマスタはholiday_jpを使えばかんたんに実装できそうです。
  • RDBなどのストレージは使わないで運用できそうなのでストレージの設計は無いです。

実装

まずはじめに開発サーバを作ります。dockerによるコンテナ上で開発をするので開発環境を準備します。

以下のファイルを準備します。

上記ファイルを準備した上で以下のコマンドを実行して、railsの実行にgemをインストールします。

% docker compose run --rm app bundle install

次に、railsの新規プロジェクトを作成します。なんら特殊なオプションを指定しないでrailsの推奨するテンプレで作っていきます。

% docker compose run --rm app bundle exec rails new . -C sqlite3

生成されたGemfileを覗いてみます。アセットに関連するgemは以下のものが指定されていました。

gem "sprockets-rails"
gem "importmap-rails"
gem "turbo-rails"
gem "stimulus-rails"

ここで、config/importmap.rb の初期設定を見てみます。ここに、ロードする可能性のあるJSの場所を一元管理しておくこととなります。

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"

1行目で書かれているapplication.jsにはstimulusの初期化が書かれています。

> cat ./app/javascript/controllers/application.js
import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

2〜4行目にあるhotwiredの部分では、turbo, stimulusのモジュール定義が入っています。

最後の行は、pin_all_fromと書かれており、controllers以下のファイルを自動的に展開してキーとして指定できるような定義が書かれています。

controllers以下のJSの中身は以下のようになっています。stimulusで書かれたJSのテンプレが入っています。stimulusを使わなければごそっと消しても良いかと思います。

> head -n 1000  ./app/javascript/controllers/*
==> ./app/javascript/controllers/application.js <==
import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = false
window.Stimulus   = application

export { application }

==> ./app/javascript/controllers/hello_controller.js <==
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.textContent = "Hello World!"
  }
}

==> ./app/javascript/controllers/index.js <==
// Import and register all your controllers from the importmap under controllers/*

import { application } from "controllers/application"

// Eager load all controllers defined in the import map under controllers/**/*_controller
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)

// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
// lazyLoadControllersFrom("controllers", application)

BootstrapのJSとCSSの追加

デザインはTwitter bootstrapを使うので追加していきます。

まずは、今回のメインであるimportmapを使ったJSのロード部分です。まずは、キーを追加します。今回デフォルトで指定されているファイルをそのまま使います。minifyされたファイルのURLが指定されています。

また、bootstrapが依存しているpopperjsも自動的に追加されました。

% docker compose run -- app bin/importmap pin bootstrap
Pinning "bootstrap" to https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap.esm.js
Pinning "@popperjs/core" to https://ga.jspm.io/npm:@popperjs/[email protected]/lib/index.js

実行後のconfig/importmap.rbがこちら。

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"
pin "bootstrap", to: "https://ga.jspm.io/npm:[email protected]/dist/js/bootstrap.esm.js"
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/[email protected]/lib/index.js"

次に、上記の定義をもとに app/javascript/application.js で読み込みます。最後の2行を追加します。

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails"
import "controllers"

import * as Popper from "@popperjs/core"
import * as Bootstrap from "bootstrap"

次に、stylesheetです。railsの推奨はdartsassなので、dartsass-railsをインストールします。

% docker compose run --rm app bundle add dartsass-rails
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
<snip>
Fetching dartsass-rails 0.4.0
<snip>
Installing dartsass-rails 0.4.0

そしてインストールを実行

% docker compose run --rm app bundle exec rails dartsass:install
Build into app/assets/builds
      create  app/assets/builds
      create  app/assets/builds/.keep
      append  app/assets/config/manifest.js
Stop linking stylesheets automatically
        gsub  app/assets/config/manifest.js
      append  .gitignore
Add default app/assets/stylesheets/application.scss
      create  app/assets/stylesheets/application.scss
Add default Procfile.dev
      create  Procfile.dev
Ensure foreman is installed
         run  gem install foreman from "."
Fetching foreman-0.87.2.gem
Successfully installed foreman-0.87.2
1 gem installed
Add bin/dev to start foreman
      create  bin/dev
Compile initial Dart Sass build
         run  rails dartsass:build from "."
+ /usr/local/bundle/gems/dartsass-rails-0.4.0/exe/aarch64-linux/sass --style\=compressed --no-source-map --load-path /app/app/assets/stylesheets --load-path /app/app/assets/builds --load-path /app/app/assets/config --load-path /app/app/assets/images --load-path /app/app/assets/stylesheets --load-path /usr/local/bundle/gems/stimulus-rails-1.2.1/app/assets/javascripts --load-path /usr/local/bundle/gems/turbo-rails-1.3.2/app/assets/javascripts --load-path /usr/local/bundle/gems/importmap-rails-1.1.5/app/assets/javascripts --load-path /usr/local/bundle/gems/actiontext-7.0.4/app/assets/javascripts --load-path /usr/local/bundle/gems/actiontext-7.0.4/app/assets/stylesheets --load-path /usr/local/bundle/gems/activestorage-7.0.4/app/assets/javascripts --load-path /usr/local/bundle/gems/actionview-7.0.4/lib/assets/compiled --load-path /app/app/javascript --load-path /app/vendor/javascript /app/app/assets/stylesheets/application.scss:/app/app/assets/builds/application.css

次に、CSSのファイルを管理するためにnpm経由でcssを取得します。今までなら、npmによってJSとCSSを同時に取得していましたが、管理が別々になっちゃいます。

dartsass自体はnodeJS無しで実行できますが、sassファイルの管理自体はnpm経由が私は楽だと思うので結局はnodeJSを使ってしまっています。

npm-check-updatesによってパッケージの更新がわかるので楽です。

package.json

{
  "dependencies": {
    "bootstrap": "^5.2.3",
    "bootstrap-icons": "^1.10.2",
    "bootswatch": "^5.2.2"
  },
  "devDependencies": {
    "npm-check-updates": "^16.4.3"
  },
  "license": "UNLICENSED"
}

app/assets/stylesheets/application.scssにファイルを読み込むように書きます。

@import "../../../node_modules/bootswatch/dist/flatly/variables";
@import "../../../node_modules/bootstrap/scss/bootstrap.scss";
@import "../../../node_modules/bootswatch/dist/flatly/bootswatch";
@import "../../../node_modules/bootstrap-icons/font/bootstrap-icons";

これでJSの読み込みができつつ、デザインが反映されるようになります。

その後は、dartsassによるファイル変更監視などを設定して、いつもどおりrailsアプリ開発と同じです。

テスト

個人サービスだし、変更はほとんど行わないので省略。

考察

lighthouseで高スコア

95点です。ユーザ体験も大きく向上できます。

popperのファイル読み込みが多数出るようになった

importmapの前提としては、HTTP2の利用があるので大した問題ではないかもしれませんが、細かく分割されたファイルが読み込まれるようになりました。全部popper関連です。流石に分割され過ぎのような気もしますが、キャッシュ効率を考えるとこれが良いのでしょう。

Google Spreadsheetで和暦表示が可能になります

Google Spreadsheetでは和暦を表示できません。Google App Scriptで西暦から和暦への変換プログラムを書いてよいのですが、なかなか複雑なので今回使ったサービスを使って和暦を表示してみたいと思います。

また、自分でロジックを書くと新たな和暦が登場したときにメンテナンスが必要になるという問題も発生します。

Google SpreadsheetではGoogle App Scriptを使うとREST APIの呼び出しと、JSONのパースができるようになります。

出力例とサンプルファイルのURL↓

https://docs.google.com/spreadsheets/d/1DiEUrZNH3Y7jJNrryQmRn04eMQO4zpoSHzKJg05Km38/edit?usp=sharing

Google App ScriptでREST APIを呼び出します。

function getWarekiFromSeireki(year) {
  
  const url = 'https://seireki.teraren.com/seireki/' + year  + '.json';
  
  try {
    ContactsApp response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true});
    response_object = JSON.parse(response);
    return response_object['wareki'];
  }catch(e){
    Logger.log(e);
  }
}

日付から、西暦を出力してあげたほうが親切かなと思いました。

その他

Webサービスの公開に際して以下のことも行っています

まとめ

  • rails7 + importmap-rails + dartsass-railsを使って最新の推奨組み合わせを使ってサイトを構築してみました。
  • かかった時間は8時間ぐらいです。この記事を書くのに2時間ぐらいです。

コメント