Rails 从 ActiveAdmin 中移除 Turbolink 拥抱 Turbo

lanzhiheng · 2021年05月30日 · 最后由 lanzhiheng 回复于 2021年05月31日 · 913 次阅读

这篇文章会以 ActiveAdmin 为例,简单聊聊怎么让那些依赖 Turbolink 的老项目投入到 Turbo 的怀抱去。https://www.lanzhiheng.com/posts/remove-turbolink-embrace-turbo-in-activeadmin


可能我们的项目的背景不同,技术栈不同,不过我相信思路是大同小异的。

其实删除 Turbolink 并非我本意,只不过是安装turbo-rails的时候,它自动帮我给删除掉了。因为Turbolink本身已经不维护了,它的功能已经被迁移到Turbo中,为了避免干扰,默认安装脚本

./bin/rails turbo:install

会帮我们移除掉Turbolink相关的依赖。对于一个洁净项目来说当然没啥大问题,然而对于开发了一段时间的项目来说可不单单是换个依赖就完了。下面简单总结一些注意事项。

Rails < 6.1

针对一些还没升级到 Rails6.1 的老项目来说,官方有这么一个指引

Rails UJS includes helpers for sending links and forms over XMLHttpRequest, so you can respond with Ajax. Turbo supersedes this functionality, so you should ensure that you're either running Rails 6.1 with the defaults that turn this off for forms, or that you add config.action_view.form_with_generates_remote_forms = false to your config/application.rb.

因为那些低于 6.1 的版本,每次渲染 form 表单都会有data-remote="true"这样的属性。Rails UJS 会根据这个属性来完成一些定制的动作,这可能会跟 Turbo 做的事情相冲突。所以如果引入 Turbo 之后,我们并不需要为 form 表单追加data-remote属性了。在 Rails6.1 之后已经默认关闭掉该属性。只是 Rails 低于 6.1 的版本还是需要手动去设置一下

module Stone
  class Application < Rails::Application
    # ....
    config.action_view.form_with_generates_remote_forms = false
    # ....
  end
end

JS 相关改动

1. 事件注册

在一般的项目中,等待 DOM 节点加载完成之后再运行 JS 相关的代码我们需要

document.addEventListener('DOMContentLoaded', (event) => {
  // 你的JS代码
})

然而,如果是引入了 Turbolink 的老项目的话那么你的代码大概是这样的

document.addEventListener('turbolink:load', (event) => {
  // 你的JS代码
})

这是 Turbolink 预设的加载事件。现在正常来说我们的 Turbolink 已经被移除了,换成了 Turbo。上述的 JS 代码只需要这样改改即可

document.addEventListener(turbo:load', (event) => {
  // 你的JS代码
})

2. 不同的打包工具

现在在 Rails 里面使用的打包工具,总的来说就两种Webpacker以及Sprocket。而 Webpacker 算是比较现代化的工具了,他能够很方便地集成各种 JS 生态的插件,简化(这个我不太确定)开发者的开发过程。

而 Turbo 在两个工具中的引入方式有些许不同

1). Webpacker 相关

相关文档写得比较清楚,在Webpack的环境下,我们只需要简单地

import { Turbo } from "@hotwired/turbo-rails"
window.Turbo = Turbo

把 Turbo 暴露在全局环境中就可以使用 Turbo 的功能了,这确实是简单粗暴的方式,个人也很喜欢这种方式。

2). Sprocket 相关

然而在传统的依赖了Sprocket的 Asset Pipline 流程中,就没那么简单了。由于一般来说Sprocket不会集成预编译,所以引入第三方 JS 包的时候,只要文件里面含有import, export这些关键字就会报错。

比如传统的 ActiveAdmin 中,直接

// active_admin.js

//= require jquery
//= require active_admin/base
//= require turbolink

改成

// active_admin.js

//= require jquery
//= require active_admin/base

// 删除
// ......require turbolink

import { Turbo } from "@hotwired/turbo-rails"
window.Turbo = Turbo

是不行的,因为源码里面包含了浏览器不认的字样export

export.png

幸好 Turbo 官方还为我们提供了 es5 的预编译版本,因此可以这样

// active_admin.js

//= require jquery
//= require active_admin/base

// 删除
// ......require turbolink
// 替换成
//= require @hotwired/turbo/dist/turbo.es5-umd.js

刷一下浏览器就会发现 ActiveAdmin 页面中也能访问 Turbo 这个常量了。

Turbo

这样一来 Turbolink 替换成Turo的工作已经完成,不过还有一些老的代码需要兼容一下。

请求重定向问题

正常的页面跳转是通过302来跳转,不过这种页面跳转都会是整页刷新,对于用户来说体验相当不好。如果想要用户体验更好一些,我们可以通过Ajax技术来更替页面内容来达到页面跳转的效果。这也是 Turbolink 或者最新的 Turbo 技术所干的事情。它们一旦被开启,只要关系到页面跳转他们会只更换body标签里面的内容。这样一来不用太多的努力也能让用户体验有很大程度的提升。

ActiveAdmin 里面如果引入了 Turbolink,也能实现类似的功能。不过有意思的是,就算我们用 JavaScript 来调用 ActiveAdmin 里面定义的接口,它也能检测到重定向并进行跳转,考虑下面代码

ActiveAdmin.register Backflow do
  member_action :sale, method: :put do
    resource.sale!
    flash['notice'] = '发布成功'
    redirect_to admin_root_url
  end
end
$.ajax({
  url: '/backflows/12/sale',
  method: 'PUT'
}).done(() => {}) // 居然重定向了

对于异步请求来说这是不可能的,我们要重定向到新的页面需要做类似这样的事情

window.location.href = '重定向url'

如果要实现 Turbolink 的跳转则要这样去做

Turbolinks.visit('重定向url', { action: 'replace' })

秘密在turbolink-rails这个仓库的Turbolinks::Redirection文件中,这也是 Turbo 没有做的事情。

因此在把 Turbolink 替换成 Turbo 之后,ActiveAdmin 里面只要自行编写 JS 代码去调用的POST, PUT, DELETE这类请求,并期望重定向的场景,它们重定向都会失败。你不会愿意重写 ActiveAdmin 里面所有包含上述逻辑的方法。后来在 Github 上找到了解决方案,只需要提供一个类似 Turbolink 的重定向逻辑就行,下面是我的代码片段

# frozen_string_literal: true
# config/initializers/active_admin_helpers.rb

ActiveAdmin::BaseController.class_eval do
  # Copy from Turbolink https://github.com/turbolinks/turbolinks-rails/blob/master/lib/turbolinks/redirection.rb
  def redirect_to(url = {}, options = {})
    turbo = options.delete(:turbo)

    super.tap do
      visit_location_with_turbo(location, turbo) if turbo != false && request.xhr? && !request.get?
    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

原理就是,在 ActiveAdmin 中,只要是通过 Ajax 发出的请求,并且不是 Get 请求,且 Turbo 已经开启,这个时候重定向的结果我们会用下面的 JavaScript 代码代替

Turbo.clearCache()
Turbo.visit(redirect_url, { action: 'replace' })

前端的 JavaScript 接收到这堆代码就会自动执行重定向操作,几乎不需要重写之前的任何逻辑了,爽歪歪。

尾声

这篇文章主要简单总结了一下,针对“历史悠久”的 ActiveAdmin 项目要如何移除 Turbolink 并替换成最新的 Turbo。因为有很多 ActiveAdmin 项目用的都是老的打包方式,以及比较老的写法,要做完全迁移需要花费多点功夫。简单分享了一下自己的经验,希望能有些帮助。

谢谢分享,最后的重定向好方便,不用改老代码了。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号