turbolinks源码分析(转)

Turbolinks5 是用 Coffeescript 编写.

学习 Turbolinks5 能够让你:

  • 从根本上掌握浏览器加载网页时的处理流程
  • 掌握 Turbolinks5 的核心原理, 学会如何模块化一个 "大" 的前端项目
  • 跟着老鸟学会如何分析源代码

准备工作

clone 项目:
git clone https://github.com/turbolinks/turbolinks

准备你的编辑器, 推荐 atom 或 sublime text

  • 找到 Turbolinks.start() 入口

老鸟提示, 在研究代码之前, 明确你研究对象的适用范围非常重要, 大部分时间先看文档是一个非常有效的熟悉项目架构的手段.

  • 所以推荐提前阅读它的 README.

开始

入口非常简单:

Turbolinks.start = ->

  if installTurbolinks()

    Turbolinks.controller ?= createController()

    Turbolinks.controller.start()

installTurbolinks = ->

  window.Turbolinks ?= Turbolinks

  moduleIsInstalled()

createController = ->

  controller = new Turbolinks.Controller

  controller.adapter = new Turbolinks.BrowserAdapter(controller)

  controller

moduleIsInstalled = ->

  window.Turbolinks is Turbolinks

Turbolinks.start() if moduleIsInstalled()

可以了解到以下信息:

  • Turbolinks 会挂载到全局的 window.Turbolinks 对象, 单例( 即全局只有一个 ).
  • Turbolinks.controller 是核心, 也是单例的( 全局只有一个 ).
  • Turbolinks.controller.start() 是真正的入口.

老鸟提示, 用空间想像力从静态的代码中抽出运行时各个类或组件的关系, 这是阅读代码的精粹. 必要时, 可以动用动态的 debug 工具进行动态分析.

controller 在做什么

我们跳入 controller.coffee, 找到 start() 方法:

  start: ->

    unless @started

      addEventListener("click", @clickCaptured, true)

      addEventListener("DOMContentLoaded", @pageLoaded, false)

      @scrollManager.start()

      @startHistory()

      @started = true

      @enabled = true

暂时不用关心非骨干的代码, 我们看到最重要的入口已经出现:
clickCaptured 函数挂载到了全局的 click 事件, pageLoaded 函数挂载到了 DOMContentLoaded 事件
DOMContentLoaded 与 Load 事件区别在于前者不继续等待 css, image 加载完成即触发, 后者等页面完全加载后触发.

从这里, 我们已经看到第一个关键实现:

如何绑定了 a 元素的事件: addEventListener

这时我们已经到了 visit() 这个入口了. 继续往下看:

visit() -> @adapter.visitProposedToLocationWithAction -> @controller.startVisitToLocationWithAction

最后来到 controller.coffee 的 startVisitToLocationWithAction:

  startVisit: (location, action, properties) ->

    @currentVisit?.cancel()

    @currentVisit = @createVisit(location, action, properties)

    @currentVisit.start()

    @notifyApplicationAfterVisitingLocation(location)

我们可以看出以下信息:

  • 用户点击一个链接后, 实际上, Turbolinks5 创建了一个 Visit 的实例, 然后调用了 .start() 来启动具体的访问过程.

这时也可以基本分析出 controller 的作用了:

  • controller 是所有相关类的一个容器, 通过它来关联各个模块, 但 Visit 是特例, 它每次访问都产生一个新的实例, 并存储在 @currentVisit

Visit 真正的访问

start() -> @adapter.visitStarted(this) -> visit.issueRequest(); visit.changeHistory(); visit.loadCachedSnapshot()

这是真正的处理流程:

  1. 发送 HTTP Request( 异步, 注意 Javascript 里面请求默认都是异步 )

  2. 更新浏览器历史( 通过 History API )

  3. 加载 cache 页面

是时候分道扬镳了.

HttpRequest

http_request.coffee 里面研究一下, HTTP Request 是如何发送的:

  createXHR: ->

    @xhr = new XMLHttpRequest

    @xhr.open("GET", @url, true)

    @xhr.timeout = @constructor.timeout * 1000

    @xhr.setRequestHeader("Accept", "text/html, application/xhtml+xml")

    @xhr.setRequestHeader("Turbolinks-Referrer", @referrer)

    @xhr.onprogress = @requestProgressed

    @xhr.onload = @requestLoaded

    @xhr.onerror = @requestFailed

    @xhr.ontimeout = @requestTimedOut

    @xhr.onabort = @requestCanceled

果然不出预料, 通过 XMLHttpRequest 对象, 发起了一个异步请求. 设定了回调. 我们暂时不看异常处理, 直接看 requestLoaded

  requestLoaded: =>

    @endRequest =>

      if 200 <= @xhr.status < 300

        @delegate.requestCompletedWithResponse(@xhr.responseText, @xhr.getResponseHeader("Turbolinks-Location"))

      else

        @failed = true

        @delegate.requestFailedWithStatusCode(@xhr.status, @xhr.responseText)

@delegate 是什么鬼? 实际上, delegate 的命名在框架里是非常常见的, 它代表一个代理人, 将请求转给对应的接口. 这里明显就是原来的 Visit 实例. 这样设计能够让 HttpRequest 对象不依赖于具体的实现类( 比如 visit ), 更为通用.

继续分析, 就发现它最后调用了

  loadResponse: ->

    if @response?

      @render ->

        @cacheSnapshot()

        if @request.failed

          @controller.render(error: @response, @performScroll)

          @adapter.visitRendered?(this)

          @fail()

        else

          @controller.render(snapshot: @response, @performScroll)

          @adapter.visitRendered?(this)

          @complete()

这就是最终 HttpRequest 之后的动作, 可以看出它调用了 @controller.render 接口. 先不继续往 render 里走. 回到上一个分支点.

老鸟提示, 好的命名能够极大程度降低阅读代码的工作量, 不要一路追到底, 明确了一个接口的含义后, 可以往其他重要的入口分析. 比如 render 就是一个非常清晰的含义, 我们几乎不分析也能明白它的作用.

loadCachedSnapshot

  loadCachedSnapshot: ->

    if snapshot = @getCachedSnapshot()

      isPreview = @shouldIssueRequest()

      @render ->

        @cacheSnapshot()

        @controller.render({snapshot, isPreview}, @performScroll)

        @adapter.visitRendered?(this)

        @complete() unless isPreview

非常妙, cache page 最终加载也通过 @controller.render 进行了.

我们最终需要进入最关键的 render 函数

controller.coffee

  render: (options, callback) ->

    @view.render(options, callback)

进入 view.coffee

  renderSnapshot: (snapshot, callback) ->

    Turbolinks.SnapshotRenderer.render(@delegate, callback, @getSnapshot(), Turbolinks.Snapshot.wrap(snapshot))

进入 snapshot_renderer.coffee

  render: (callback) ->

    if @trackedElementsAreIdentical()

      @mergeHead()

      @renderView =>

        @replaceBody()

        @focusFirstAutofocusableElement()

        callback()

    else

      @invalidateView()

我们转了一圈, 最终找到了 render 的实际入口. 我们看到 render 做了以下几件事:

  1. 合并头

  2. 替换 body

  3. 一些杂项

继续看 mergeHead

  mergeHead: ->

    @copyNewHeadStylesheetElements()

    @copyNewHeadScriptElements()

    @removeCurrentHeadProvisionalElements()

    @copyNewHeadProvisionalElements()

非常明显的命名.

我们继续从 head_details.coffee 中分析到具体操作:

document.head.appendChild(element)

也就是 mergeHead 也就是同步了头部信息, 并将其加载起来. 注意这里在明确理解 Javascript 操作 script 标签元素的作用.( 会自动异步取回 src 属性中的内容并执行 )

同理, replaceBody 的操作关键是:

    for replaceableElement in @getNewBodyScriptElements()

      element = @createScriptElement(replaceableElement)

      replaceableElement.parentNode.replaceChild(element, replaceableElement)

非常清晰的命名, 让我们能够很快明白这里的逻辑.

原著-- 深圳市百分之八十科技有限公司 李亚飞

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

推荐阅读更多精彩内容