- Ruby / Rails関連
Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳)
-
シェア
-
ツイート
-
Turboアップグレードガイド(翻訳)">ブックマーク
-
LINE
Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳)
Turboは、従来Rails UJSが行っていたリンク変更やフォーム送信をXMLHttpRequests
に変える機能を置き換えます。Rails UJSおよびTurbolinksからTurboへの移行を完了するには、アプリケーションのconfig/application.rb
でconfig.action_view.form_with_generates_remote_forms = false
を設定する必要があります。しかし、すべてのアプリケーションが一挙に移行可能とは限りません。また、Rails UJSをTurboと共存させる必要が生じることもあるでしょう。そのために必要な手順を以下に示します。
1. Rails UJSでTurbo互換のセレクタを使うようにする
Rails UJSは、Railsフレームワークで直接提供されるか、さもなければ古いjquery-ujsプラグインによって提供されます。どちらもそのままにできますが、Turboと互換性のあるバージョンにアップグレードするか(#42476を参照)、アプリで必要なJavaScriptファイルをベンダリングして自分で調整する必要があります。
訳注
Rails UJSで書かれたアプリケーションをHotwireの新しいTurboフレームワークに移行する場合、移行の間(あるいは今後もずっと)TurboとUJSを共存させたい場合もあります。そのためには、フォーム送信の扱いを区別する手段が必要です。フォームで
data-turbo=true
を指定すると、Rails UJSはそのフォームを処理せずにTurboに処理を任せるようになります。
#42476より
2. Gemfile内のturbolinks gemをturbo-railsに置き換える
Gemfile内のgem 'turbolinks'
を消し去ってgem 'turbo-rails'
に置き換えたいことでしょう。しかし、UJSが発行する古いスタイルのXMLHttpRequestsがTurboでも動くようにするためには、古いTurbolinksの振る舞いを修正して、それらのリクエストをHTTP 302互換にする必要があります(Turbolinks呼び出しをTurbo呼び出しにする)。
まず、app/controllers/concerns/turbo/redirection.rb
ファイルを追加する必要があります。このconcernをApplicationController
内でTurbo::Redirection
としてinclude
してください。
module Turbo
module Redirection
extend ActiveSupport::Concern
def redirect_to(url = {}, options = {})
turbo = options.delete(:turbo)
super.tap do
if turbo != false && request.xhr? && !request.get?
visit_location_with_turbo(location, turbo)
end
end
end
private
def visit_location_with_turbo(location, action)
visit_options = {
action: action.to_s == "advance" ? action : "replace"
}
script = []
script << "Turbo.clearCache()"
script << "Turbo.visit(#{location.to_json}, #{visit_options.to_json})"
self.status = 200
self.response_body = script.join("\n")
response.content_type = "text/javascript"
response.headers["X-Xhr-Redirect"] = location
end
end
end
次に、test/helpers/turbo_assertions_helper.rb
ファイルを追加する必要もあります。このヘルパーをActionDispatch::IntegrationTest
にinclude
してください。
module TurboAssertionsHelper
TURBO_VISIT = /Turbo\.visit\("([^"]+)", {"action":"([^"]+)"}\)/
def assert_redirected_to(options = {}, message = nil)
if turbo_request?
assert_turbo_visited(options, message)
else
super
end
end
def assert_turbo_visited(options = {}, message = nil)
assert_response(:ok, message)
assert_equal("text/javascript", response.try(:media_type) || response.content_type)
visit_location, _ = turbo_visit_location_and_action
redirect_is = normalize_argument_to_redirection(visit_location)
redirect_expected = normalize_argument_to_redirection(options)
message ||= "Expected response to be a Turbo visit to <#{redirect_expected}> but was a visit to <#{redirect_is}>"
assert_operator redirect_expected, :===, redirect_is, message
end
# 「Content-Typeがtext/javascriptの非GETリクエスト」かどうかという
# 簡単なヒューリスティックでTurbolinksのリクエストを検出する
#
# 技術的にはTurbolinks-Referrerリクエストヘッダが設定されていることも
# チェックするが、そのためには`turbo:`オプションを指定して
# POSTやPATCHなどのテストメソッドのヘッダーをオーバーライドして渡す
# 必要がある
#
# `request.xhr?`チェックは利用できない
# (X-Requested-Withヘッダーは、後続のリクエストで漏洩しないよう
# コントローラのアクション実行後にクリアされるため)
def turbo_request?
!request.get? && (response.try(:media_type) || response.content_type) == "text/javascript"
end
def turbo_visit_location_and_action
if response.body =~ TURBO_VISIT
[ $1, $2 ]
end
end
end
3. packファイルに含まれるTurbolinksをTurboに置き換える
おそらくpackファイルにrequire("turbolinks").start()
のような記述があると思いますが、これをimport "@hotwired/turbo-rails"
に変更する必要があります。Turboはインポート時に自動的に起動されてwindow.Turbo
に割り当てられるので、何も起動する必要はありません。
4. Turbolinks名前空間をすべてTurbo名前空間に置き換える
document.addEventListener("turbolinks:before-cache" ...)
のようなイベント名にあるTurblinks名前空間は、Turboのturbo:before-cache
に置き換える必要があります。-
同様に、
Turbolinks.visit
呼び出しもTurbo.visit
に置き換える必要があります。 -
DOM要素の
data-turbolinks-action
などの属性も、data-turbo-action
のように置き換える必要があります。 -
data: { turbolinks: false }
もすべてdata: { turbo: false }
に置き換えることを忘れずに。
5. オプション: モバイルアダプタ向けの後方互換shimを提供する
Turbolinksモバイルアダプタを用いて構築したネイティブアプリも移行する必要がある場合は、Turbolinksの呼び出しをTurbo呼び出しに変換するshimが必要になるでしょう。Basecampでは以下のshimが必要でした。
// モバイルアプリ向けの互換性shim
window.Turbolinks = {
visit: Turbo.visit,
controller: {
isDeprecatedAdapter(adapter) {
return typeof adapter.visitProposedToLocation !== "function"
},
startVisitToLocationWithAction(location, action, restorationIdentifier) {
window.Turbo.navigator.startVisit(location, restorationIdentifier, { action })
},
get restorationIdentifier() {
return window.Turbo.navigator.restorationIdentifier
},
get adapter() {
return window.Turbo.navigator.adapter
},
set adapter(adapter) {
if (this.isDeprecatedAdapter(adapter)) {
// 古いモバイルアダプタはvisitProposedToLocation()をサポートしない
adapter.visitProposedToLocation = function(location, options) {
adapter.visitProposedToLocationWithAction(location, options.action)
}
// 古いモバイルアダプタはvisit.location.absoluteURLを利用するが、
// TurboではLocationクラスを廃止してDOM URL APIに変えたので利用できない
const adapterVisitStarted = adapter.visitStarted
adapter.visitStarted = function(visit) {
Object.defineProperties(visit.location, {
absoluteURL: {
configurable: true,
get() { return this.toString() }
}
})
adapter.currentVisit = visit
adapterVisitStarted(visit)
}
}
window.Turbo.registerAdapter(adapter)
}
}
}
// デスクトップアプリによってrequireされる
document.addEventListener("turbo:load", function() {
const event = new CustomEvent("turbolinks:load", { bubbles: true })
document.documentElement.dispatchEvent(event)
})
概要
MITライセンスに基づいて翻訳・公開いたします。