Rails Everyday: 更深刻的理解 Turbolinks

最近一直在用 Rails 写Web项目, 在项目开发过程中, 一直遇到一个问题:

JavaScript 代码在重新刷新的时候工作正常
但是只要在浏览器中前进后退一下, JavaScript代码就会执行多次
而且跟页面来回切换的次数呈相关性

可以通过 Chrome 的调试工具来查看多次 XHR 请求:

多次 XHR 请求

刚开始的时候, 没有特别在意, 因为一直在关注整个项目的架构设计和数据结构, 没有太在意这个小问题, 心里觉得可能是 Rails 哪里自己还没有彻底弄懂才导致的小问题.

直到昨天晚上开发完一个功能后, 发现JS代码居然重复执行了十次, 而且今天还要引入另外一个重要的功能, 从编程习惯上, 告诉自己是时候解决这个问题了, 否则以后搬着石头砸自己的脚...

早上的时候用 Google 搜索了相关的现象, 看了越来越多的 github issue 和 StackOverFlow 文章, 猜测问题估计出在 Turbolink 上, 但是究竟是哪里出了问题并不知道.

下午在医院等老婆的时候, 决定好好地看一下 Turbolink 相关的材料, 包括官网的手册, Turbolink 的历史, Turbolink 解决问题的目标以及 Turbolink 解决问题的思路.

看了一下午的材料, 虽然还是没有头绪, 但是脑袋里对 Turbolink 理解的更加深刻了.

Turbolinks 的历史和基本原理

  1. Rails 里的 Assets Pipeline 会把 JavaScript、StypeSheet等资源都合并成单个文件, 以减少浏览器需要发起的请求数量, 以加速浏览器加载页面的时间
  2. Assets Pipeline 的这种原理导致单个文件本身比较大, 如果每次都是全页刷新, 对加载速度一定会有影响, 所以 DHH 引入 Turbolinks 来解决重复载入和解析资源文件的时间浪费
  3. 页面加载速度 = 下载资源速度 + 解析资源速度, Turbolinks 解决的主要是解析资源的速度, 当切换页面时, Turbolinks会检查新页面 head 中 link 与 script 标签, 识别其中带有 data-turbolinks-track 的属性, 如果 src 发生变化, 就重新载入所有页面, 如果没有变化只是用新页面的 body 来替换老页面的 body 内容, 从而在绝大部分时间里避免每次重复解析和加载 head 中资源文件的时间(这个时间非常耗时)

Turbolinks 的缓存机制

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

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

在浏览器后退时, Turbolinks 使用的是 cloneNode(true) 来缓存页面, 这样将导致它替换页面时丢失掉所有的事件绑定, 它必须重新解析执行其中的 script 脚本才能让缓存页面正常工作.

Turbolinks 的处理流程

  1. 浏览器第一次加载, 或点击刷新: 这种情况保持与浏览器的加载顺序一致.
  2. 点击浏览器后退或前进: 直接调取缓存页面并显示, 不再拉取服务端数据.
  3. 点击页面的 a 元素: 先尝试拉取缓存, 如果有, 渲染缓存页面, 然后同时拉取服务端新页面并替换缓存; 如果没有, 则异步拉取服务端新页面, 缓存之并渲染新页面.

下面是网络朋友画的一张 Turbolinks 的处理流程图:


turbolinks流程图

Turbolinks 解析页面的步骤分解

Chrome 解析页面步骤

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

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

Turbolinks 解析页面步骤

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

看完了网上大部分资料, 对于 Turbolinks 整体有一个大概的了解, 但是对于我文章最开始问题的原因还没有头绪, 加上 Turbolinks 的官方文档针对常见问题几乎没有FAQ, 所以这时候就要运用逆向思维了:

  1. 手动在 head 中加入 script 代码, 不用 Turbolinks, 没有问题, 所以对比看, 问题出在 Turbolinks 的影响
  2. 为什么会多次加载 JavaScript 函数? 一定是被多次调用了
  3. 为什么JavaScript会被多次调用? 大胆猜测一下, Turbolinks 缓存的时候, 多次执行 script 部分的代码, 导致同一个 DOM element 被绑定了多次 JS callback, 所以点击一个 DOM element 执行了多次 JavaScript 函数, 一次是页面第一次加载时的 JS 代码, 更多次是 Turbolinks 缓存页面时多次执行 script 代码执行的
  4. 根据刚才整理的资料推理, 如果 CSS/JS 资源文件没变化, head 部分的资源解析过程只会做一次, 而我的 CSS/JS 文件一直没有发生变化, 所以不太可能是 head 部分代码的问题
  5. 如果 head 部分代码排除掉, 唯一的推理结论就是, Turbolinks 在缓存页面时, 因为页面发生变化, 替换了 body 的部分, 替换 body 部分以后还要执行 body 里的 script 脚本(请看上面 Turbolinks 解析页面步骤第5条)

逆向思考完毕后, 推理的假设是: 我的 body 模板中有 script 相关代码, 为了证明我的假设, 先查看一下渲染后的源代码:


source code

果然发现可疑的 script 代码, 上面的一大片 script 都是第三插件的代码, body 里的 script 点开, 发现都是我写的 Stimulusjs 相关的代码.

既然查看 HTML 源码证明了我的假设, 问题已经出在模板文件中, 用我的 color-rg 一番搜索 javascript 关键字以后, 发现在 _head.html.erb 模板文件中居然有这句:

body script

这不就是问题的原因吗? 太激动了

结合 Turbolinks 的缓存逻辑, 我其实只用把 javascript_pack_tag 的代码从 body 移动到 head 中, Turbolinks 缓存时 body 内容后就不会 script, 因为所有页面的 body 里面都没有 script 代码.

果然, 只移动一行代码以后, 问题完美解决:

it works!

最后

这就是我的学习方式, 当我们对一个难题久攻不克时, 不要着急去满世界找现成的答案, 即使答案不小心碰对了, 我们心中依然有困惑, 出来混的, 总有一天会还回去的.

当不知道答案时, 应该先放下问题, 多去看相关的资料, 相关的资料研究的越深, 潜意识就会在背后筹备线索, 当吃透的知识越多时, 即使你没法一眼看出问题,这些深厚的知识也会帮你编制一张无比严密的逻辑网帮助你逆向推理原因, 直到问题原因就蹦到你眼前, 迎刃而解.

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

推荐阅读更多精彩内容