Turbo Stream 和个性化的内容


Hotwire 和Turbo对于实时更新网页而不需要加载整页非常的好。

但是如果你想使用stream传个性化的信息到客户端,你可能会遇到点问题,比如发帖人“James” ,如果你是“James”,就将James 变成 You。

看看官方的相关例子

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
  <%= message.content %>
</div>

<!-- app/views/messages/index.html.erb -->
<h1>All the messages</h1>
<%= render partial: "messages/message", collection: @messages %>


# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end

  def create
    message = Message.create!(params.require(:message).permit(:content))

    respond_to do |format|
      format.turbo_stream do
        render turbo_stream: turbo_stream.append(:messages, partial: "messages/message",
          locals: { message: message })
      end

      format.html { redirect_to messages_url }
    end
  end
end

这个示例是当我们这index页面提交一个请求创建一个new message,通过turbo stream就不会重新打开index页面,而是直接append 这个message的html。 使用这个partial: _message.html.erb.  不需要加载整页。非常简洁。

我们扩展这例子,增加个消息的发送者 :message.user.name。 Message模型关联一个User模型。这是这个新的partial

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
  <span class="from"><%= message.user.name %></span>:
  <%= message.content %>
</div>

这个没问题。但是如果想通过You代替实际的名字。当看到是我发的信息的情况下。我们假设有个current_user的方法。现在新的partial变成了:

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
  <span class="from">
    <% if message.user == current_user %>
      You
    <% else %>
      <%= message.user.name %>
    </span>:
  <%= message.content %>
</div>

按道理,我应该将这些逻辑判断的代码做成一个helper,但是现在我们先不考虑这些优化的问题。

现在当你加载这个index page of messages。你看见的可能就是你预期想看到的。一些是别人的名字,而你留下的message写的是You。

但是当你post一个新message,当这个消息出现增加在bottom底部时候,他不是You,而是James。 发生了什么呢?关于显示You的逻辑有什么问题? 当你reload这个页面的时候,又变成正常的了。You又正常的显示了。
难道这是什么Maybe it was some kind of non-deterministic bug? So you post another message, and again, it's from "James", not "You".
某种不确定的错误码?你又发布了一条消息,依然显示“James”,而不是“You”。而且当你再次reload的时候,又开始显示“You”。

为什么?
Turbo stream从来都不知道这个current_user, 这是个好事!


用于Turbo Steam的Partials不能有全局引用, 他们是通过ApplicationRenderer呈现的。不包含在特定的上下文请求中。

这是什么意思呢?就是说当这个方法调用turbo_stream.append的时候,实际上就是渲染一个partial,他做这个没有引用任何的请求,他不知道 “current user”应该是啥,就好像没有人登录一样。

这实际上是好事,因为让我们你想一秒钟,我们的朋友Patrick也在index page上。当我post我的message。我的请求会触发controller,它将这个消息渲染,然后广播到所有的连接上来的clients。如果渲染的时候将我作为了current_user,那么这个partial里面的name就变成了“You”,不仅仅是我看到You,Patrick也看到的是You,和发送者看到的是一样了。

这是因为同样的rendererd string会通过trubo stream发送给所有的连上来的客户,而不是每个client得到不同的renderer,并且知道他们是谁。

那么现在我想要的得到“You”,我要怎么做?

我已经不得不在很多个应用里面解决这个问题了。当我想要highlight高亮显示一些内容,当这个内容和用户strongly related 密切相关的时候。
I've had to solve this in a couple of applications now, where I wanted to highlight when a message or other piece of data was strongly related to the user viewing it.

这里也没有practical 切实可行的方式去解决这服务端,至少在没有用双份的turbostream链接,通过一个后端去处理current user. 然后其它的后端渲染别的客户端。(然后在客户端,构建逻辑让turbo知道忽略来自group stream的message,而是用personal stream来取代它 supercede it.)

相反,我们在客户端去fix这个问题。Here's what I've come up with. Firstly, a Stimulus controller:  这是我的想法。

// app/javascript/controllers/personalization_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["name"]
  static values = { userId: Number }

  nameTargetConnected(target) {
    if (target.dataset.userId == this.userIdValue) {
      target.innerHTML = "You"
    }
  }
}

然后在main application layout里面,我们启用这个controller,告诉contrller这个current_user的id

<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html lang="en">
  <!-- head element stuff -->

  <body data-controller="personalization" 
data-personalization-user-id-value="<%= current_user.id %>">
    <%= yield %>

    <!-- etc... -->
  </body>
</html>

一个hepler,存放显示username的逻辑。

# app/helpers/application_helper.rb
def user_name(user)
  display_name = (user == current_user) ? 'You' : user.name

  content_tag(:span, display_name, 
data: { personalization_target: 'name', user_id: user.id })

end

最后修改我们的partial来使用那个helper:

<!-- app/views/messages/_message.html.erb -->
<div id="<%= dom_id message %>">
  <span class="from"><%= user_name message.user %></span>:
  <%= message.content %>
</div>

现在显示都正常了。
当我们的partial通过turbo插入,stimulus控制器running across整个页面,通知新name target。然后invokes调用 nameTargetConnected() function在那个element。对比目标上的userId和页面上的属于current_user的id。如果相同就替换目标内容为"You"。
阅读量: 975
发布于:
修改于: