Coding Rails UJS + Stimulusjs + Turbolinks 5 = ❤️

hooopo(Hooopo) · 2020年08月24日 · 62 次阅读
本帖已被管理员设置为精华贴

del.icio.us 复活?

最近看到被雅虎收购后关掉的社会化书签网站del.icio.us又更新了内容,一个叫 Maciej Ceglowski 的家伙拥有了 del.icio.us 的域名,估计又要重新搞起?

造个轮子

自从 google reader 被关掉、del.icio.us 被关掉,获取信息的渠道少了,我主要通过 github 和 twitter,用 pocket 来保存书签,但 pocket 更偏向收藏。

由于很久没有写 Web,HTML CSS JavaScript 这些快忘的差不多了,抱着重新学习一下前端和做一个社会化标签网站的想法,注册了一个域名 hackershare.dev.

产品的设计功能点:

  • 可以通过 chrome extension 方便保存网页 URL,类似 pocket 的 chrome extension.
  • Web 端可以根据用户收藏、分享等对 URL 的热度进行排序,类似 hackernews 和 reddit 的机制
  • 灵活的类目组织,可以对 URL 进行打标签,整理类目
  • 对 URL 进行评论讨论
  • 一点社区化功能,关注用户和类目之类

下面写一下重新使用 Rails 来做 Web 的一些体验,技术栈主要是 Postgresql 12+,Rails6, Turbolinks 5,Rails UJS 和 Stimulusjs

拿收藏功能来举例,使用 stimulusjs+rails-ujs 来演示一下 Rails 的新三板斧。页面效果:

ERB 片段:

<span data-controller="likes" class="relative z-0 inline-flex shadow-sm rounded-md">
  <%= link_to toggle_liking_bookmark_path(bookmark), method: :post, type: 'button', remote: true, data: {type: :json, action: "ajax:success->likes#toggle"}, class: "relative inline-flex items-center px-4 py-2 rounded-l-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150" do %>
    <svg data-target="likes.svg" class="h-5 w-5 <%= bookmark.liked_by?(current_user) ? "text-yellow-300 hover:text-yellow-400" : "text-gray-300 hover:text-gray-400" %>" viewBox="0 0 20 20" fill="currentColor">
      <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
    </svg>
  <% end %>
  <button data-target="likes.data" type="button" class="-ml-px relative inline-flex items-center px-3 py-2 rounded-r-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
    <%= bookmark.likes_count %>
  </button>
</span>

首先是 UJS,

<%= link_to toggle_liking_bookmark_path(bookmark), method: :post, type: 'button', remote: true, data: {type: :json, action: "ajax:success->likes#toggle"}

link_to 带 remote: true 之后 ujs 发起一个 ajax 请求,type 是 json,和 jquery-ujs 那时候的写法一致,只不过之前一般都是服务端返回 JavaScript 操作 DOM。现在有了 stimulusjs 层,返回 JavaScript 和 HTML 还有 JSON 都很方便处理。下面说 stimulusjs:

HTML 里的data-controllerdata-action还有data-target作用是事件绑定。收藏这个功能就是 ujs 发起 ajax 请求成功之后,我们要更新 button 颜色,更新 likes.data 区域的数量。

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

export default class extends Controller {
  static targets = [ "data", "svg" ]

  connect() {
  }

  toggle(event){
    let [data, status, xhr] = event.detail;

    if (typeof(data) === 'string') {
      // 这里如果返回的是JavaScript或html,说明是未登录情况,turbolinks直接给跳转了
      return;
    }
    if (data["like"]) {
      this.svgTarget.classList.remove("text-gray-300");
      this.svgTarget.classList.remove("hover:text-gray-400");
      this.svgTarget.classList.add("text-yellow-300");
      this.svgTarget.classList.add("hover:text-yellow-400");
    } else {
      this.svgTarget.classList.remove("text-yellow-300");
      this.svgTarget.classList.remove("hover:text-yellow-400");
      this.svgTarget.classList.add("text-gray-300");
      this.svgTarget.classList.add("hover:text-gray-400");
    }
    this.dataTarget.innerHTML = data["bookmark"]["likes_count"];
  }
}

上面就是 stimulusjs 的全部代码,data: {action: "ajax:success->likes#toggle"} 让 ajax 成功之后调用 likes_controller.js 的 toggle 方法,并且可以取得到 event 对象,这里来处理 ajax 请求之后的页面效果。无论是后端返回 HTML 还是 JSON 还是 JavaScript,相对旧的 jquery-ujs 的方式,在 stimulusjs 这层里处理都非常容易。并且 JavaScript 有了新的组织,统一在 app/javascrips/controllers 目录,每个功能单独一个 controller。

当然,上面的功能还可以用另外的方式来实现,抛弃 rails-ujs,直接 stimulusjs 绑定 link 的 click 事件,在 likes_controller.js 里去做 ajax 请求 + 页面处理的工作,但我觉得 ujs+stimulusjs 这种更简单一些。

下面说一下 turbolinks 5 和 ujs 的一些坑,我们知道使用 turbolinks 之后,内部链接之间切换不会刷新页面,体验要比每次重新刷新页面好很多,但对于服务器端来说,整个页面还是要渲染一遍,相关的查询也要执行一遍,想有更好的体验也需要一些专门的 ajax 的请求,返回局部内容,比如分页和过滤场景。

但目前版本的 rails-ujs 和 turbolinks 5 还不是兼容的很好,拿分页举例,下一页按钮由 rails-ujs 触发,使用 pushState 接口同步 URL 历史,便于收藏,但由于这个请求并不是 turbolinks 参与的,turbolinks 维护的快照会缺失,当浏览器点后退或前进时,rails-ujs 渲染的部分会丢掉,体验很差。一个 hack 的解法是,把浏览器 restoration visit 屏蔽掉,统一 reload,由于浏览器后退的操作其实并不是高频,并不会代理什么影响:

window.onpopstate = function(event) {
  document.location.reload();
};

不知道 Turblinks 6 会不会把这个统一做好,或者抛弃 rails-ujs,让 turbolinks 来完成 ajax 的局部刷新功能。总体来说 rails-ujs+Stimulusjs+turbolinks 5 这套组合拳还是非常棒的,并且是真的渐进增强体验,比如分页和过滤这种,关闭了 JS,页面渲染还是正常,一点不影响 SEO. 只需要一行额外代码:

respond_to do |format|
  format.js { render partial: "bookmarks/bookmarks_with_pagination", content_type: "text/html" }
  format.html
end
cmlanche 将本帖设为了精华贴。 08月24日 15:32
需要 登录 后方可回复, 如果你还没有账号请 注册新账号