Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: rails-ujs->Turboアップグレードガイド、RubyとWeb Componentsほか(2022017前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

今回は以下の差分のうちChangelogが変更されているものを見繕いました。

🔗 Active Jobのシリアライズテストの失敗時にdiscard_onretry_onを利用できるよう修正

従来のperform_enqueued_jobsでは、deserialize_arguments_if_neededperform_nowを呼び出す前に呼び出されていた。レコードが既に存在せず、GlobalIDでシリアライズされている場合だと、perform_nowを呼び出す前にActiveJob::DeserializationErrorエラーが発生していた。このため、ジョブでdiscard_onretry_onのロジックがテストしにくくなっていた。
この修正では、deserialize_arguments_if_needed呼び出しがperform_now呼び出しまで延期されるようになる。

class UpdateUserJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError

  def perform(user)
      # ...
   end
end

# テストコード
User.destroy_all
assert_nothing_raised do
  perform_enqueued_jobs only: UpdateUserJob
end
assert_no_enqueued_jobs

従来は上のテストコードが失敗したが、修正によってパスするようになる。
同PRより


つっつきボイス:「Active Jobでデシリアライズ時にGlobalIDを解決できないとエラーが発生してリトライや破棄ができなかった問題が修正されたんですね」「本来こうあるべき挙動という感じですね」

# activejob/lib/active_job/test_helper.rb#L663
      def flush_enqueued_jobs(only: nil, except: nil, queue: nil, at: nil)
        enqueued_jobs_with(only: only, except: except, queue: queue, at: at) do |payload|
          queue_adapter.enqueued_jobs.delete(payload)
          queue_adapter.performed_jobs << payload
-         instantiate_job(payload).perform_now
+         instantiate_job(payload, skip_deserialize_arguments: true).perform_now
        end.count
      end
# ...
-     def instantiate_job(payload)
+     def instantiate_job(payload, skip_deserialize_arguments: false)
        job = payload[:job].deserialize(payload)
        job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
-       job.send(:deserialize_arguments_if_needed)
+       job.send(:deserialize_arguments_if_needed) unless skip_deserialize_arguments
        job
      end

rails/globalid - GitHub

🔗 update_attribute!が追加

新しいActiveRecord::Persistence#update_attribute!メソッドを追加する。update_attributeと似ているが、saveではなくsave!を呼び出す。update_attribute!メソッドはbefore_*コールバックが:abortをスローしたときにActiveRecord::RecordNotSavedエラーをraiseする。

class Topic < ActiveRecord::Base
  before_save :check_title

  def check_title
    throw(:abort) if title == "abort"
  end
end

topic = Topic.create(title: "Test Title")
# #=> #<Topic title: "Test Title">
topic.update_attribute!(:title, "Another Title")
# #=> #<Topic title: "Another Title">
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved

同PRより


つっつきボイス:「!付きのActiveRecord::Persistence#update_attribute!メソッドが追加された」「!なしのupdate_attributeはエラーをraiseしないのか」「たしかそうですね」「それなら整合性の面でも!ありのメソッドはあっていいですね」「ちなみにsavesave!の違いもエラーをraiseするかどうかですね」「普段updateしか使ってなかったのでupdate_attributeの挙動を知らなかった」「そういえば私もupdateしか使ってなかった」

🔗 Relation#pretty_printのeager loadingを回避

#inspectの振る舞いにならって、レコードが読み込み済みでない場合は要素を最大11個までしかフェッチしないようにした。
同PRより


つっつきボイス:「Relation#pretty_printで出力が大量にならないように11個までしかフェッチしないようにしたみたいですね」「お〜、これはいい👍」「あ、なるほど、自分もppをよく使うので助かります」「rails consoleで不用意にppして大量の出力でコンソールが固まらずに済む」

# activerecord/lib/active_record/relation.rb#L776
-   def pretty_print(q)
-     q.pp(records)
+   def pretty_print(pp)
+     subject = loaded? ? records : annotate("loading for pp")
+     entries = subject.take([limit_value, 11].compact.min)
+
+     entries[10] = "..." if entries.size == 11
+
+     pp.pp(entries)
    end

🔗 QueryMethods#in_order_ofの挙動をEnumerableに合わせた

in_order_ofが、Enumerable版のin_order_ofの挙動に合わせて、指定された値のリストでフィルタされるようになった。
これは#43916のコメントに対応したもの。振る舞いはEnumerable版に合うようになったが、リリース後の移行パスをどうすればよいかまだわからない。バグとみなしてパッチリリースの対象にする?
同PRより


つっつきボイス:「Enumerablein_order_ofに合わせて、リストの値にない値を含めないようになったようですね」

# activerecord/lib/active_record/relation/query_methods.rb#L439
    def in_order_of(column, values)
      klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
      return spawn.none! if values.empty?

      references = column_references([column])
      self.references_values |= references unless references.empty?

-     if values.empty?
-       spawn.order!(column)
-     else
+       values = values.map { |value| type_caster.type_cast_for_database(column, value) }
+     values = values.map { |value| type_caster.type_cast_for_database(column, value) }
-     arel_column = column.is_a?(Symbol) ? order_column(column.to_s) : column

-       arel_column = column.is_a?(Symbol) ? order_column(column.to_s) : column
-       spawn.order!(connection.field_ordered_value(arel_column, values), column)
-     end
+     spawn
+       .order!(connection.field_ordered_value(arel_column, values))
+       .where!(arel_column.in(values))
    end

🔗 Rails 7.1からconfig.add_autoload_paths_to_load_pathがデフォルトで無効になる

参照: 668673f
現在のオートローダーはZeitwerkのみとなったので、オートロード済みのパスを$LOAD_PATHに追加する理由がなくなった(7.0までにやれればよかったのだけど)。
@fxn 問題がなさそうならメンションくれればマージできます。
同PRより


つっつきボイス:「config.add_autoload_paths_to_load_path、長い設定名だな〜」「Railsガイドを7に更新翻訳したときにこの設定を見かけました」「今まではオートロードしたパスを$LOAD_PATHにしこしこコピーしていたけど、Rails 7がZeitwerkのみになって不要になったので、このプルリクでそれをデフォルトで無効にしようということか、なるほど」

参考: 3.1.5 config.add_autoload_paths_to_load_path
-- Rails アプリケーションを設定する - Railsガイド

$LOAD_PATHにオートロードパスを足すべきかどうかを指定します。このフラグはデフォルトでtrueですが、:zeitwerkモードでは早い段階でconfig/application.rbでfalseに設定することをおすすめします。Zeitwerkは内部で絶対パスが使われ、:zeitwerkモードで動作するアプリケーションではrequire_dependencyが不要なので、モデルやコントローラやジョブなどが$LOAD_PATHに存在する必要はありません。これをfalseに設定すると、requireの解決が相対パスで呼び出されるときにRubyがそれらのディレクトリのチェックを削減でき、それらのインデックスの構築が不要になるのでBootsnapの動作やメモリも節約できます。
Railsガイドより

「昔の記事によくあった『まずオートロードパスを追加しましょう』みたいなことはもうしなくていいんだ」「Zeitwerkになってから、appディレクトリの下に置いたものは何もしなくてもオートロードされるようになりましたよね」「そうそう」

参考: 定数の自動読み込みと再読み込み (Zeitwerk) - Railsガイド

🔗 6.1リリースノートにAction Mailboxの非推奨事項を追加

6.1リリースノートを更新して、Action Mailboxの非推奨項目を追加した。6.1のChangelogには以下が記載されているが、Railsguidesのリリースノートに含まれていなかったので。

Deprecate Rails.application.credentials.action_mailbox.api_key and MAILGUN_INGRESS_API_KEY in favor of Rails.application.credentials.action_mailbox.signing_key and MAILGUN_INGRESS_SIGNING_KEY.

同PRより


つっつきボイス:「過去のリリースノートに追記するのが珍しいと思ったので取り上げてみました」「よくぞ抜けに気づきましたよね」「もしかして自分で踏んだからプルリクあげたのかも?」

🔗 デフォルトのヘッダーからX-Download-Optionsを削除

X-Download-Optionsは、deprecation間近のInternet Explorerでフィッシング攻撃を防ぐためにしか使われていないので、デフォルトのヘッダーから削除するのが筋。
解決されるissue: #43948
新しいデフォルトヘッダーはRails 7.1からフレームワークのデフォルトになる。
同PRより


つっつきボイス:「X-Download-OptionsがIEでしか使われないヘッダーだから消しましょうということだそうです」「IEだけなら消すのが順当でしょうね」

# guides/source/security.md#L1038
config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '0',
  'X-Content-Type-Options' => 'nosniff',
- 'X-Download-Options' => 'noopen',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin'
}

X-Download-Optionsの解説↓を見ると、アプリケーションからダウンロードしたファイルをブラウザのファイルオープンダイアログに表示しないようにするためのヘッダーなのか」

参考: HTTP headers - HTTP | MDN

X-Download-Options
The X-Download-Options HTTP header indicates that the browser (Internet Explorer) should not display the option to "Open" a file that has been downloaded from an application, to prevent phishing attacks as the file otherwise would gain access to execute in the context of the application. (Note: related MS Edge bug )
developer.mozilla.orgより

🔗Rails

🔗 RubyとWeb Componentsを連携させる(Ruby Weeklyより)


つっつきボイス:「Bridgetownというstatic site generatorにRubyコードベースのテンプレートをWeb Componentsにmappingする機能があって、記事中前半のコードはこのBridgetownのWeb Components連携機能のようですね」「なるほど」「Star Rating Componentから先がViewCompoment gemを使ったRailsでの実装の話のようです: ViewComponentを素で使うだけじゃなく、Ruby2JSを使ってRubyでWeb ComponentsのJS classを定義している感じ」

参考: Ruby Components | Bridgetown

bridgetownrb/bridgetown - GitHub

参考: Web Components | MDN
参考: Ruby2JS: an extensible Ruby to modern JavaScript transpiler

# 同記事より
# src/_components/note.rb
class Note < Bridgetown::Component
  def initialize(type: :primary, icon: nil)
    @type, @icon = type.to_sym, icon
  end

  def icon
    return @icon if @icon

    case @type
    when :primary
      "system/information"
    when :warning
      "system/alert"
    end
  end
end

「拡張子はserbなのか」「SerbeaというERBのオルタナらしい↓」「Serbea自体はテンプレートエンジンのようですね」

参考: Serbea: Similar to ERB, Except Awesomer

「記事の後半でViewComponent(ウォッチ20200330)にも触れていますが↓、ViewComponent gem自体にはWeb Componentsの統合の機能はなくて、この記事ではLit&Ruby2JSを使ってViewComponentの吐き出すHTMLをRubyで書いてRuby2JSに変換したJS classでうまいことWeb Componentsとして連携させている、という感じですね」「なるほど」

参考: Overview - ViewComponent
参考: Lit
参考: 今後Railsで話題になるかもしれないViewComponentを試した | Webuilder240.com
参考: View Componentのすすめ①

github/view_component - GitHub

# viewcomponent.orgより
# app/components/message_component.rb
class MessageComponent < ViewComponent::Base
  def initialize(name:)
    @name = name
  end

  def call
    @output_buffer.safe_append='<h1>Hello, '.freeze
    @output_buffer.append=( @name )
    @output_buffer.safe_append='!</h1>'.freeze
    @output_buffer.to_s
  end
end

🔗 Turbo関連記事

つっつきボイス:「Turboについて調べていて、いくつか目についた記事です: 1つめは上のアップグレードガイドです」「rails-ujsやTurbolinksからTurboへのアップグレードガイドか」「ドキュメントの量はそんなに多くない感じです」

「rails-ujsが担当していた機能はそんなに多くないので、考慮するものの種類は少ないんですが、rails-ujsが使われているUIの数は少なくないんですよね」「たしかに」「あって当然の機能だっただけに、完全に移行したことを確認する工数はかかりそう」


「2つめは、この間のjnchitoさんの記事でも扱われていたstatus: :unprocessable_entityを付ける話や、turbo-frameを制御する方法などの記事ですね」


「3つ目は1年前の記事ですが、今になって気づきました↓」「お、これは読んだことあったかも」「Hotwireを開発のプログレッシブエンハンスメントという観点から説明してくれているのが自分にとってありがたかったです」

参考: Progressive Enhancement (プログレッシブエンハンスメント) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

「RailsとHotwire関連はこの半年ぐらいで大きく進みましたよね」「そうそう」「記事はその前に書かれているので、変更点に注意して読めば大丈夫そう👍」

🔗 DeviseのRails 7対応状況


つっつきボイス:「DeviseにRails 7のサポートが入りましたけど↓、上のChangelogではまだTurboの対応は完全ではないとありました」「自分はDeviseのデフォルトビューをほとんど使ってませんけどね」

「ちなみにDeviseに5-rcブランチがあったのを見かけたんですが、2019年からそのままでした」「Deviseの開発体制はわからないけど、とりあえず作ったものっぽいのでまた作り直すんじゃないかな」「そうかもしれませんね」

🔗 その他Rails

つっつきボイス:「jnchitoさんがQiitaのAdvent Calendar 2021 Online Meetupと銀座Rails#41に登壇するそうです」「そうそう、例の点字メーカープログラム↓に参加した人は銀座Railsでjnchitoさんが公開レビューしてくれますよ」

JunichiIto/tenji-maker-challenge-for-ginza-rails - GitHub

参考: Qiita Advent Calendar Online Meetup - connpass
参考: 【オンライン開催】銀座Rails#41 - connpass


以下はつっつき後のツイートです。おめでとうございます🎉


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rails 7をRuby 3.1で動かす、クックパッドのRuby 3.1解説記事、Rails 6->7更新ほか(20220112)

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。