2018年12月

Turbolinks® 让你的 web 应用导航更快。 无需使用任何复杂的客户端 JavaScript 框架,即能获得单页应用一般的性能。在服务端使用 HTML 渲染视图,用超级链接将页面链接起来,就像你熟知的那样。在你点击链接时,Turbolinks 会自动地获取页面,替换它的 <body>,合并 <head>,这些操作都不会存在整页加载所需的额外花销。

turbolinks.gif

特点

  • 自动优化导航 无须注解链接;无须指定页面上需要改变的部分。
  • 无须后端协作 直接出来整个 HTML 页面,不需要将页面切成片段或者返回 JSON 数据。
  • 符合 web 规范 前进、后退按钮依然如你期望地工作。搜索引擎友好。
  • 支持移动端应用 适配 iOSAndroid,创建混合应用时可以使用原生导航控件。

浏览器支持

Turbolinks 支持所有桌面和移动浏览器。它依赖于 HTML5 History APIWindow.requestAnimationFrame。在不支持的浏览器上,Turbolinks 优雅降级为标准导航。

安装

在应用程序的 JavaScript bundle 中引入 dist/turbolinks.js

Turbolinks automatically initializes itself when loaded via a standalone <script> tag or a traditional concatenated JavaScript bundle. If you load Turbolinks as a CommonJS or AMD module, first require the module, then call the provided start() function.

使用 Ruby on Rails 安装

Your Ruby on Rails application can use the turbolinks RubyGem to install Turbolinks. This gem contains a Rails engine which integrates seamlessly with the Rails asset pipeline.

  1. Add the turbolinks gem, version 5, to your Gemfile: gem 'turbolinks', '~> 5.2.0'
  2. Run bundle install.
  3. Add //= require turbolinks to your JavaScript manifest file (usually found at app/assets/javascripts/application.js).

The gem also provides server-side support for Turbolinks redirection, which can be used without the asset pipeline.

使用 npm 安装

Your application can use the turbolinks npm package to install Turbolinks as a module for build tools like webpack.

  1. Add the turbolinks package to your application: npm install --save turbolinks.
  2. Require and start Turbolinks in your JavaScript bundle:

    var Turbolinks = require("turbolinks")
    Turbolinks.start()

The npm package alone does not provide server-side support for Turbolinks redirection. See Following Redirects for details on adding support.

内容目录

Navigating with Turbolinks

  • Each Navigation is a Visit
  • Application Visits
  • Restoration Visits
  • Canceling Visits Before They Start
  • Disabling Turbolinks on Specific Links

Building Your Turbolinks Application

  • Working with Script Elements

    • Loading Your Application’s JavaScript Bundle
  • Understanding Caching

    • Preparing the Page to be Cached
    • Detecting When a Preview is Visible
    • Opting Out of Caching
  • Installing JavaScript Behavior

    • Observing Navigation Events
    • Attaching Behavior With Stimulus
  • Making Transformations Idempotent
  • Persisting Elements Across Page Loads

Advanced Usage

  • Displaying Progress
  • Reloading When Assets Change
  • Ensuring Specific Pages Trigger a Full Reload
  • Setting a Root Location
  • Following Redirects
  • Redirecting After a Form Submission
  • Setting Custom HTTP Headers

API Reference

  • Turbolinks.visit
  • Turbolinks.clearCache
  • Turbolinks.setProgressBarDelay
  • Turbolinks.supported
  • Full List of Events

Contributing to Turbolinks

  • Building From Source
  • Running Tests

Navigating with Turbolinks

Turbolinks intercepts all clicks on <a href> links to the same domain. When you click an eligible link, Turbolinks prevents the browser from following it. Instead, Turbolinks changes the browser’s URL using the History API, requests the new page using XMLHttpRequest, and then renders the HTML response.

During rendering, Turbolinks replaces the current <body> element outright and merges the contents of the <head> element. The JavaScript window and document objects, and the HTML <html> element, persist from one rendering to the next.

Each Navigation is a Visit

Turbolinks models navigation as a visit to a location (URL) with an action.

Visits represent the entire navigation lifecycle from click to render. That includes changing browser history, issuing the network request, restoring a copy of the page from cache, rendering the final response, and updating the scroll position.

There are two types of visit: an application visit_, which has an action of _advance or _replace_, and a _restoration visit_, which has an action of _restore_.

Application Visits

Application visits are initiated by clicking a Turbolinks-enabled link, or programmatically by calling Turbolinks.visit(location).

An application visit always issues a network request. When the response arrives, Turbolinks renders its HTML and completes the visit.

If possible, Turbolinks will render a preview of the page from cache immediately after the visit starts. This improves the perceived speed of frequent navigation between the same pages.

If the visit’s location includes an anchor, Turbolinks will attempt to scroll to the anchored element. Otherwise, it will scroll to the top of the page.

Application visits result in a change to the browser’s history; the visit’s action determines how.

Advance visit action

The default visit action is _advance_. During an advance visit, Turbolinks pushes a new entry onto the browser’s history stack using history.pushState.

Applications using the Turbolinks iOS adapter typically handle advance visits by pushing a new view controller onto the navigation stack. Similarly, applications using the Android adapter typically push a new activity onto the back stack.

Replace visit action

You may wish to visit a location without pushing a new history entry onto the stack. The replace visit action uses history.replaceState to discard the topmost history entry and replace it with the new location.

To specify that following a link should trigger a replace visit, annotate the link with data-turbolinks-action="replace":

<a href="/edit" data-turbolinks-action="replace">Edit</a>

To programmatically visit a location with the replace action, pass the action: "replace" option to Turbolinks.visit:

Turbolinks.visit("/edit", { action: "replace" })

Applications using the Turbolinks iOS adapter typically handle replace visits by dismissing the topmost view controller and pushing a new view controller onto the navigation stack without animation.

Restoration Visits

Turbolinks automatically initiates a restoration visit when you navigate with the browser’s Back or Forward buttons. Applications using the iOS or Android adapters initiate a restoration visit when moving backward in the navigation stack.

Restore visit action

If possible, Turbolinks will render a copy of the page from cache without making a request. Otherwise, it will retrieve a fresh copy of the page over the network. See Understanding Caching for more details.

Turbolinks saves the scroll position of each page before navigating away and automatically returns to this saved position on restoration visits.

Restoration visits have an action of restore and Turbolinks reserves them for internal use. You should not attempt to annotate links or invoke Turbolinks.visit with an action of restore.

Canceling Visits Before They Start

Application visits can be canceled before they start, regardless of whether they were initiated by a link click or a call to Turbolinks.visit.

Listen for the turbolinks:before-visit event to be notified when a visit is about to start, and use event.data.url (or $event.originalEvent.data.url, when using jQuery) to check the visit’s location. Then cancel the visit by calling event.preventDefault().

Restoration visits cannot be canceled and do not fire turbolinks:before-visit. Turbolinks issues restoration visits in response to history navigation that has already taken place, typically via the browser’s Back or Forward buttons.

Disabling Turbolinks on Specific Links

Turbolinks can be disabled on a per-link basis by annotating a link or any of its ancestors with data-turbolinks="false".

<a href="/" data-turbolinks="false">Disabled</a>

<div data-turbolinks="false">
  <a href="/">Disabled</a>
</div>

To reenable when an ancestor has opted out, use data-turbolinks="true":

<div data-turbolinks="false">
  <a href="/" data-turbolinks="true">Enabled</a>
</div>

Links with Turbolinks disabled will be handled normally by the browser.

Building Your Turbolinks Application

Turbolinks is fast because it doesn’t reload the page when you follow a link. Instead, your application becomes a persistent, long-running process in the browser. This requires you to rethink the way you structure your JavaScript.

In particular, you can no longer depend on a full page load to reset your environment every time you navigate. The JavaScript window and document objects retain their state across page changes, and any other objects you leave in memory will stay in memory.

With awareness and a little extra care, you can design your application to gracefully handle this constraint without tightly coupling it to Turbolinks.

Working with Script Elements

Your browser automatically loads and evaluates any <script> elements present on the initial page load.

When you navigate to a new page, Turbolinks looks for any <script> elements in the new page’s <head> which aren’t present on the current page. Then it appends them to the current <head> where they’re loaded and evaluated by the browser. You can use this to load additional JavaScript files on-demand.

Turbolinks evaluates <script> elements in a page’s <body> each time it renders the page. You can use inline body scripts to set up per-page JavaScript state or bootstrap client-side models. To install behavior, or to perform more complex operations when the page changes, avoid script elements and use the turbolinks:load event instead.

Annotate <script> elements with data-turbolinks-eval="false" if you do not want Turbolinks to evaluate them after rendering. Note that this annotation will not prevent your browser from evaluating scripts on the initial page load.

Loading Your Application’s JavaScript Bundle

Always make sure to load your application’s JavaScript bundle using <script> elements in the <head> of your document. Otherwise, Turbolinks will reload the bundle with every page change.

<head>
  ...
  <script src="/application-cbd3cd4.js" defer></script>
</head>

If you have traditionally placed application scripts at the end of <body> for performance reasons, consider using the <script defer> attribute instead. It has widespread browser support and allows you to keep your scripts in <head> for Turbolinks compatibility.

You should also consider configuring your asset packaging system to fingerprint each script so it has a new URL when its contents change. Then you can use the data-turbolinks-track attribute to force a full page reload when you deploy a new JavaScript bundle. See Reloading When Assets Change for information.

Understanding Caching

Turbolinks maintains a cache of recently visited pages. This cache serves two purposes: to display pages without accessing the network during restoration visits, and to improve perceived performance by showing temporary previews during application visits.

When navigating by history (via Restoration Visits), Turbolinks will restore the page from cache without loading a fresh copy from the network, if possible.

Otherwise, during standard navigation (via Application Visits), Turbolinks will immediately restore the page from cache and display it as a preview while simultaneously loading a fresh copy from the network. This gives the illusion of instantaneous page loads for frequently accessed locations.

Turbolinks saves a copy of the current page to its cache just before rendering a new page. Note that Turbolinks copies the page using cloneNode(true), which means any attached event listeners and associated data are discarded.

Preparing the Page to be Cached

Listen for the turbolinks:before-cache event if you need to prepare the document before Turbolinks caches it. You can use this event to reset forms, collapse expanded UI elements, or tear down any third-party widgets so the page is ready to be displayed again.

document.addEventListener("turbolinks:before-cache", function() {
  // ...
})

Detecting When a Preview is Visible

Turbolinks adds a data-turbolinks-preview attribute to the <html> element when it displays a preview from cache. You can check for the presence of this attribute to selectively enable or disable behavior when a preview is visible.

if (document.documentElement.hasAttribute("data-turbolinks-preview")) {
  // Turbolinks is displaying a preview
}

Opting Out of Caching

You can control caching behavior on a per-page basis by including a <meta name="turbolinks-cache-control"> element in your page’s <head> and declaring a caching directive.

Use the no-preview directive to specify that a cached version of the page should not be shown as a preview during an application visit. Pages marked no-preview will only be used for restoration visits.

To specify that a page should not be cached at all, use the no-cache directive. Pages marked no-cache will always be fetched over the network, including during restoration visits.

<head>
  ...
  <meta name="turbolinks-cache-control" content="no-cache">
</head>

To completely disable caching in your application, ensure every page contains a no-cache directive.

Installing JavaScript Behavior

You may be used to installing JavaScript behavior in response to the window.onload, DOMContentLoaded, or jQuery ready events. With Turbolinks, these events will fire only in response to the initial page load, not after any subsequent page changes. We compare two strategies for connecting JavaScript behavior to the DOM below.

Observing Navigation Events

Turbolinks triggers a series of events during navigation. The most significant of these is the turbolinks:load event, which fires once on the initial page load, and again after every Turbolinks visit.

You can observe the turbolinks:load event in place of DOMContentLoaded to set up JavaScript behavior after every page change:

document.addEventListener("turbolinks:load", function() {
  // ...
})

Keep in mind that your application will not always be in a pristine state when this event is fired, and you may need to clean up behavior installed for the previous page.

Also note that Turbolinks navigation may not be the only source of page updates in your application, so you may wish to move your initialization code into a separate function which you can call from turbolinks:load and anywhere else you may change the DOM.

When possible, avoid using the turbolinks:load event to add other event listeners directly to elements on the page body. Instead, consider using event delegation to register event listeners once on document or window.

See the Full List of Events for more information.

Attaching Behavior With Stimulus

New DOM elements can appear on the page at any time by way of Ajax request handlers, WebSocket handlers, or client-side rendering operations, and these elements often need to be initialized as if they came from a fresh page load.

You can handle all of these updates, including updates from Turbolinks page loads, in a single place with the conventions and lifecycle callbacks provided by Turbolinks’ sister framework, Stimulus.

Stimulus lets you annotate your HTML with controller, action, and target attributes:

<div data-controller="hello">
  <input data-target="hello.name" type="text">
  <button data-action="click->hello#greet">Greet</button>
</div>

Implement a compatible controller and Stimulus connects it automatically:

// hello_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  greet() {
    console.log(`Hello, ${this.name}!`)
  }

  get name() {
    return this.targets.find("name").value
  }
}

Stimulus connects and disconnects these controllers and their associated event handlers whenever the document changes using the MutationObserver API. As a result, it handles Turbolinks page changes the same way it handles any other type of DOM update.

See the Stimulus repository on GitHub for more information.

Making Transformations Idempotent

Often you’ll want to perform client-side transformations to HTML received from the server. For example, you might want to use the browser’s knowledge of the user’s current time zone to group a collection of elements by date.

Suppose you have annotated a set of elements with data-timestamp attributes indicating the elements’ creation times in UTC. You have a JavaScript function that queries the document for all such elements, converts the timestamps to local time, and inserts date headers before each element that occurs on a new day.

Consider what happens if you’ve configured this function to run on turbolinks:load. When you navigate to the page, your function inserts date headers. Navigate away, and Turbolinks saves a copy of the transformed page to its cache. Now press the Back button—Turbolinks restores the page, fires turbolinks:load again, and your function inserts a second set of date headers.

To avoid this problem, make your transformation function _idempotent_. An idempotent transformation is safe to apply multiple times without changing the result beyond its initial application.

One technique for making a transformation idempotent is to keep track of whether you’ve already performed it by setting a data attribute on each processed element. When Turbolinks restores your page from cache, these attributes will still be present. Detect these attributes in your transformation function to determine which elements have already been processed.

A more robust technique is simply to detect the transformation itself. In the date grouping example above, that means checking for the presence of a date divider before inserting a new one. This approach gracefully handles newly inserted elements that weren’t processed by the original transformation.

Persisting Elements Across Page Loads

Turbolinks allows you to mark certain elements as _permanent_. Permanent elements persist across page loads, so that any changes you make to those elements do not need to be reapplied after navigation.

Consider a Turbolinks application with a shopping cart. At the top of each page is an icon with the number of items currently in the cart. This counter is updated dynamically with JavaScript as items are added and removed.

Now imagine a user who has navigated to several pages in this application. She adds an item to her cart, then presses the Back button in her browser. Upon navigation, Turbolinks restores the previous page’s state from cache, and the cart item count erroneously changes from 1 to 0.

You can avoid this problem by marking the counter element as permanent. Designate permanent elements by giving them an HTML id and annotating them with data-turbolinks-permanent.

<div id="cart-counter" data-turbolinks-permanent>1 item</div>

Before each render, Turbolinks matches all permanent elements by id and transfers them from the original page to the new page, preserving their data and event listeners.

Advanced Usage

Displaying Progress

During Turbolinks navigation, the browser will not display its native progress indicator. Turbolinks installs a CSS-based progress bar to provide feedback while issuing a request.

The progress bar is enabled by default. It appears automatically for any page that takes longer than 500ms to load. (You can change this delay with the Turbolinks.setProgressBarDelay method.)

The progress bar is a <div> element with the class name turbolinks-progress-bar. Its default styles appear first in the document and can be overridden by rules that come later.

For example, the following CSS will result in a thick green progress bar:

.turbolinks-progress-bar {
  height: 5px;
  background-color: green;
}

To disable the progress bar entirely, set its visibility style to hidden:

.turbolinks-progress-bar {
  visibility: hidden;
}

Reloading When Assets Change

Turbolinks can track the URLs of asset elements in <head> from one page to the next and automatically issue a full reload if they change. This ensures that users always have the latest versions of your application’s scripts and styles.

Annotate asset elements with data-turbolinks-track="reload" and include a version identifier in your asset URLs. The identifier could be a number, a last-modified timestamp, or better, a digest of the asset’s contents, as in the following example.

<head>
  ...
  <link rel="stylesheet" href="/application-258e88d.css" data-turbolinks-track="reload">
  <script src="/application-cbd3cd4.js" data-turbolinks-track="reload"></script>
</head>

Ensuring Specific Pages Trigger a Full Reload

You can ensure visits to a certain page will always trigger a full reload by including a <meta name="turbolinks-visit-control"> element in the page’s <head>.

<head>
  ...
  <meta name="turbolinks-visit-control" content="reload">
</head>

This setting may be useful as a workaround for third-party JavaScript libraries that don’t interact well with Turbolinks page changes.

Setting a Root Location

By default, Turbolinks only loads URLs with the same origin—i.e. the same protocol, domain name, and port—as the current document. A visit to any other URL falls back to a full page load.

In some cases, you may want to further scope Turbolinks to a path on the same origin. For example, if your Turbolinks application lives at /app, and the non-Turbolinks help site lives at /help, links from the app to the help site shouldn’t use Turbolinks.

Include a <meta name="turbolinks-root"> element in your pages’ <head> to scope Turbolinks to a particular root location. Turbolinks will only load same-origin URLs that are prefixed with this path.

<head>
  ...
  <meta name="turbolinks-root" content="/app">
</head>

Following Redirects

When you visit location /one and the server redirects you to location /two, you expect the browser’s address bar to display the redirected URL.

However, Turbolinks makes requests using XMLHttpRequest, which transparently follows redirects. There’s no way for Turbolinks to tell whether a request resulted in a redirect without additional cooperation from the server.

To work around this problem, send the Turbolinks-Location header in the final response to a visit that was redirected, and Turbolinks will replace the browser’s topmost history entry with the value you provide.

The Turbolinks Rails engine sets Turbolinks-Location automatically when using redirect_to in response to a Turbolinks visit.

Redirecting After a Form Submission

Submitting an HTML form to the server and redirecting in response is a common pattern in web applications. Standard form submission is similar to navigation, resulting in a full page load. Using Turbolinks you can improve the performance of form submission without complicating your server-side code.

Instead of submitting forms normally, submit them with XHR. In response to an XHR submit on the server, return JavaScript that performs a Turbolinks.visit to be evaluated by the browser.

If form submission results in a state change on the server that affects cached pages, consider clearing Turbolinks’ cache with Turbolinks.clearCache().

The Turbolinks Rails engine performs this optimization automatically for non-GET XHR requests that redirect with the redirect_to helper.

Setting Custom HTTP Headers

You can observe the turbolinks:request-start event to set custom headers on Turbolinks requests. Access the request’s XMLHttpRequest object via event.data.xhr, then call the setRequestHeader method as many times as you wish.

For example, you might want to include a request ID with every Turbolinks link click and programmatic visit.

document.addEventListener("turbolinks:request-start", function(event) {
  var xhr = event.data.xhr
  xhr.setRequestHeader("X-Request-Id", "123...")
})

API Reference

Turbolinks.visit

Usage:

Turbolinks.visit(location)
Turbolinks.visit(location, { action: action })

Performs an Application Visit to the given location (a string containing a URL or path) with the specified action (a string, either "advance" or "replace").

If location is a cross-origin URL, or falls outside of the specified root (see Setting a Root Location), or if the value of Turbolinks.supported is false, Turbolinks performs a full page load by setting window.location.

If action is unspecified, Turbolinks assumes a value of "advance".

Before performing the visit, Turbolinks fires a turbolinks:before-visit event on document. Your application can listen for this event and cancel the visit with event.preventDefault() (see Canceling Visits Before They Start).

Turbolinks.clearCache

Usage:

Turbolinks.clearCache()

Removes all entries from the Turbolinks page cache. Call this when state has changed on the server that may affect cached pages.

Turbolinks.setProgressBarDelay

Usage:

Turbolinks.setProgressBarDelay(delayInMilliseconds)

Sets the delay after which the progress bar will appear during navigation, in milliseconds. The progress bar appears after 500ms by default.

Note that this method has no effect when used with the iOS or Android adapters.

Turbolinks.supported

Usage:

if (Turbolinks.supported) {
  // ...
}

Detects whether Turbolinks is supported in the current browser (see Supported Browsers).

Full List of Events

Turbolinks emits events that allow you to track the navigation lifecycle and respond to page loading. Except where noted, Turbolinks fires events on the document object.

  • turbolinks:click fires when you click a Turbolinks-enabled link. The clicked element is the event target. Access the requested location with event.data.url. Cancel this event to let the click fall through to the browser as normal navigation.
  • turbolinks:before-visit fires before visiting a location, except when navigating by history. Access the requested location with event.data.url. Cancel this event to prevent navigation.
  • turbolinks:visit fires immediately after a visit starts.
  • turbolinks:request-start fires before Turbolinks issues a network request to fetch the page. Access the XMLHttpRequest object with event.data.xhr.
  • turbolinks:request-end fires after the network request completes. Access the XMLHttpRequest object with event.data.xhr.
  • turbolinks:before-cache fires before Turbolinks saves the current page to cache.
  • turbolinks:before-render fires before rendering the page. Access the new <body> element with event.data.newBody.
  • turbolinks:render fires after Turbolinks renders the page. This event fires twice during an application visit to a cached location: once after rendering the cached version, and again after rendering the fresh version.
  • turbolinks:load fires once after the initial page load, and again after every Turbolinks visit. Access visit timing metrics with the event.data.timing object.

Contributing to Turbolinks

Turbolinks is open-source software, freely distributable under the terms of an MIT-style license. The source code is hosted on GitHub.
Development is sponsored by Basecamp.

We welcome contributions in the form of bug reports, pull requests, or thoughtful discussions in the GitHub issue tracker.

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Building From Source

Turbolinks is written in CoffeeScript and compiled to JavaScript with Blade. To build from source you’ll need a recent version of Ruby. From the root of your Turbolinks directory, issue the following commands to build the distributable files in dist/:

$ gem install bundler
$ bundle install
$ bin/blade build

Running Tests

The Turbolinks test suite is written in TypeScript with the Intern testing library.

To run the tests, first make sure you have the Yarn package manager installed. Follow the instructions for Building From Source above, then run the following commands:

$ cd test
$ yarn install
$ yarn test

If you are testing changes to the Turbolinks source, remember to run bin/blade build before each test run.


© 2018 Basecamp, LLC.

在应用程序中安装 Stimulus,需要用到 stimulus npm 包。或者,在 <script> 标签中加载stimulus.umd.js

使用 webpack

Stimulus 集成了 webpack 资源打包器,可以从应用程序文件夹中自动加载控制器文件。

调用 webpack 的 require.context 助手函数,带上 Stimulus 控制器的目录路径。然后,使用 definitionsFromContext 助手函数将产生的上下文传递到 Application#load 方法:

// src/application.js
import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"

const application = Application.start()
const context = require.context("./controllers", true, /\.js$/)
application.load(definitionsFromContext(context))

Controller Filenames Map to Identifiers

将控制器文件命名为 [identifier]_controller.js,这里的 identifier 对应 HTML 中的每个 data-controller 标识符。

Stimulus 通常使用下划线连接文件名的多个单词。每个下划线在其标识符里都会被翻译为连字符。

还可以使用子文件夹作为命名空间。每个斜线都会在标识符里变成两个连字符。

如果元素,你可以在控制器文件内任意位置使用连字符替换下划线。

控制器如果命名为它的标识符将会是
clipboard_controller.jsclipboard
date_picker_controller.jsdate-picker
users/list_item_controller.jsusers--list-item
local-time-controller.jslocal-time

使用其他构建系统

Stimulus 也能用于其他构建系统,但不再支持自动加载了。你必须在应用程序实例中显式地加载和注册控制器文件:

// src/application.js
import { Application } from "stimulus"

import HelloController from "./controllers/hello_controller"
import ClipboardController from "./controllers/clipboard_controller"

const application = Application.start()
application.register("hello", HelloController)
application.register("clipboard", ClipboardController)

使用 Babel

如果在构建系统中使用 Babel ,你需要安装@babel/plugin-proposal-class-properties 并添加至配置中:

// .babelrc
{
  "presets": ["@babel/preset-env"],
  "plugins": ["@babel/plugin-proposal-class-properties"]
}

不使用任何构建系统

如果不喜欢使用构建系统,可以在 <script> 标签中加载 Stimulus,它可以通过 window.Stimulus 对象全局可用。

定义目标时,使用 static get targets() 方法替换 static targets = […] 类属性,因为它不再元素支持

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://unpkg.com/stimulus/dist/stimulus.umd.js"></script>
  <script>
    (() => {
      const application = Stimulus.Application.start()

      application.register("hello", class extends Stimulus.Controller {
        static get targets() {
          return [ "name" ]
        }

        // …
      })
    })()
  </script>
<head>
<body>
  <div data-controller="hello">
    <input data-target="hello.name" type="text">
    …
  </div>
</body>
</html>

浏览器支持

Stimulus 支持所有现代浏览器,包括桌面浏览器和手机浏览器。

如果需要支持 IE11 之类的旧版浏览器,需要在加载 Stimulus 前引入 @stimulus/polyfills 包。

import "@stimulus/polyfills"
import { Application } from "stimulus"

const application = Application.start()
// …

上一章,我们学习了如何使用 Data API 加载和持久化控制器的内部状态。

有时,控制器需要追踪外部资源的状态,在外部则意味着不是在 DOM 中,也不是 Stimulus 的一部分。例如,发送 HTTP 请求,以及由于请求状态的改变我们需要产生的应答。或者我们可能想要启动一个定时器,然后在控制器断开连接后停止它。这一章,我们将学习如何实现这两个场景。

异步加载 HTML

学习如何在页面中填充远程异步载入的 HTML 片段。我们在 Basecamp 中使用此技术是初始页面加载更快,保存非用户特定内容的视图使其更有效地缓存。

我们将构建一个通用的内容加载控制器,它从服务器获取 HTML 来填充页面。然后,使用它来加载未读消息,像收件箱那样。

从在 public/index.html 起草收件箱开始:

<div data-controller="content-loader"
     data-content-loader-url="/messages.html"></div>

然后新建 public/messages.html 文件,作为消息列表:

<ol>
  <li>New Message: Stimulus Launch Party</li>
  <li>Overdue: Finish Stimulus 1.0</li>
</ol>

(在真实的应用程序中你应该在服务器上动态地生成 HTML,作为演示,这里我们用静态的就行了。)

实现控制器:

// src/controllers/content_loader_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.load()
  }

  load() {
    fetch(this.data.get("url"))
      .then(response => response.text())
      .then(html => {
        this.element.innerHTML = html
      })
  }
}

当控制器取得连接,便开始用 Fetch 请求元素的 data-content-loader-url 指定 URL。然后,加载返回的 HTML 将其指派到元素的 innerHTML 属性。

打开控制台的 “network” 选项卡,刷新页面。你会看到指向 index.html 的初始页面请求,紧接着是控制器发出的指向 messages.html 的请求。

使用定时器自动刷新

改进控制器。通过定期刷新保证收件箱总是最新的。

使用 data-content-loader-refresh-interval 属性指定控制器应该多久重新加载一次内容,以毫秒为单位:

<div data-controller="content-loader"
     data-content-loader-url="/messages.html"
     data-content-loader-refresh-interval="5000"></div>

现在,修改控制器,使其读取刷新时间间隔,如果存在,开启刷新定时器:

connect() {
    this.load()

    if (this.data.has("refreshInterval")) {
      this.startRefreshing()
    }
  }

  startRefreshing() {
    setInterval(() => {
      this.load()
    }, this.data.get("refreshInterval"))
  }
}

刷新页面,在控制台观察每5秒发出一次的请求。然后修改一下 public/messages.html 并等待它出现在收件箱。

释放被追踪的资源

控制器取得连接时开启了定时器,但我们从未使其停下。这就意味着,如果控制器元素消失了,控制器也会继续发送 HTTP 请求。

修改 startRefreshing() 方法,使其保存定时器应用,就能解决问题。然后,在 disconnect()方法中,可以退出。

  disconnect() {
    this.stopRefreshing()
  }

  startRefreshing() {
    this.refreshTimer = setInterval(() => {
      this.load()
    }, this.data.get("refreshInterval"))
  }

  stopRefreshing() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  }
}

现在便能确保内容加载控制器只会在它连接到 DOM 时才会发送 HTTP 请求了。

看一下最终的控制器类:

// src/controllers/content_loader_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  connect() {
    this.load()

    if (this.data.has("refreshInterval")) {
      this.startRefreshing()
    }
  }

  disconnect() {
    this.stopRefreshing()
  }

  load() {
    fetch(this.data.get("url"))
      .then(response => response.text())
      .then(html => {
        this.element.innerHTML = html
      })
  }

  startRefreshing() {
    this.refreshTimer = setInterval(() => {
      this.load()
    }, this.data.get("refreshInterval"))
  }

  stopRefreshing() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  }
}

总结和下一步

在本章,学习了如何使用 Stimulus 的生命周期回调函数来获取和发布外部资源。

下一步,我们将学习何如在应用程序中安装和配置 Stimulus。

Next: Installing Stimulus in Your Application

很多框架怂恿你在 JavaScript 中一直保存着状态。它们将 DOM 看作“只写”的渲染目标,由客户端模板根据来自服务端的 JSON 进行调解。

Stimulus 采用了不同的方式。Stimulus 应用程序的状态以 DOM 属性的方式存在;控制器本身很大程度上是无状态的。这种方式可以用于任意位置初始化的 HTML 文档,Ajax 请求、Turbolinks 访问,甚至另一个 JavaScript 库等,而且不需要任何显式步骤就能自动让控制器焕发活力。

构建幻灯片

在上一章,我们学习了 Stimulus 控制器可以通过添加 class 类名来维持简单的状态。但是在我们需要存储一个值也不是简单的标记时,应该怎么做呢?

我们将通过构建一个幻灯片控制器来研究这个问题,在幻灯片中,需要在属性里保存当前选中的滑块的索引。

同样,从 HTML 开始:

<div data-controller="slideshow">
  <button data-action="slideshow#previous">←</button>
  <button data-action="slideshow#next">→</button>

  <div data-target="slideshow.slide" class="slide">A</div>
  <div data-target="slideshow.slide" class="slide">B</div>
  <div data-target="slideshow.slide" class="slide">C</div>
  <div data-target="slideshow.slide" class="slide">D</div>
</div>

每个滑块目标代表幻灯片里的单个滑块。控制器将负责同一时间只有一个滑块被显示。

默认地,使用 CSS 隐藏所有滑块,仅在 slide--current class 被应用时才显示:

.slide {
  display: none;
}

.slide.slide--current {
  display: block;
}

现在,起草我们的控制器。新建文件 src/controllers/slideshow_controller.js

// src/controllers/slideshow_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "slide" ]

  initialize() {
    this.showSlide(0)
  }

  next() {
    this.showSlide(this.index + 1)
  }

  previous() {
    this.showSlide(this.index - 1)
  }

  showSlide(index) {
    this.index = index
    this.slideTargets.forEach((el, i) => {
      el.classList.toggle("slide--current", index == i)
    })
  }
}

控制器定义了一个方法 showSlide(),它将循环每个滑块目标,在滑块被匹配到时切换slide--current class。

通过显示第一个滑块来初始化控制器,next()previous() 操作方法用于向前或向后翻页。

生命周期回调函数解释

initialize() 方法有何作用?与之前所用的connect()方法有何不同?

有一些 Stimulus 回调方法,它们用于在控制器进入或离开文档时创建或销毁相关状态。

方法何时被 Stimulus 调用
initialize()仅一次,控制器被首次实例化时
connect()控制器连接到 DOM 时
disconnect()控制器与 DOM 断开连接时

刷新页面,确认 NEXT 按钮可以跳到下一个滑块。

从 DOM 读取初始状态

注意控制器如何追踪它的状态——当前被选中的滑块——在 this.index 属性 中。

现在,我们想要让第二个滑块替代第一个显示出来。我们应该如何对起始索引编码?

一种方式是从 HTML data 属性中加载初始索引。例如,可以为控制器元素添加 data-slideshow-index 属性:

<div data-controller="slideshow" data-slideshow-index="1">

然后,在 initialize()方法中,读取该属性,将它转换为整型,然后传递给 showSlide()

initialize() {
    const index = parseInt(this.element.getAttribute("data-slideshow-index"))
    this.showSlide(index)
  }

在控制器元素中使用 data 属性是很常见的方式,Stimulus 为此提供了 API。不同于直接读取属性的值,使用 this.data.get()更方便:

  initialize() {
    const index = parseInt(this.data.get("index"))
    this.showSlide(index)
  }

Data API 的解释 每个 Stimulus 控制器都有一个 this.data 对象和 has(), get(),和 set() 方法。这些方法为访问控制器元素的 dara 属性带来方便,有控制器标识符限定了作用域。

例如,在上面的控制器中:

  • this.data.has("index") 如果控制器元素拥有 data-slideshow-index 属性,则返回 true
  • this.data.get("index") 以字符串形式返回元素的 data-slideshow-index 属性的值
  • this.data.set("index", index) 将元素的 data-slideshow-index 属性的值设置为 index 的字符串值

如果你的属性名由一个以上单词注册,在 JavaScript 中以驼峰形式表示,在 HTML 中以属性形式表示。例如,读取data-slideshow-current-class-name属性可以使用this.data.get("currentClassName")

添加 data-slideshow-index属性到控制器元素,刷新页面,确认幻灯片从指定的滑块开始滚动。

在 DOM 中持久化状态

我们已经了解如何通过读取 data 属性来引导幻灯片的初始滑块索引。

在浏览幻灯片时,属性不会与控制器的 index 属性同步。如果在文档中科隆控制器元素,克隆的控制器会恢复到它的初始值。

可以通过为 index 属性定义代表 Data API 的 getter 和 setter 来改进控制器:

// src/controllers/slideshow_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [ "slide" ]

  initialize() {
    this.showCurrentSlide()
  }

  next() {
    this.index++
  }

  previous() {
    this.index--
  }

  showCurrentSlide() {
    this.slideTargets.forEach((el, i) => {
      el.classList.toggle("slide--current", this.index == i)
    })
  }

  get index() {
    return parseInt(this.data.get("index"))
  }

  set index(value) {
    this.data.set("index", value)
    this.showCurrentSlide()
  }
}

这里将 showSlide() 重命名为 showCurrentSlide()并修改使其从 this.index读取值。 get index() 以整型返回控制器元素的 data-slideshow-index 属性。set index()方法设置属性的值,然后刷新当前滑块。

支持,控制器的状态便完全保存在 DOM 中了。

总结和下一步

本章我们了解了如何使用 Stimulus 的 Data API 来加载和保存幻灯片控制器的当前索引。

从可用性角度,控制器尚未完成。考虑如何完善控制器,大概需要解决以下问题:

在查看第一个滑块时,上一页按钮什么事都不用干也显示出来了。在其内部,索引值从0降到-1。是否可以在这里点击上一页按钮跳到最后一个滑块的索引?(此问题同样存在于下一页按钮)。

如果忘了指定 data-slideshow-index 属性,get index() 方法中的 parseInt() 会返回 NaN。在此情况下,是否可以预设一个默认值 0?

下一步,我们将了解如何在 Stimulus 控制器中追踪外部资源,比如定时器和 HTTP 请求等。

Next: Working With External Resources

我们已经实现了第一个控制器,并学习了 Stimulus 是怎样将 HTML 连接到 JavaScript 的。现在,我们看一下 Basecamp 是怎样创建控制器并将它用在真实的应用程序中的。

封装 DOM 剪贴板 API

在 Basecamp 的 UI 中,有类似这样的按钮:
bc3-clipboard-ui.png
点击按钮,Basecamp 就会将文字、URL 或者邮箱地址复制到你的剪贴板中。

Web 平台有这样的系统剪贴板访问 API,但是我们并不需要其中的 HTML 元素。要实现这样的复制按钮,我们必须使用 JavaScript。

实现复制按钮

比如说,我们有一个 APP,允许我们使用生成的 PIN 来授权别人访问。如果在生成的 PIN 旁边有一个可以直接点击就能复制 PIN 的按钮,那么我们就能更方便地分享它了。

打开public/index.html并使用下面这样的按钮替换掉<body>中的内容:

<div>
  PIN: <input type="text" value="1234" readonly>
  <button>Copy to Clipboard</button>
</div>

设置控制器

下一步,新建 src/controllers/clipboard_controller.js 并添加一个空的方法 copy()

// src/controllers/clipboard_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  copy() {
  }
}

在最外层的 <div> 中添加 data-controller="clipboard"。这个属性将在元素上显示,Stimulus 将会连接一个控制器实例:

<div data-controller="clipboard">

定义目标

我们需要引用文本域,这样我们就能在调用剪贴板 API 前选择它的内容了。在文本域元素上添加 `data-target="clipboard.source":

PIN: <input data-target="clipboard.source" type="text" value="1234" readonly>

现在,给控制器添加一个目标定义,这样我们就能使用 this.sourceTarget 访问文本域元素:

export default class extends Controller {
  static targets = [ "source" ]

  // ...
}

static targets 这行是什么? 在 Stimulus 加载控制器类时,它会在被称为目标(targets)的静态数组中寻找目标名称字符串。针对数组中的每个目标名称,Stimulus

会为你的控制器添加三个新属性。这里 "source" 这个目标名称就变成了这样三个属性:

  • this.sourceTarget 在控制器作用域内寻找到的第一个来源目标。如果没有找到,访问此属性会抛出一个错误
  • this.sourceTargets 在控制器作用域内寻找到的由所有来源目标组成的数组。
  • this.hasSourceTarget 如果找到了来源目标,返回 true,否则,返回 false。

连接操作

我们已经准备好装配这个复制按钮了。

我们想要点击这个难就能调用控制器的 copy() 方法,所以需要添加 data-action="clipboard#copy":

<button data-action="clipboard#copy">Copy to Clipboard</button>

常见的事件有一个速记符号 你可能已经注意到了我们省略了操作描述符中的 click->。那是因为 Stimulus 将点击事件设置成了操作 <button> 元素的默认事件。

当然其他元素都有默认事件。这里是完整的列表:

元素默认事件
aclick
buttonclick
formsubmit
inputchange
input type=submitclick
selectchange
textareachange

最后,在 copy() 方法中,我们可以选中输入框中的内容并调用剪贴板 API:

  copy() {
    this.sourceTarget.select()
    document.execCommand("copy")
  }

刷新页面,然后点击复制按钮。然后在文本编辑器中粘贴,你应该看到 PIN 值是1234

设计有弹性的用户界面

尽管剪贴板 API 在当前浏览器里能得到很好的支持,但可能还会有一小部分人在通过老版本浏览器使用我们的应用程序。

我们还应该意识到人民在访问应用程序时,可能会遇到一些问题。例如,网络连接问题或者 CDN 可用性问题等,都有可能影响到 JavaScript 的加载。

你可能觉得向后支持老版本浏览器是不值得去做的,或者,网络问题只是一时的,只需要刷新一下就好了。但是往往能有一些简单的方式来优雅地应对此类问题。

所谓有弹性的方式,通常被理解为渐进增强,它是构建 web 界面的一种实践,最基础的功能由 HTML 和 CSS 实现,然后再在此基础上使用 CSS 和 JavaScript 简历基础的用户体验,逐步地根据浏览器支持实现最佳效果。

PIN 字段的渐进增强

我们一起看看,如何对 PIN 字段进行渐进增强,隐藏复制按钮,除非它被浏览器支持。这样就能避免给别人展示不能用的按钮。

首先在 CSS 中隐藏复制按钮。然后测试 Stimulus 控制器中剪贴板 API 的支持情况。如果 API 被支持,我们可以控制器元素上添加一个 CSS 类名将按钮显示出来。

为按钮添加 class="clipboard-button"

<button data-action="clipboard#copy" class="clipboard-button">Copy to Clipboard</button>

然后,将下拉样式添加到 public/main.css

.clipboard-button {
  display: none;
}

.clipboard--supported .clipboard-button {
  display: initial;
}

在控制器中实现 connect() 方法,它可以在 API 被支持时添加 CSS 类名 到控制器元素上:

  connect() {
    if (document.queryCommandSupported("copy")) {
      this.element.classList.add("clipboard--supported")
    }
  }

如果愿意,你可以在浏览器中禁用 JavaScript,刷新页面,注意,复制按钮不见了。

我们对 PIN 字段进行了渐进增强:复制按钮的基线状态是隐藏,只有在 JavaScript 探测到剪贴板 API 被支持时才显示出来。

Stimulus 控制器可复用

到目前位置,我们看到了同一时间页面上有一个控制器实例时会发生什么。

在页面上同时存在多个控制器实例也不是罕见现象。例如,我们想要显示一个 PIN 的列表,每一个都有它的复制按钮。

控制器是可复用的:任意时候我们想要提供一种方式去向剪贴板复制一段文本,我们只需要页面上添加一段带着正确注解的代码。

我们来试试再在页面上添加一个 PIN。复制并粘贴<div>便有了两个完全一样的 PIN 字段,修改第二个的 value 属性:

<div data-controller="clipboard">
  PIN: <input data-target="clipboard.source" type="text" value="3737" readonly>
  <button data-action="clipboard#copy" class="clipboard-button">Copy to Clipboard</button>
</div>

刷新并确认两个按钮都能使用。

操作和目标可以用在任意元素上

再添加一个 PIN 字段。这次我们使用复制链接来替换原有的按钮:

<div data-controller="clipboard">
  PIN: <input data-target="clipboard.source" type="text" value="3737" readonly>
  <a href="#" data-action="clipboard#copy" class="clipboard-button">Copy to Clipboard</a>
</div>

Stimulus 允许我们使用任意元素,只要它拥有正确的 data-action 就行。

注意,在此时,点击链接同样会引起浏览器跳转到链接的 href 所指地址。可以在 action 中调用 event.preventDefault() 禁用默认行为:

  copy(event) {
    event.preventDefault()
    this.sourceTarget.select()
    document.execCommand("copy")
  }

同样地,来源目标不需要是 <input type="text">。控制器只期望它拥有 value 熟悉和 select() 方法。这意味着,可以使用 <textarea> 替代:

PIN: <textarea data-target="clipboard.source" readonly>3737</textarea>

总结和下一步

在本章,我们了解在 Stimulus 控制器中封装浏览器 API 的真是案例。还稍作修改让控制器针对旧版浏览器和网络问题变得有弹性。还了解了在页面上可以同时存在控制器的多个实例。最后,我们探索了操作和目标的解耦。

下一步,我们将学习 Stimulus 控制器的状态管理。

Next: Managing State