在 Rails 5.1 中使用 Turbolinks 无法正确呈现 React 组件

San*_*kai 7 ruby-on-rails turbolinks reactjs

我有一个非常简单的 Rails 应用程序,它有一个 React 组件,它只在特定页面(假设显示页面)的现有div 元素中显示“Hello” 。

当我使用其 URL 加载相关页面时,它可以工作。我在页面上看到你好

但是,当我之前在另一个页面上时(假设是索引页面,然后我使用 Turbolinks转到显示页面,好吧,除非我再次来回切换,否则不会呈现该组件。(回到索引页面)并返回显示页面)

从这里每次来回,我可以说视图渲染了两次。
不仅两次,而且两次!(即 2 次,然后是 4 次,然后是 6 次等等。)

我知道,因为同时我设置了div我向控制台输出一条消息的内容。

事实上,我想回到索引页应该仍然运行没有显示的组件代码,因为div 元素不在索引页上。但为什么以累积的方式呢?

我想解决的问题是:

  1. 显示页面的第一个请求上运行代码
  2. 阻止代码在其他页面(包括索引页面)中运行
  3. 显示页面的后续请求上运行一次代码

这是我使用的确切步骤和代码(我会尽量简洁。)

  • 我有一个安装了 react 的 Rails 5.1 应用程序:

    rails new myapp --webpack=react
    
    Run Code Online (Sandbox Code Playgroud)
  • 然后我创建了一个简单的Item脚手架来获取一些页面:

    rails generate scaffold Item name
    
    Run Code Online (Sandbox Code Playgroud)
  • 我只是在显示页面 ( ) 中添加以下div 元素app/views/items/show.html.erb

    <div id=hello></div>
    
    Run Code Online (Sandbox Code Playgroud)
  • Webpacker已经生成了一个Hello组件 ( hello_react.jsx),我对其进行了如下修改以使用上述div 元素。我改变了原来的'DOMContentLoaded' 事件

    document.addEventListener('turbolinks:load', () => {
      console.log("DOM loaded..");
      var element = document.getElementById("hello");
      if(element) {
        ReactDOM.render(<Hello name="React" />, element)
      }
    })
    
    Run Code Online (Sandbox Code Playgroud)
  • 然后我在上一个视图( )的底部添加了以下webpack脚本标记:app/views/items/show.html.erb

    <%= javascript_pack_tag("hello_react") %>
    
    Run Code Online (Sandbox Code Playgroud)
  • 然后我运行rails serverwebpack-dev-serverusing foreman start(通过gem 'foreman'在 中添加安装Gemfile)。这是Procfile我使用的内容:

    web:     bin/rails server -b 0.0.0.0 -p 3000
    webpack: bin/webpack-dev-server --port 8080 --hot
    
    Run Code Online (Sandbox Code Playgroud)

以下是重现所描述行为的步骤:

  1. 使用 URL加载索引http://localhost:3000/items
  2. 单击“新建项目”以添加新项目。Rails 重定向到URL 处的项目显示页面localhost:3000/items/1。在这里我们可以看到Hello React!信息。它运作良好!
  3. 使用 URL重新加载索引http://localhost:3000/items。该项目按预期显示。
  4. 使用 URL重新加载显示页面http://localhost:3000/items/1。在您好与一个控制台消息预期的提示信息。
  5. 使用 URL重新加载索引http://localhost:3000/items
  6. 单击显示链接(应通过 turbolink 执行)。在消息示出既不控制台消息。
  7. 点击Back链接(应该通过 turbolink 执行)转到索引页面。
  8. 再次单击Show链接(应通过 turbolink 执行)。这次消息显示得很好。其控制台消息显示两次

从那里每次我回到索引并再次回到显示页面时,每次都会在控制台上显示另外两条消息。

注意:不要使用(和更换)特定的DIV元素,如果我让原本hello_react是附加文件的div元素,这种行为更加明显。
编辑:另外,如果我link_to通过包含data: {turbolinks: false}. 它运作良好。就像我们使用浏览器地址栏中的 URL 加载页面一样。


我不知道我做错了什么..有什么
想法吗?

编辑:如果有兴趣尝试这个,我将代码放在以下 repo 中:https : //github.com/sanjibukai/react-turbolinks-test

Dom*_*tie 5

这是一个相当复杂的问题,恐怕我不认为它有一个直截了当的答案。我会尽我所能解释!

  • 在显示页面的第一个请求上运行代码

您的turbolinks:load事件处理程序没有运行,因为您的代码在事件触发运行turbolinks:load。这是流程:

  1. 用户导航到显示页面
  2. turbolinks:load 触发
  3. 已评估正文中的脚本

因此turbolinks:load在下一个页面加载之前不会调用事件处理程序(因此不会呈现您的 React 组件)。

要(部分)解决此问题,您可以删除turbolinks:load事件侦听器,并render直接调用:

ReactDOM.render(
  <Hello name="React" />,
  document.body.appendChild(document.createElement('div'))
)
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用<%= content_for … %>/<%= yield %>head. 例如在您的 application.html.erb 布局中

…
<head>
  …
  <%= yield :javascript_pack %>
  …
</head>
…
Run Code Online (Sandbox Code Playgroud)

然后在你的 show.html.erb 中:

<%= content_for :javascript_pack, javascript_pack_tag('hello_react') %>
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,对于您在turbolinks:load块中使用 JavaScript 添加到页面的任何 HTML ,您应该将其删除turbolinks:before-cache以防止在重新访问页面时出现重复问题。在您的情况下,您可能会执行以下操作:

var div = document.createElement('div')

ReactDOM.render(
  <Hello name="React" />,
  document.body.appendChild(div)
)

document.addEventListener('turbolinks:before-cache', function () {
  ReactDOM.unmountComponentAtNode(div)
})
Run Code Online (Sandbox Code Playgroud)

即使有这一切,您在重新访问页面时仍可能会遇到重复问题。我相信这与渲染预览的方式有关,但我无法在不禁用预览的情况下修复它。

  • 在显示页面的后续请求上运行一次代码
  • 阻止代码在其他页面(包括索引页面)中运行

正如我上面提到的,在使用 Turbolinks 时,动态包含特定于页面的脚本可能会造成困难。Turbolinks 应用程序中的事件侦听器的行为与没有 Turbolinks 的事件侦听器的行为非常不同,其中每个页面都会获得一个新页面document,因此事件侦听器会自动删除。除非您手动删除事件侦听器(例如 on turbolinks:before-cache),否则对该页面的每次访问都会添加另一个侦听器。更重要的是,如果 Turbolinks 缓存了该页面,一个turbolinks:load事件将触发两次:一次用于缓存版本,另一次用于新副本。这可能就是您看到它被渲染 2、4、6 次的原因。

考虑到这一点,我最好的建议是避免添加特定于页面的脚本来运行特定于页面的代码。相反,将所有脚本包含在 application.js 清单文件中,并使用页面上的元素来确定组件是否已挂载。你的例子在评论中做了这样的事情:

document.addEventListener('turbolinks:load', () => {
  var element = document.getElementById("hello");
  if(element) {
    ReactDOM.render(<Hello name="React" />, element)
  }
})
Run Code Online (Sandbox Code Playgroud)

如果它包含在您的 application.js 中,那么任何带有#hello元素的页面都将获得该组件。

希望有帮助!