Rails 关于 turbolinks:load 重复执行问题的研究

jicheng1014 · May 18, 2019 · Last by mfb777 replied at March 13, 2022 · 2394 hits

被朋友问到了关于 turbolinks:load 被各种页面运行了多次的问题 (他再 load 里绑了 ajax, 结果每个页面都请求), 自己好久没写 rails 的前端,还是抽空解答了一下

感觉这个东西还是有部分朋友没弄明白,正好写篇帖子,自己也加强下认知。

这里简化了下朋友的代码,

<head>
  ... 
  <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  <%= yield "javascript" %>
</head>

之后弄了两个页面,分别是 index 和 dashboard 页面都很简单

index.html.erb

<h1>Welcome#index</h1>

<%= link_to 'index self', welcome_index_path %>
<%= link_to 'dashboard', welcome_dashboard_path %>

<%= content_for "javascript" do %>
  <script>
    function consoleByRender(){
      console.log('from index turbolinks:load');
    }
    document.addEventListener("turbolinks:load", consoleByRender) 
  </script>
<%end%>

dashboard.html.erb

<h1>dashboard.html.erb</h1>
<%= link_to 'dashboard self', welcome_dashboard_path %>
<%= link_to 'index', welcome_index_path %>

<%= content_for "javascript" do %>
  <script>
    document.addEventListener("turbolinks:load", function(){
      console.log("from dashboard turbolinks:load" );
    })

  </script>
<% end %>

之后我们打开 index, 再点击 dashboard, 发现如下的日志输出

我们可以看到,从浏览器的地址栏里输入 url 里访问的时候,index 页面的 turbolinks 被正常加载了,但是当我们点击 dashboard 链接后

我们发现 在进入 dashboard 的 turbolinks:load 前,index.html.erb 中的 turbolinks 也被响应了。

那么这是什么原因呢?其实道理很简单,因为 turbolinks 劫持了 a 属性,改为了在页面提交 ajax, 并替换原来的属性 , 又因为页面实际上没刷新,所以,document 在这个时候没有被释放 这样 原来添加到 turbolinks:load 里的方法 仍然会再次执行

为了验证这个地方,我们再引入一个 about 页面,这个页面什么都不做,

<%= link_to 'about self', welcome_about_path %>
<%= link_to 'dashboard', welcome_dashboard_path %>
<%= link_to 'index', welcome_index_path %>

由 index 先进入 dashboard, 再进入 about , 我们会发现下面的情况

就算 about 里什么 js 都没有,仍然会出发之前的 turbolinks:load

这也就是为何如如果将 ajax 直接绑定在 turbolinks:load 中 任意页面都会去请求的问题

那么如何解决呢?

目前我看到比较多的 有两种做法。分别是幂等法 和 remove 事件

所谓的幂等法,就是仍然让函数执行,只是执行 1 次和 n 次对外表现的效果是一致的即可,比如最常见的方法是在 application.html.erb 中 把 body 定义一个 id

<body id='<%= "#{controller_name}-#{action_name}"%>'>
  <%= yield %>
</body>

这样 在具体页面绑定的时候就可以这样写: index 页面

<h1>Welcome#index</h1>

<%= link_to 'index self', welcome_index_path %>
<%= link_to 'dashboard', welcome_dashboard_path %>
<%= link_to 'about', welcome_about_path %>



<%= content_for "javascript" do %>
  <script>
    function consoleByRender(){
      if(document.querySelector("#welcome-index")) {
        console.log('from index turbolinks:load');
      }
    }
    document.addEventListener("turbolinks:load", consoleByRender) 




  </script>
<%end%>

dashboard 页面

<h1>dashboard.html.erb</h1>
<%= link_to 'dashboard self', welcome_dashboard_path %>
<%= link_to 'index', welcome_index_path %>
<%= link_to 'about', welcome_about_path %>

<%= content_for "javascript" do %>
  <script>

    document.addEventListener("turbolinks:load", function(){
      if(document.querySelector("#welcome-dashboard")) {
        console.log("from dashboard turbolinks:load" );
      }
   })

  </script>
<% end %>


这样 结果就变成了

并不是他们没有运行,而是在其他页面运行的时候,由于没有办法找到对应的 body , 所以实际函数并没有执行。

其实大多数时候我就喜欢用这种简单粗暴的方式。虽然说会有一点性能损失,但好像还是在接受范围内。

另外一种是 事件注销法,他要求我们在 addEventListener 的时候,传入的是一个 function 进去, 之后再在适当的事件里去 remove 这个方法, 我对这种方式比较排斥,主要是不太好写,自己 js 也挺烂的 也没找到什么太好的例子 不知道有没有朋友能详细说下这种情况的。

之后补充下看到的简单心理的关于 turbolinks 的处理 https://jiandanxinli.github.io/2017-01-17.html

我觉得本质上还是属于第一种幂等法,利用 全局变量的方式来进行的处理

最后吹吹牛,虽然说现在大环境上都玩前后端分离,react 全家桶,但是我觉得在大多数传统项目上,后端控制路由,上 turbolinks, 也挺好的,开发速度快,SEO 效果好,部署也算简单。

不要依赖 turbolinks:load 了,https://stimulusjs.org/ 是最适合 Turbolinks 的做法。

https://github.com/turbolinks/turbolinks#attaching-behavior-with-stimulus

虽然是老帖了,但是还是回复一下。

在 Stimulus 中也存在类似的问题:在启用 turbolinks 的页面中相关区块的 connect() 方法会被调用两次。这个问题现在还存在于 turbolinks 中,不确定是否在 turbo 中有解决。

https://discuss.hotwired.dev/t/controller-initialized-twice-when-visiting-from-a-turbolinks-page/17

turbolinks 首先读取 preview cache 中的内容,这个时候 js 会被执行一次。同时会从远端获取最新内容,返回后会替换 DOM 并再次执行一遍 js。

我自己琢磨出来比较简单的解决方法有两种:

  1. 在相关页面使用 <meta name="turbolinks-cache-control" content="no-preview"> 关闭 preview cache

  2. connect() 方法中增加判断,如果页面存在 data-turbolinks-preview 这个属性就跳过相关操作

export default class extends Controller {
    connect(){
        // Turbolinks is not displaying a preview
        if (!document.documentElement.hasAttribute("data-turbolinks-preview")) {
            // Only run one time
            this.doSomething()
        }
    }
}
You need to Sign in before reply, if you don't have an account, please Sign up first.