diff options
-rw-r--r-- | guides/source/action_cable_overview.md | 272 |
1 files changed, 160 insertions, 112 deletions
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 2f602c3e0a..7809607574 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -147,16 +147,13 @@ established using the following JavaScript, which is generated by default by Rai #### Connect Consumer ```js -// app/assets/javascripts/cable.js -//= require action_cable -//= require_self -//= require_tree ./channels +// app/javascript/channels/consumer.js +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. -(function() { - this.App || (this.App = {}); +import ActionCable from "actioncable" - App.cable = ActionCable.createConsumer(); -}).call(this); +export default ActionCable.createConsumer() ``` This will ready a consumer that'll connect against `/cable` on your server by default. @@ -167,12 +164,16 @@ you're interested in having. A consumer becomes a subscriber by creating a subscription to a given channel: -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } +```js +// app/javascript/cable/subscriptions/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }) + +// app/javascript/cable/subscriptions/appearance_channel.js +import consumer from "./consumer" -# app/assets/javascripts/cable/subscriptions/appearance.coffee -App.cable.subscriptions.create { channel: "AppearanceChannel" } +consumer.subscriptions.create({ channel: "AppearanceChannel" }) ``` While this creates the subscription, the functionality needed to respond to @@ -181,9 +182,12 @@ received data will be described later on. A consumer can act as a subscriber to a given channel any number of times. For example, a consumer could subscribe to multiple chat rooms at the same time: -```coffeescript -App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } -App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" } +```js +// app/javascript/cable/subscriptions/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" }) +consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" }) ``` ## Client-Server Interactions @@ -256,24 +260,31 @@ When a consumer is subscribed to a channel, they act as a subscriber. This connection is called a subscription. Incoming messages are then routed to these channel subscriptions based on an identifier sent by the cable consumer. -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -# Assumes you've already requested the right to send web notifications -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - @appendLine(data) - - appendLine: (data) -> - html = @createLine(data) - $("[data-chat-room='Best Room']").append(html) - - createLine: (data) -> - """ - <article class="chat-line"> - <span class="speaker">#{data["sent_by"]}</span> - <span class="body">#{data["body"]}</span> - </article> - """ +```js +// app/javascript/cable/subscriptions/chat_channel.js +// Assumes you've already requested the right to send web notifications +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + this.appendLine(data) + }, + + appendLine(data) { + const html = this.createLine(data) + const element = document.querySelector("[data-chat-room='Best Room']") + element.insertAdjacentHTML("beforeend", html) + }, + + createLine(data) { + return ` + <article class="chat-line"> + <span class="speaker">${data["sent_by"]}</span> + <span class="body">${data["body"]}</span> + </article> + ` + } +}) ``` ### Passing Parameters to Channels @@ -293,23 +304,30 @@ end An object passed as the first argument to `subscriptions.create` becomes the params hash in the cable channel. The keyword `channel` is required: -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - @appendLine(data) - - appendLine: (data) -> - html = @createLine(data) - $("[data-chat-room='Best Room']").append(html) - - createLine: (data) -> - """ - <article class="chat-line"> - <span class="speaker">#{data["sent_by"]}</span> - <span class="body">#{data["body"]}</span> - </article> - """ +```js +// app/javascript/cable/subscriptions/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + this.appendLine(data) + }, + + appendLine(data) { + const html = this.createLine(data) + const element = document.querySelector("[data-chat-room='Best Room']") + element.insertAdjacentHTML("beforeend", html) + }, + + createLine(data) { + return ` + <article class="chat-line"> + <span class="speaker">${data["sent_by"]}</span> + <span class="body">${data["body"]}</span> + </article> + ` + } +}) ``` ```ruby @@ -340,13 +358,17 @@ class ChatChannel < ApplicationCable::Channel end ``` -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - # data => { sent_by: "Paul", body: "This is a cool chat app." } +```js +// app/javascript/cable/subscriptions/chat_channel.js +import consumer from "./consumer" + +const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + // data => { sent_by: "Paul", body: "This is a cool chat app." } + } +} -App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) +chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) ``` The rebroadcast will be received by all connected clients, _including_ the @@ -396,46 +418,69 @@ appear/disappear API could be backed by Redis, a database, or whatever else. Create the client-side appearance channel subscription: -```coffeescript -# app/assets/javascripts/cable/subscriptions/appearance.coffee -App.cable.subscriptions.create "AppearanceChannel", - # Called when the subscription is ready for use on the server. - connected: -> - @install() - @appear() - - # Called when the WebSocket connection is closed. - disconnected: -> - @uninstall() - - # Called when the subscription is rejected by the server. - rejected: -> - @uninstall() - - appear: -> - # Calls `AppearanceChannel#appear(data)` on the server. - @perform("appear", appearing_on: $("main").data("appearing-on")) - - away: -> - # Calls `AppearanceChannel#away` on the server. - @perform("away") - - - buttonSelector = "[data-behavior~=appear_away]" - - install: -> - $(document).on "turbolinks:load.appearance", => - @appear() - - $(document).on "click.appearance", buttonSelector, => - @away() - false - - $(buttonSelector).show() - - uninstall: -> - $(document).off(".appearance") - $(buttonSelector).hide() +```js +// app/javascript/cable/subscriptions/appearance_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create("AppearanceChannel", { + // Called once when the subscription is created. + initialized() { + this.update = this.update.bind(this) + }, + + // Called when the subscription is ready for use on the server. + connected() { + this.install() + this.update() + }, + + // Called when the WebSocket connection is closed. + disconnected() { + this.uninstall() + }, + + // Called when the subscription is rejected by the server. + rejected() { + this.uninstall() + }, + + update() { + this.documentIsActive ? this.appear() : this.away() + }, + + appear() { + // Calls `AppearanceChannel#appear(data)` on the server. + this.perform("appear", { appearing_on: this.appearingOn }) + }, + + away() { + // Calls `AppearanceChannel#away` on the server. + this.perform("away") + }, + + install() { + window.addEventListener("focus", this.update) + window.addEventListener("blur", this.update) + document.addEventListener("turbolinks:load", this.update) + document.addEventListener("visibilitychange", this.update) + }, + + uninstall() { + window.removeEventListener("focus", this.update) + window.removeEventListener("blur", this.update) + document.removeEventListener("turbolinks:load", this.update) + document.removeEventListener("visibilitychange", this.update) + }, + + get documentIsActive() { + return document.visibilityState == "visible" && document.hasFocus() + }, + + get appearingOn() { + const element = document.querySelector("[data-appearing-on]") + return element ? element.getAttribute("data-appearing-on") : null + } +}) ``` ##### Client-Server Interaction @@ -445,16 +490,16 @@ ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The **Server** identifies this connection by `current_user`. 2. **Client** subscribes to the appearance channel via -`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`) +`consumer.subscriptions.create({ channel: "AppearanceChannel" })`. (`appearance_channel.js`) 3. **Server** recognizes a new subscription has been initiated for the appearance channel and runs its `subscribed` callback, calling the `appear` method on `current_user`. (`appearance_channel.rb`) 4. **Client** recognizes that a subscription has been established and calls -`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`. -`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a -data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is +`connected` (`appearance_channel.js`) which in turn calls `install` and `appear`. +`appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a +data hash of `{ appearing_on: this.appearingOn }`. This is possible because the server-side channel instance automatically exposes all public methods declared on the class (minus the callbacks), so that these can be reached as remote procedure calls via a subscription's `perform` method. @@ -488,13 +533,17 @@ end Create the client-side web notifications channel subscription: -```coffeescript -# app/assets/javascripts/cable/subscriptions/web_notifications.coffee -# Client-side which assumes you've already requested -# the right to send web notifications. -App.cable.subscriptions.create "WebNotificationsChannel", - received: (data) -> - new Notification data["title"], body: data["body"] +```js +// app/javascript/cable/subscriptions/web_notifications_channel.js +// Client-side which assumes you've already requested +// the right to send web notifications. +import consumer from "./consumer" + +consumer.subscriptions.create("WebNotificationsChannel", { + received(data) { + new Notification(data["title"], body: data["body"]) + } +}) ``` Broadcast content to a web notification channel instance from elsewhere in your @@ -629,10 +678,9 @@ class Application < Rails::Application end ``` -You can use `App.cable = ActionCable.createConsumer()` to connect to the cable -server if `action_cable_meta_tag` is invoked in the layout. A custom path is -specified as first argument to `createConsumer` (e.g. `App.cable = -ActionCable.createConsumer("/websocket")`). +You can use `ActionCable.createConsumer()` to connect to the cable +server if `action_cable_meta_tag` is invoked in the layout. Otherwise, A path is +specified as first argument to `createConsumer` (e.g. `ActionCable.createConsumer("/websocket")`). For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis |