碰到一个页面 js 的小问题,发现自己对于 Turbolinks 了解太少了。
例如首页上有个图片切换的 slide 幻灯片效果,看代码是使用 js 定时器,第一次打开首页时一切正常。
点击其它页面以后,再返回到首页,幻灯片出现了闪动。页面调试发现浏览器这时候已经有两个 js 定时器在同时运转。初步分析结果是打开其它页面时,首页上的 js 代码又被引用,又产生了一个新的定时器任务。
虽然可以通过追加一些 js 代码,将已经存在的定时器先清除,然后再执行新的 js 定时器,但是这个显然不是个好办法。使用了 turbolinks 以后所有的 js/coffeescript 一次性被读入以后,执行时候也是被全部执行的吗?如果不同页面上有相同的 js 变量,那不是会乱套吗?
使用 turbolinks 时候,有没有让页面只执行该页面单独的 js 脚本的好办法?
先不说单独执行页面脚本的好办法,就当这个方法你已经知道并且用上了吧。Turbolinks 实现的是页面无刷新的视图更新,当你从一个“页面”进入到下一个“页面”的时候,浏览器并没有重刷新。这就意味着在上个页面创建的对象(比如说定时器回调)依然存在于内存之中,你必须要先清除旧的定时器然后再执行新的,所以并非“不是个好办法”,而是必须得用这个办法。
你举的这个例子其实和“该页面单独执行脚本”这个诉求根本就没有关系,是一个 A/B 问题。
#1 楼 @nightire 感谢指点!已经修改了幻灯片 js 脚本,表面上看起来没啥问题了。
反复被触发的原因之一估计是因为我在 coffeescript 脚本里使用了$( document ).on 'turbolinks:load', ->
,导致每次点击其它页面,首页的幻灯片脚本定时器都会被执行一遍。
另外,我感觉 Turbolinks 在加载时对于提高效率特别有帮助,但是这么多 js 程序同时活动在一个浏览器页面容器里,如果出现不同页面里面有相同的 html 元素 class 命名,或者是 js 命名,应该会出现一些麻烦吧。
例如, app/assets/javascripts/demo1.coffee里面的代码:
$ ->
$(".sample").click ()->
alert("test")
app/assets/javascripts/demo2.coffee里面的代码:
$ ->
$(".sample").click ()->
alert("this is a debug message")
当这两个脚本都被 Turbolinks 加载以后,应该会出现冲突。我的困惑其实是类似这种问题。
我在网上看到有人提出的解决方案是在 html 的 body 标签上添加 class,然后在 js 脚本中使用这些自定义的 class 标签作为过滤器。
ORGANIZING JAVASCRIPT IN RAILS APPLICATION WITH TURBOLINKS PAGE SPECIFIC JAVASCRIPT IN RAILS 别人整理好的 js 脚本:/jquery-readyselector
#3 楼 @lyfi2003 你的文章很有价值。我多少开始有点明白在使用 turbolinks+jquery 时碰到的那些莫名奇妙的问题是怎么来的。
借鉴了各种方案,我的做法如下:
1). 在 layout 文件里给 body 动态加 class 标签。
<body id="home2" class="<%= controller_name %> <%= action_name %>">
2). 在 app/assets/javascripts/xxxx.coffee 文件里面对 html 元素的 class 进行过滤,防止在不相关的页面上重复执行 js 脚本
$( document ).on 'turbolinks:load', ->
#如果当前页面检测不到指定的class标签,则停止执行后面的脚本
return unless $(".static_pages.home").length > 0
#.......
在学习过程中碰到的第一个坑是 turbolinks 有效的时候 jquery 的 ready 事件不会被触发。后来才发现 rails 5+ turbolinks 5 下需要用'turbolinks:load'来代替 jquery 的“ready”事件。
还曾经碰到的一个大坑是在集成 adminlte 模板的时候,解决的方式也是为 turbolinks 追加额外的事件处理,否则左侧的菜单收放会不正常。
谢谢各位大拿的帮助!
根据场景不同可以有不同写法。
如果只是一段很短的代码,并且只有一个页面用到,那么可以直接写在 body 的 script 里。 https://github.com/turbolinks/turbolinks#working-with-script-elements
<script>
$('.sample').on('click', function() {
alert('this is a debug message');
});
</script>
很多情况下,你可以将事件绑定到 document 或 window,避免绑定 Turbolinks event。https://github.com/turbolinks/turbolinks#running-javascript-when-a-page-loads
$(document).on 'click', '.sample', ->
alert('this is a debug message')
如果多个页面用到相似逻辑,那么可以抽取通用逻辑。
# <div data-alert-message="this is a debug message"></div>
$(document).on 'click', '[data-alert-message]', ->
alert($(this).data('alert-message'))
更进一步,你可以用 MutationObserver 和 Custom Elements 将可重用的前端逻辑组件化,让它自动在插入或被移除的时候执行初始化和清除逻辑。 https://github.com/turbolinks/turbolinks#responding-to-page-updates
<!-- https://github.com/basecamp/trix -->
<form …>
<input id="x" type="hidden" name="content">
<trix-editor input="x"></trix-editor>
</form>
Turbolinks 不只是让你更改绑定的事件,而是把你的 Web 应用变成持久运行的进程,让你重新思考 JavaScript 的组织方式。
目前我的做法还属于偷懒的做法,主要是不想花太多时间去研究和改动别人已经写好的 jquery 代码。对出现命名冲突的地方不得已而代码开头部分增加一个 body 标签的范围过滤器,来避免 js 脚本同时运行而造成的页面异常。
如果前端的 js 也全部是自己写的话,在编写 js 时就充分考虑到利用好 turbolinks 的「单页面」的特性应该才是正确的做法吧。
在 body 上加上 action_name
和 controller_name
作为 class。
然后以是否包含对应 class 作为判断条件。例如:
initTyped: function(){
if($('.home.index').length > 0){
$('.website-description').typed({
strings: ['xxx'],
loop: true,
showCursor: true,
startDelay: 500,
backSpeed: 10,
backDelay: 1500,
typeSpeed: 80,
contentType: 'html'
});
}
},
initPage: {
home: function(){
if($('.home.index').length > 0){
App.Home.messageForm();
}
}
},
@lyfi2003 的文章加上 ORGANIZING JAVASCRIPT IN RAILS APPLICATION WITH TURBOLINKS 应该就能上手了。
#7 楼 @aspirewit 看了一下这个js-namespace-rails,这个动态生成 js.erb 的方法也可以,不过我想这种方式生成的 js 应该已经不再属于 turbolinks 的加载对象了。