Home Blog Talks

Introduction to Turbolinks and Usage Tips · Turbolinks Part 1

2017-01-17

Turbolinks makes navigating your web application faster.

Turbolinks is a web loading optimization solution originating from Ruby on Rails. In simple terms, when a user clicks a link, it does not actually navigate to a new page but instead reads the content of the target page through ajax and replaces the current page. This method has the following advantages:

In addition to these functional advantages, this traditional “page” style of interaction also makes it easier for users to understand (although the application-style interaction is more popular now, it requires higher standards from designers, and if not well designed, it can easily cause users to get lost).

See https://github.com/turbolinks/turbolinks, I will not repeat the content of the documentation here.

Basic Tips

Avoiding Giant CSS and JS Files

Due to the loading mechanism of Turbolinks, users tend to package all styles and scripts into a giant CSS and JS file, which can severely affect the initial load. Turbolinks fully supports loading different CSS and JS for different pages.

In the Simple Psychology project, the following method was used to automatically determine and load the required styles and scripts for the current page:

/ Load style files, such as the CSS file for pages#home, stored in app/assets/stylesheets/www/pages/home.sass
- if File.exist?(Rails.root.join("app/assets/stylesheets/#{request.subdomain.split('.')[0]}/#{params[:controller]}/#{params[:action]}.sass"))
  = stylesheet_link_tag "#{request.subdomain.split('.')[0]}/#{params[:controller]}/#{params[:action]}"

/ Load script files, such as the JS file for pages#home, stored in app/assets/javascripts/www/pages/home.coffee
- if File.exist?(Rails.root.join("app/assets/javascripts/#{request.subdomain.split('.')[0]}/#{params[:controller]}/#{params[:action]}.coffee"))
  = javascript_include_tag "#{request.subdomain.split('.')[0]}/#{params[:controller]}/#{params[:action]}"

Simplifying Page Load Events

Due to the special loading mechanism of Turbolinks, we often need to manually register and unregister events. At Simple Psychology, we simplified this part of the code in the following way:

_page_loaded = []
_page_unload = []
_page_unload_once = []

# Execute when the web page is loaded
window.$loaded = (func)->
  _page_loaded.push(func)

# Execute when leaving the web page
window.$unload = (func)->
  _page_unload.push(func)

# Execute once when leaving the web page
window.$unload_once = (func)->
  _page_unload_once.push(func)

# The following code is the implementation of the above interfaces

window.addEventListener 'turbolinks:load', ->
  func() for func in _page_loaded

window.addEventListener 'turbolinks:before-visit', ->
  func() for func in _page_unload_once
  _page_unload_once = []
  func() for func in _page_unload

window.addEventListener 'beforeunload', ->
  func() for func in _page_unload_once
  _page_unload_once = []
  func() for func in _page_unload

  null

Compatibility with Vue.js 1.x

When a page’s interaction becomes increasingly complex, it is necessary to use frameworks like Vue.js to simplify the interaction code. However, Turbolinks does not cache the events bound to the DOM when caching pages. When a user returns to a page that uses Vue components, the interactions of these components will not be effective.

Our solution is to cache the data of Vue components and unregister the components when leaving the page. When returning, re-render the components and inject the previously cached data.

Another thing to note is that Turbolinks also remembers the current page’s scroll position, recorded in Turbolinks.controller.getCurrentRestorationData().scrollPosition. Therefore, if your component is a long list and is loaded asynchronously, you need to manually cache this position before loading (because after returning to the page, Turbolinks will automatically refresh the recorded value), and then scroll the page to the cached position after the list data is loaded and rendered.

The specific implementation is as follows: (Since we are no longer using Vue.js, only the implementation for version 1.x is provided)

vm_caches = {}

cache_data = (props, data)->
  cache = {}
  for k, v of $.extend(props, data)
    if v && typeof v['raw'] isnt 'undefined'
      continue if v.raw && v.raw.indexOf('$data') is 0
      try
        cache[k] = eval(v.raw)
      catch error
        cache[k] = v.raw
    else
      cache[k] = v
  cache

cache_vm = (parent, cache)->
  for child in parent.$children
    continue if typeof child is 'undefined'
    cache[child.constructor.name] = cache_data(child._data, child._props)
    cache_vm child, cache

$loaded ->
  window.vm = new Vue
    el: 'body'

window.addEventListener 'turbolinks:before-cache', ->
  list = []
  while vm.$children.length > 0
    child = vm.$children[0]
    id = $.id() # $.id is a function to generate a unique ID, which you can implement yourself
    vm_caches[id] =
      data: cache_data(child._data, child._props)
      children: {}
    $(child.$el).after(child.$options.el.outerHTML.replace('><', " _cache_key=\"#{id}\"><"))
    cache_vm child, vm_caches[id].children
    child.$destroy(true)

Vue.define = (id, options)->
  if options['props']
    options.props.push '_cache_key'
  else
    options.props = []

  if options['ready']
    originalReady = options.ready
    options.ready = ->
      if vm_caches[@$parent._cache_key]
        for k, v of vm_caches[@$parent._cache_key].children[@constructor.name]
          Vue.set this, k, v
      if vm_caches[@_cache_key]
        for k, v of vm_caches[@_cache_key].data
          Vue.set this, k, v
      originalReady.call(this)
  else
    options.ready = ->
      if @_cache_key
        for k, v of vm_caches[@_cache_key]
          Vue.set this, k, v

  Vue.component id, options

If you have more tips, welcome to share and exchange.

Back to all posts