Turbolinks 不友好?

Ben*_*Ben 2 ruby-on-rails turbolinks turbolinks-5

我完全明白为什么 Turbolinks 5 很棒,如果您正在阅读它,您可能也会这样做,但我对它与其他脚本的糟糕表现感到非常沮丧。

迄今为止,还没有简单的解释(人类可读的)来展示如何以允许它们运行的​​方式包装现有的 jQuery 脚本。以这个为例: https: //github.com/Bttstrp/bootstrap-switch。写得很好,简单易懂。您将 js 和 css 加载到资产管道中并在某个页面上实例化它。

# view.html.erb
<input type="checkbox" class="switch"> switch button
<script type="text/javascript">
    $(".switch").bootstrapSwitch();
</script>
Run Code Online (Sandbox Code Playgroud)

您转到 view.html,单击另一个页面,单击返回,您会看到两个按钮。

接下来,您花了 5 个小时寻找一种方法,让 Turbolinks 仅加载一次 bootstrapSwitch 实例(如果之前未加载)。好吧,即使你这样做了,功能也会消失。点击它不会起作用。

$(document).on("turbolinks:load", function()...将在每次 Turbolink 访问时加载它,目前,我可以让它工作并且不创建重复项的唯一方法是禁用 view.html 上的缓存

<%= content_for :head do %>
    <meta name="turbolinks-cache-control" content="no-cache">
<% end %>
Run Code Online (Sandbox Code Playgroud)

这感觉有点愚蠢。

我认为这一切都与使用幂等有关 - https://github.com/turbolinks/turbolinks#making-transformations-idempot但你实际上是如何做到这一点的?

有人可以以这个简单的插件为例,分享一个简单、优雅的解决方案来使其工作,然后我们可以用其他脚本重现它吗?

Dom*_*tie 6

Developing apps with Turbolinks does require a particular approach in order to get things running smoothly. Due to differences the way pages are loaded and cached, some patterns of running scripts won\'t behave in the same way with Turbolinks vs. without. This may seem unfriendly at first, and the "gotchas" can be frustrating, but I\'ve found that with a little understanding, it encourages more organised, robust code :)

\n\n

As you have figured out, the problem with duplicate switches is that the plugin is being called more than once on the same element. This is because Turbolinks caches a page just before navigating away from it, and so the cached version includes any dynamically added HTML[1] e.g. stuff added via plugins. When navigating back/forward, the cached version is restored, and the behaviour is duplicated :/

\n\n

So how to fix this? When working with code which adds HTML or event listeners, it is generally a good idea to teardown behaviours before the page is cached. The Turbolinks event for that is turbolinks:before-cache. So your setup/teardown might be:

\n\n
// app/assets/javascripts/switches.js\n$(document)\n  .on(\'turbolinks:load\', function () {\n    $(\'.switch\').bootstrapSwitch()\n  })\n  .on(\'turbolinks:before-cache\', function () {\n    $(\'.switch\').bootstrapSwitch(\'destroy\')\n  })\n
Run Code Online (Sandbox Code Playgroud)\n\n

This is a bit difficult to test since all the setup and teardown is done in event handlers. What\'s more, there maybe many more cases like this, so to prevent repitition, you may want to introduce your own "mini-framework" for setting up and tearing down functionality. The following walks through creating a basic framework.

\n\n

Here\'s is what we\'ll aim for: calling window.App.addFunction with a name and a function registers a function to call. That function gets the elements and calls the plugin. It returns an object with a destroy function for teardown:

\n\n
// app/assets/javascripts/switches.js\nwindow.App.addFunction(\'switches\', function () {\n  var $switches = $(\'.switch\').bootstrapSwitch()\n  return {\n    destroy: function () {\n      $switches.bootstrapSwitch(\'destroy\')\n    }\n  }\n})\n
Run Code Online (Sandbox Code Playgroud)\n\n

以下实现addFunction,将添加的功能存储在functions属性中:

\n\n
// app/assets/javascripts/application.js\n// \xe2\x80\xa6\nwindow.App = {\n  functions: {},\n\n  addFunction: function (name, fn) {\n    this.functions[name] = fn\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们将在应用程序初始化时调用每个函数,并将每个函数调用的结果存储在数组中results(如果存在):

\n\n
// app/assets/javascripts/application.js\n// \xe2\x80\xa6\nvar results = []\n\nwindow.App = {\n  // \xe2\x80\xa6\n  init: function () {\n    for (var name in this.functions) {\n      var result = this.functions[name]()\n      if (result) results.push(result)\n    }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

拆除应用程序涉及销毁destroy对任何结果的调用(如果存在):

\n\n
// app/assets/javascripts/application.js\n// \xe2\x80\xa6\nwindow.App = {\n  // \xe2\x80\xa6\n  destroy: function () {\n    for (var i = 0; i < results.length; i++) {\n      var result = results[i]\n      if (typeof result.destroy === \'function\') result.destroy()\n    }\n    results = []\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后我们初始化并拆卸应用程序:

\n\n
$(document)\n  .on(\'turbolinks:load\', function () {\n    window.App.init.call(window.App)\n  })\n  .on(\'turbolinks:before-cache\', window.App.destroy)\n
Run Code Online (Sandbox Code Playgroud)\n\n

所以把这一切放在一起:

\n\n
;(function () {\n  var results = []\n\n  window.App = {\n    functions: {},\n\n    addFunction: function (name, fn) {\n      this.functions[name] = fn\n    },\n\n    init: function () {\n      for (var name in this.functions) {\n        var result = this.functions[name]()\n        if (result) results.push(result)\n      }\n    },\n\n    destroy: function () {\n      for (var i = 0; i < results.length; i++) {\n        var result = results[i]\n        if (typeof result.destroy === \'function\') result.destroy()\n      }\n      results = []\n    }\n  }\n\n  $(document)\n    .on(\'turbolinks:load\', function () {\n      window.App.init.call(window.App)\n    })\n    .on(\'turbolinks:before-cache\', window.App.destroy)\n})()\n
Run Code Online (Sandbox Code Playgroud)\n\n

函数现在独立于调用它们的事件处理程序。这种解耦有几个好处。首先,它更易于测试:函数在window.App.functions. 您还可以选择何时调用您的函数。例如,假设您决定不使用 Turbolinks,您唯一需要更改的部分是何时window.App.init调用。

\n\n
\n\n

[1] 我认为这比默认浏览器行为(按“返回”使用户返回到首次加载时的页面)更好。Turbolinks“返回”使用户返回到离开时的页面,这可能是用户所期望的。

\n