在Rails 5.1中使用Turbolinks时,React组件未能正确渲染。

8
我有一个非常简单的Rails应用程序,其中包含一个React组件,该组件在特定页面(假设为“show”页面)中的现有
元素中仅显示“Hello”。当我使用其URL加载相关页面时,它可以正常工作。我在页面上看到了“Hello”。但是,当我之前在另一页(假设为“index”页面)上,然后使用Turbolinks转到“show”页面时,除非我再次来回跳转(返回到“index”页面并返回到“show”页面),否则组件不会呈现。从这里开始,每次我来回跳转,我都可以说视图渲染了两次以上。不仅仅是两次,而是两次以上! (即2次,然后是4次,然后是6次等等)。我知道由于同时设置
的内容,我会向控制台输出一条消息。实际上,我猜想返回到“index”页面仍应运行组件代码,而不显示,因为
元素不在“index”页面上。但为什么以累积方式? 我要解决的问题是:
  1. 在第一次请求“show”页面时运行代码
  2. 阻止在其他页面(包括“index”页面)上运行代码
  3. 在随后的“show”页面请求中仅运行一次代码

以下是我使用的确切步骤和代码(我将尽可能简洁)。
  • I have a Rails 5.1 app with react installed with:

    rails new myapp --webpack=react
    
  • I then create a simple Item scaffold to get some pages to play with:

    rails generate scaffold Item name
    
  • I just add the following div element in the Show page (app/views/items/show.html.erb):

    <div id=hello></div>
    
  • Webpacker already generated a Hello component (hello_react.jsx) that I modified as following in ordered to use the above div element. I changed the original 'DOMContentLoaded' event:

    document.addEventListener('turbolinks:load', () => {
      console.log("DOM loaded..");
      var element = document.getElementById("hello");
      if(element) {
        ReactDOM.render(<Hello name="React" />, element)
      }
    })
    
  • I then added the following webpack script tag at the bottom of the previous view (app/views/items/show.html.erb):

    <%= javascript_pack_tag("hello_react") %>
    
  • I then run the rails server and the webpack-dev-server using foreman start (installed by adding gem 'foreman' in the Gemfile) . Here is the content of the Procfile I used:

    web:     bin/rails server -b 0.0.0.0 -p 3000
    webpack: bin/webpack-dev-server --port 8080 --hot
    
以下是重现所述行为的步骤:
  1. 使用URL http://localhost:3000/items加载index页面。
  2. 单击New Item添加一个新项。Rails会将您重定向到项目的show页面,URL为localhost:3000/items/1。在这里,我们可以看到Hello React!消息。它工作得很好!
  3. 使用URL http://localhost:3000/items重新加载index页面。该项按预期显示。
  4. 使用URL http://localhost:3000/items/1重新加载show页面。如预期显示Hello消息,同时有一条控制台消息。
  5. 使用URL http://localhost:3000/items重新加载index页面。
  6. 单击Show链接(应通过turbolink执行)。消息显示,也没有控制台消息。
  7. 单击Back链接(应通过turbolink执行)回到index页面。
  8. 再次单击Show链接(应通过turbolink执行)。这次消息正常显示。而控制台消息则显示两次

从那里开始,每次返回index并再次返回show页面都会在控制台显示两条消息。

注意:如果不使用(和替换)特定的div元素,而是让原始的hello_react文件附加一个div元素,则此行为更加明显。
编辑:另外,如果我通过包括data: {turbolinks: false}来更改link_to链接。它也可以正常工作。就像我们使用浏览器地址栏中的URL加载页面一样。


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

编辑:如果有兴趣尝试,请将代码放入以下存储库: https://github.com/sanjibukai/react-turbolinks-test


你能把演示放到GitHub上吗?我怀疑问题是每次显示页面时都添加了事件处理程序,但在卸载时没有删除它。如果我能全面查看项目,我应该能够给出更完整的答案。 - Dom Christie
@DomChristie 好的,我会尝试放置完整的存储库。但是步骤实际上是详尽的。不多不少。 - SanjiBukai
@DomChristie 你好,这是代码的Github存储库:https://github.com/sanjibukai/react-turbolinks-test。非常感谢您考虑这个问题。 - SanjiBukai
我正在为类似的问题苦苦挣扎。跳过Turbolinks,例如:rails new app --skip-turbolinks,能完全解决这个问题吗? - Ibraheem Ahmed
3个回答

6
这是一个相当复杂的问题,恐怕没有简单的答案。我会尽力解释!
  • 在展示页面的第一次请求中运行代码
你的 turbolinks:load 事件处理程序未能运行,因为你的代码是在 turbolinks:load 事件被触发之后 执行的。以下是流程:
  1. 用户导航到展示页面
  2. turbolinks:load 被触发
  3. 在 body 中评估脚本
因此,turbolinks:load 事件处理程序不会被调用(因此你的 React 组件不会被渲染),直到下一次页面加载。
为了(部分)解决这个问题,你可以删除 turbolinks:load 事件监听器,并直接调用 render
ReactDOM.render(
  <Hello name="React" />,
  document.body.appendChild(document.createElement('div'))
)

或者你可以使用<%= content_for … %>/<%= yield %>head中插入脚本标签。例如,在你的application.html.erb布局中。

…
<head>
  …
  <%= yield :javascript_pack %>
  …
</head>
…

然后在您的show.html.erb文件中:

<%= content_for :javascript_pack, javascript_pack_tag('hello_react') %>

在这两种情况下,值得注意的是,对于使用 turbolinks:load 块在页面上添加的任何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)
})

即使如此,当重新访问页面时,您仍然可能会遇到重复问题。我认为这与预览的呈现方式有关,但是如果不禁用预览,我无法解决它。
  • 在后续请求显示页面时仅运行一次代码
  • 阻止代码在其他页面(包括索引页面)中运行
如上所述,动态包含特定于页面的脚本可能会在使用Turbolinks时创建困难。在Turbolinks应用程序中,事件侦听器的行为与没有Turbolinks的情况非常不同,其中每个页面都会获得一个新的document,因此事件侦听器会自动删除。除非您手动删除事件侦听器(例如在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)
  }
})

如果将这段代码包含在你的application.js中,那么任何一个拥有#hello元素的页面都将加载该组件。
希望能对你有所帮助!

嗨,@Dom,谢谢;这段最后的代码片段是 jsx,我无法将其添加到 application.js 清单文件中。那么最好的方法是什么? - SanjiBukai
很抱歉,构建Webpack超出了我的能力范围,但你可以尝试将 <%= javascript_pack_tag 'application' %> 放置在应用程序布局的 head 部分,然后在 app/javascript/packs/application 中导入 import './hello_react'。就像我说的,我不是一个Webpack策略的专家,所以我不会在我的回答中涉及这部分内容,但希望能帮助您入门。 - Dom Christie

4

我曾经遇到类似的问题(link_to助手方法改变了URL,但是React内容没有加载;必须手动刷新页面才能正确加载)。 经过一些谷歌搜索,我在这个页面上找到了简单的解决方法。

<%= link_to "Foo", new_rabbit_path(@rabbit), data: { turbolinks: false } %>

由于这会导致在单击链接时进行完整页面刷新,现在我的React页面可以正常加载。也许你在自己的项目中会发现它也很有用 :)


这是解决方案,而不仅仅是权宜之计。然而,它需要更多的自我解释。到目前为止,Rails使用turbolinks在顶部加载以控制页面渲染,通过在导航时推送已呈现的HTML。我们调用一些事件(例如DOMContentLoaded)来呈现DOM的脚本,在turbolinks下无法工作。关闭turbolinks导航,或者转向SPA,可以解决这个问题。 - Dat Le Tien

2
根据您所说的,我测试了一些代码。
首先,我像您在第一个片段中建议的那样,简单地从监听器中提取了ReactDOM.render方法。这是一个很大的进步,因为消息不再像在index页面那样显示在其他地方,而只在show页面中显示,正如所需。
但是,在show页面发生了一些有趣的事情。没有更多的消息累积作为追加的div元素,这很好。实际上,它甚至只显示一次,就像所需的那样。但是..控制台消息却显示了两次!?
我猜这里发生了与缓存机制相关的事情,但由于消息应该被追加,为什么控制台消息不会像消息一样显示两次呢?
放下这个问题,这似乎有效,我想知道为什么需要在页面加载后放置React渲染(没有Turbolinks时使用DOMContentLoaded事件监听器)? 我猜这与执行一些DOM元素尚未加载时的JavaScript代码导致意外渲染有关。
然后,我尝试了您的替代方法使用<%= content_for … %>/<%= yield %>。 正如您所期望的那样,这会产生缓解结果和一些奇怪的行为。
当我通过URL加载索引页面,然后使用Turbolink转到显示页面时,它可以工作!
消息div以及控制台消息一次显示。
然后,如果我返回(使用Turbolink),div消息消失了,并且我得到了所需的“..卸载..”控制台消息。
但是从那时起,每当我返回到展示页面时,div和控制台消息都不再显示
当我回到index页面时,唯一显示的消息是".. unmounted.."控制台消息。
更糟糕的是,如果我使用URL加载show页面,则
消息不再显示!? 控制台消息被显示,但我收到有关div元素的错误(Cannot read property 'appenChild' of null)。
我不会否认我完全不知道这里发生了什么.. 最后,我尝试了您的最后一个最佳建议,只需将{{link1:最后一段代码片段}}放在HTML头中。
由于这是 jsx 代码,我不知道如何在 Rails 资产管道/文件结构中处理它,因此我将我的 javascript_pack_tag 放在 html 的 head 中。 实际上,这很好用。
这次代码在所有地方都会被执行,因此使用特定于页面的元素(如以前在注释代码中打算的那样)是有意义的。
缺点是,除非我将所有特定于页面的代码放在测试页面特定元素存在的 if 语句内,否则这次代码可能会凌乱。 然而,由于 Rails/Webpack 有良好的代码结构,将特定于页面的代码放入特定于页面的 jsx 文件应该很容易管理。
尽管如此,好处在于这次所有特定于页面的部分与整个页面同时呈现,从而避免了其他情况下发生的显示故障。
我一开始没有解决这个问题,但实际上,我想知道如何让特定于页面的内容与整个页面同时呈现。 当结合 Turbolink 和 React(或任何其他框架)时,我不知道是否可能。
总之,我把这个问题留到以后再解决。
谢谢你的贡献,Dom。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接