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:
- Avoids the need to reload JavaScript and CSS
- Reduces the workload of re-rendering web pages
- Is search engine friendly
- No backend changes are required
- Supports app platforms (with Android and iOS SDKs)
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).
Installing Turbolinks
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.