Rails Rails UJS + Stimulusjs + Turbolinks 5 = ❤️

hooopo · 2020年08月24日 · 最后由 huacnlee 回复于 2020年10月09日 · 2962 次阅读

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

传说 Turbolinks 6 会抛弃 UJS,但是 DHH 休假去了。。。目前几个 Rails 的组件处于维护状态。。。

我觉得 stimulusjs 把 JS 代码组织好了,再来个量身打造的测试框架就完美了

del.icio.us 倒手几次,没想到最后的结局居然是被 pinboard 的作者给收购了。pinboard 只是一个 one person business

jasl 回复

他们新的不错 还没有投放到社区

lazybios 回复

好惨

哇 现在都不玩 bootstrap 上 tailwindcss 了呀

jicheng1014 回复

bootstrap 审美疲劳了

hooopo 回复

写一下 Tailwind CSS,你会重新爱上 Boostrap 的。。

ericguo 回复

我以为是因为我 css 水平不行

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");
}

从这里看出来 tailwind 在 js 里面调用很麻烦。如果是 BEM 风格就是

this.svgTarget.classList.toggle("like-icon--active", data["like"])

另外 tailwind 不能解决开发者设计能力的问题,要快速做原型不如用 bootstrap。

Rei 回复

用 apply 可以简化一点儿

不折腾 学 React 很容易的,一步到位

huacnlee 回复

Stimulus 优在和服务端渲染完美配合,无论怎么插入组件只要有 data-controller 属性就会自动绑定,不用关心 js 绑定的时机。

huacnlee 回复

主要大多数人认为所谓的 react 是 react + redux + react router + immutable + hook + webpack plugins + fetch, axios 啥的

而且一上就要从头开始 搞个 all in react 的前端项目, 光弄明白初始化配置, 刚接触的人一个星期都不一定能搞定。

其实 react 可以单纯的在某个局部使用, 只依赖 react.js 甚至连 webpack 都不用配置, 直接怼进去都可以,

一旦意识到 其实 react 的本质就是 state => ui 后, 就可以在觉得依赖数据变化的 UI component 使用 react 了

Rei 回复

tailwind 最好的地方就是 你想咋变就咋变

bootstrap 我真记不住他的 css 结构 每次改个样式都很麻烦

jicheng1014 回复

同意。All In React => Pain.

炮哥要不要考虑用https://ruby-china.org/topics/40334 重写一个😁

stimulus 涉及到列表渲染的时候,还是非常麻烦,但恰恰前端又最需要这个。我觉得现在前端 ssr 框架(next nuxt)的方便程度已经让 erb 没有任何优势了。另外给看到的避个雷,现在的 react 比起 vue3 基本没有值得推荐的地方,vue3 的 hooks 完美解决了 react hooks 需要的心智负担,但是却能做到同样的事情,唯一的缺陷就是 vue3 还没 release 了。

mizuhashi 回复

列表渲染为什么麻烦 我还没遇到

hooopo 回复

我一时也想不到怎么说,就是 stimulus 的话 data -> ui 这个过程是在服务器发生的,但是很多时候我们为了性能/响应更快,希望在前端作出反应,这个时候就需要在前端增减 class 什么的,这个很反现代前端的响应式原则。毕竟 stimulus 只是处理了事件绑定的工作,没有管其他前端库管的 if 和 for 这些 data -> ui 的东西,然后一旦需要用到,就会开始后悔用 stimulus 了。

mizuhashi 回复

有时间可以找个例子来 因为我 js 也不太熟 也不知道 stimulus 的局限在哪里 但我使用下来感觉还没有什么场景是特别复杂的 至于是不是现代前端思维我不太 care 因为这东西只是一个潮流 就像最开始结构表现行为分离是主旋律 现在也没人提了 现在什么 js compoent,css in js,tailwind 这种 css in html 都是打破了之前的思维方式……

react / vue 才是出路

kikyous 回复

简单的或者需要 seo 的用 stimulus, 复杂的用 react/vue

kikyous 回复

学不动了

jicheng1014 回复

直接怼个 React Component 的确很方便,这也是 Pure React 的优点之一,也降低了新手学习成本。但是 React 的真正威力难道不是要搭配着所谓 React 生态一起使用才能发挥出来吗?拿 Rails 来说,在某些局部使用 React 不是不可以,但如果只在某个局部使用就有点为了 React 而 React 的意思了,如果其他地方还在用着 turbolinks + stimulus 就更不伦不类了,turbolinks + stimulus 本身就已经是非常好的解决方案了。真正的差别,我认为是 React 或者 Vue 所代表的现代前端的思维方式,这个跟 stimulus 是本质上不同的,如果你认同,就应该整体切换到大前端思路,反之,就可以直接 Rails 全家桶,而不太适合混着用。

SpiderEvgn 回复

不太赞同 “但如果只在某个局部使用就有点为了 React 而 React 的意思了”

react 的本质是 state => ui, state 变 ui 变

如果 有较高密度的 数据变化引起的 ui 变化, 就适合 react

所谓 react 生态我是觉得挑自己想要的就好 不需要全站数据刷新的时候就没必要上 redux, 量少 localstorage 也是够用的。

也可以参考下这里

https://ruby-china.org/topics/38603

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

其实可以用 service worker 来处理一些 URL,然后就不会访问到服务器啦。具体例子在:

https://github.com/turbolinks/turbolinks/issues/468

luikore 回复

网页加速器!!谷歌好多年前有这么一个插件

Rei 回复

我甚至搞了个 stimulus-bind 去做绑定,然后就能类似 vue / react 的写 class,不用手动 add / remove class 了

不过由于实现过于简陋,要触发更新得更新 root object。或许现在有更好的办法

不过 Stimulus 还是有些比较难搞的问题,controller 其实并没有很好的 scope 控制。

重交互的页面或者树形的节点,得把 controller 放在比较高的节点里,并且最好只出现一次。

Turbolinks 还有返回的页面过大,渲染过多消耗服务器 CPU 和传输带宽的问题。

一些 render partial 可以用 custom element 代替,那样返回的内容就接近 json ajax 了,只是你用 HTML 格式化而已。

不过 custom element 也有难用的地方……

hooopo 回复

学着起来其实很快,两年前我试了下 vue, 很快就能搬砖了。

不过很久没搬,现在好像又忘记了 😂 ,脑子里忘不掉的就是 jQuery 大法,这个太根深蒂固了,估计忘不了了😅

huobazi 回复

是初恋

https://github.com/rubyapi/rubyapi tailwind+Stimulusjs+graphql 的开源项目

hooopo 回复

不是,是在客户端造个假的 API 例如用 msw https://mswjs.io/docs/

假设你有个 API 不想走服务器渲染了,就在客户端 mock 之,修改之前服务器返回的响应,交给 mock service worker 就可以了

不喜欢 tailwindcss 的 CSS 命名风格

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册