turbolinks踩坑(转)

由于公司内部OA 项目将turbolinks升级到了5,但是有许多的js还是采用着之前的写法,结果一大堆的莫名其妙的页面渲染问题暴露出来,总结一下遇到的坑,以及整理网上各路大神给出的解决方案,备忘。

要不要用 Turbolinks5

必须用起来, 既然是 Rails5应用, 强烈推荐使用 Turbolinks5来构建你的应用. 在用好它的前提下, 它的用户体验速度赶超用 AngularJS, ReactJS, VueJS构建的单页应用( 在详细页里面有解释 ). 而学习成本实际上很低, 一个团队只要一个人掌握即可. 而且我希望通过我的文章你就能完全掌握好它.

实际上, Turbolinks 用的好, 能够让 Rails 应用比 AngularJS 或 ReactJS 构建的单页应用还要快. 而学习成本又极低. 说不定你看完此文就明白了. 本文尝试解答以下问题:

  • Turbolinks5 的出现背景
  • Turbolinks5 是否应该使用, 在什么样的情况下使用?
  • Turbolinks5 与 Turbolinks-classic 有哪些区别?
  • Turbolinks5 典型问题与解决方法
  • Turbolinks5 技术内幂

Turbolinks 背景

首先, 应该先解读一下 Turbolinks5 出现的背景与意义, 以此可以基本推出它解决问题的思路与采用的技术.

Turbolinks5 是一个 "简单" 的单页应用 JS 客户端框架, 能够让你轻易地实现 "单页面" 应用, 换言之, 它某种程度与 AngularJS, ReactJS 一样, 都是为了提升客户端体验更快. 不同的地方是, 它是足够 "简单" 的方案, 几乎只需要提供一行代码即可:

//=require 'turbolinks'

它解决问题的思路与 Rails的理念是一致的:

  • 通过简单直白的思路解决 80% 的问题, 剩下的交给你. 我们来看看它如何解决问题.

一个直白的公式: 网页加载速度 = 下载资源速度 + 解析资源速度

"单页面应用" 快的秘诀就在于它同时减少了下载资源的大小( 除却第一次加载模板后, 后续全部使用 JSON API), 以及极大提高了解析资源的速度( 通过 JSON 数据就能更新页面).

Turbolinks 无法解决下载资源的大小的问题, 却可以通过几乎不影响原有网页架构的情况下极大提高解析资源的速度.

为了理解 Turbolinks 的工作原理, 我们先来看一下在 chrome 浏览器中, 网页是如何被加载的.

  1. 下载 index.html
  2. 解析 head 标签中的 link 与 script 标签, 如果是带有 src属性, 阻塞其他逻辑执行, 继续去下载对应的资源并执行. 如果没带, 则直接执行其中的代码逻辑.
  3. 渲染 body 标签的内容, 并解析执行 body 中的 script 标签.
  4. 全部执行完毕, 执行 DOMContentLoaded 事件绑定的逻辑.

第一次加载时网页执行跟上述是一致, 之后 Turbolinks 会绑定 Body 下所有的 a 元素的 click事件, 切换页面时, Turbolinks 将会接管浏览器的页面加载过程, 采用以下方式:

  1. 异步加载新页面的 index.html
  2. 解析 head 标签中的 link 与 script 标签, 识别其中带有 data-turbolinks-track
    的属性, 如果 src有变化( 可能性很小 ), 则重载所有页面. 如果没有变化, 则不进行任何操作.
  3. 解析 head 标签中新的 link 与 script 标签, 加载并执行.
  4. 用新页面的 body 替换老的 body 中的内容, 并执行其中的 script 脚本.

这样一来, Turbolinks 能够绝大时间里避免每次重复的 head 其中的 css 与 javascript 标签的解析与加载时间( 这个时间往往很耗时 ).

Turblinks5 与 Turbolinks-classic 的异同

Turblinks5 与 Turbolinks-classic 核心理念没有变化, 最大的区别在于更清晰明确了流程, 以及更清晰的事件触发, 以及重构了代码.

对使用者影响最大的可能就是事件了.

  1. page:change -> turbolinks:load
  2. 不需要继续绑定 ready事件了
  3. script 的追踪标签 由 data-turbolinks-track -> data-turbolinks-track="reload"

现在请直接使用 Turbolinks5, 更简单明了. 以下均在 Turbolinks5 的基础上进行讲解与分析.

Turbolinks5 不是免费的

Turbolinks5 能够让你在极小的改动下提升网页的加载速度, 但也带来了新的问题: 页面执行逻辑的变化要求你的 javascript 逻辑尽可能的幂等( 即重复执行也不影响最终的结果 ). 否则的话, 需要你理解 Turbolinks5 的工作原理并针对进行调整. 这便是本文最大的价值所在.

最佳实践:

将所有 javascript 脚本打包为一个 application.js, 放在 head 中, 并用 'data-turbolinks-track="reload"' 追踪.

但有的时候, 我们必须打破这个最佳, 比如第三方组件, 这时候我们该如何处理?

典型问题一: 为什么刷新就好了, 从其他页面点击过来就有问题?

注意到启用了 Turbolinks5 的页面, 浏览器的加载流程与 Turbolinks5 去加载的流程有大的差异. 如果没有注意到一些幂等或依赖的 JS 问题, 就会出现这种问题.

遇到这种情况, 要具体问题具体分析.

典型问题二: head 中添加了新的 script, body 中有 script 对其有依赖

在典型的第三方插件中, 会要求你在 head 中添加它们的源, 在 body 中添加插件的初始化操作. 注意, 这种情况在 Turblinks5 中会有依赖加载的问题.

例:

// page1
<head>
  <script src='application.js' data-turbolinks-track='reload'></script>
  <script src='plugin.js'></script>
</head>
<body>
  <script> window.plugin.init(); </script>
</body>
// plugin.js
window.plugin = { init: function(){ // init code here }}

这种写法在标准的浏览器加载时是非常正常的, 但是在 Turbolinks5 流程里就有问题了:

plugin.js在 Turbolinks5 中执行是用的类似于

script = document.createElement('script')script.src = 'plugin.js'

这个加载过程是异步的, 所以往往在 body 中的 script 标签执行时, plugin.js还没有下载完, 所以执行就会出现变量未定义错误.在这种情况下, 建议将其改为

// page1
<head> 
  <script src='application.js' data-turbolinks-track='reload'></script>
</head>
<body> 
  <script> 
    $.getScript('plugin.js', function(){ 
      window.plugin.init(); 
    }); 
  </script>
</body>

典型问题三: body 中更新某个 DOM 节点会导致页面缓存重复的问题

这也是一个非常容易犯的错误, 举个例子

// page1
<head>
  <script src='application.js' data-turbolinks-track='reload'></script>
</head>
<body> 
  <p id='el'>hello world</p> 
  <a href='page2'>page2</a> 
  <script>
    $('#el').append('+123'); 
  </script>
</body>

这个问题非常难于发现, 但很容易犯错. 采用这个页面的写法之后, 就能触发一个 bug:

  1. 打开 page1
  2. 点击进入 page2
  3. 点击浏览器的 "后退" 按钮
  4. 点击浏览器的 "前进" 按钮
  5. 再次 "后退" 按钮

bug 出现, 页面上 #el元素变为 'hello world+123+123' 了. 如果你继续 "后退", "前进" 将出现重复的 +123 这样的错误.想像一下, 如果将上述简单代码换成一个图表插件, 插件将会导致重复的图表渲染. 这个 bug 还是非常严重的.

如何修复?

有两种方法:

  1. 关闭缓存, 在这个特定的页面的 head 放置 <meta name="turbolinks-cache-control" content="no-cache">的 meta 标记以关闭当前页面的缓存, 这样将强制每一次后退必须从服务端拉取最新的页面.
  2. 利用 turbolinks:before-cache事件, 在缓存前重置这个元素, 例如这里可以用
$(document).on('turbolinks:before-cache', function(){
     $('#el').text('hello world');
});

来解决这个问题.

这种情况与我们接下来要讲的缓存机制有关. 接下来我们要深入理解 Turbolinks5 的机制, 达到掌控它的能力.

Turbolinks5 的缓存机制

Turbolinks5 在某种程度上比使用 "AngularJS", "VueJS", "ReactJS" 构建的单页应用更快. 这得益于它的缓存机制设计.

Turbolinks5 在每一次访问页面后, 都会缓存当前页面, 默认最多缓存 20 个. 缓存页面有两个用途:

  1. 使用浏览器后退, 前进时, 直接从缓存中取出对应的页面并渲染.
  2. 通过 a 元素点击时, Turbolinks5 会率先从缓存中取出页面, 渲染出来, 然后再通过 XMLHttpRequest 取得服务器最新的页面, 再替换掉缓存页, 并渲染最新的页面.

这个时候请注意第一种情况, Turbolinks5 使用的是 cloneNode(true)
来缓存页面, 这样将导致它替换页面时丢失掉所有的事件绑定, 它必须重新解析执行其中的 script
脚本才能让缓存页面正常工作. 这时候如果处理不当就会出现上一段落第 3 个典型问题.

充分利用 Turbolinks5 的缓存机制, 能够让我们的页面访问速度超出 "单页应用", 所以, 我不建议你轻易的关闭它.

深入 Turbolinks5 的处理流程

我们再来深入的分析 Turbolinks5 在不同的情况的加载过程.

浏览器第一次加载, 或点击刷新: 这种情况保持与浏览器的加载顺序一致.

点击浏览器后退或前进: 直接调取缓存页面并显示, 不再拉取服务端数据.

点击页面的 a 元素: 先尝试拉取缓存, 如果有, 渲染缓存页面, 然后同时拉取服务端新页面并替换缓存; 如果没有, 则异步拉取服务端新页面, 缓存之并渲染新页面.

为了更清晰展示这个流程, 我花了几个小时整理了一个流程图:


还要详细说明一下如何渲染页面( 无论是缓存页面还是新页面 ):

  1. 检查 head 中是否有标记过 reload 的 script 的 src 有变化, 如果有, 则完全刷新页面了.
  2. head 中如果有新的 script 标签, 就异步加载它( 通过 Element.src = ''的方式 )
  3. 执行 body 中的 script 标签

通过这个分析, 我们可以推断出, 如果同时有缓存页和新页面, 其中的 script 标签就可能被执行两次.

除了以上描述的标准流程外, Turbolinks5 还有几个选项可以定制它的流程:

  • 定义如何临时关闭缓存
  • 定义如何缓存页面
  • 定义如何管理历史记录
  • 定义如何临时关闭 Turbolinks5

关于这些选项, 可以阅读它的源码和官方主页了解更多: https://github.com/turbolinks/turbolinks
如果想了解源代码:http://www.jianshu.com/p/622571b33eca

原文资料链接 Turbolinks5 概述及实现原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,629评论 1 92
  • 1. 介绍 浏览器可能是最广泛使用的软件。本书将介绍浏览器的工作原理。我们将看到,当你在地址栏中输入google....
    康斌阅读 1,966评论 7 18
  • 简介浏览器可以被认为是使用最广泛的软件,本文将介绍浏览器的工 作原理,我们将看到,从你在地址栏输入google.c...
    听风阁阅读 3,200评论 0 7
  • 一:在制作一个Web应用或Web站点的过程中,你是如何考虑他的UI、安全性、高性能、SEO、可维护性以及技术因素的...
    Arno_z阅读 1,087评论 0 1
  • 转载说明 一、介绍 浏览器可以被认为是使用最广泛的软件,本文将介绍浏览器的工作原理,我们将看到,从你在地址栏输入g...
    17碎那年阅读 2,375评论 0 22