Rails 开发者应该拥抱 Web Component

Rails 8 将会继续将 Hotwire 作为默认,我觉得这很好。Hotwire 是以服务端渲染为核心的前端方案,由于服务端是数据的根源,大部分应用可以通过服务端渲染解决问题而不用考虑数据同步。

不过能做不表示最优,还是有一些问题需要在客户端处理,这通常是涉及客户端状态和前端渲染。举个例子,多选输入框。当前 Geeknote 的标签输入使用了 hotwire_combobox 这个库,它充分利用了 hotwire 服务端渲染的特性,用一种聪明的方式实现了多选输入框。但如果网络状况不好,会发现输入会有较大延迟:

我没有人为降低网络延迟,从中国访问境外网站就是这个状态。

这里的问题在于在将输入转为 chip 的时候,hotwire_combobox 使用了服务端渲染,而这实际可以在客户端完成,因为展示补全列表的时候已经获得渲染所需要的数据了。当然 hotwire_combobox 可能以后会改为客户端渲染,这时候会发现需要处理两个问题:客户端状态和渲染。由于 hotwire 并不包含客户端渲染的功能,如果纯用 JavaScript 处理会非常痛苦。考虑一下如何管理可选项、已输入、当前选择,并且反应到 UI 上?

这时候就要考虑前端方案了。

前端组件

在过去十年的前端框架混战中,“组件”概念取得了共识。所谓组件,是指一块包含了样式、状态和功能的前端模块。组件可以包含更多组件,组件间也可以通信。以组件的方式管理前端代码是当今前端框架的主流。

所以在选择前端方案时,我会更多考虑基于组件的前端方案。

可选的方案

View Component

如果搜索 Rails Component 你会发现 ViewComponent 这个 gem。它本质上是对象化的服务端局部模版,提供了更好的测试接口,但不解决客户端状态和渲染。

React/Vue 等

React/Vue 和其他流行的前端框架,可以很方便的构建前端组件。它们通常提供了响应式属性、声明式模版,可以很好处理客户端状态和渲染。并且这些框架的社区庞大,有大量现成的 UI 库和组件库。

但目前主流的前端框架有一个问题,互操作性不好。React 的组件需要用在 React 项目,Vue 的组件需要用在 Vue 项目。当然,也可以通过增加适配器让不同框架的组件互操作,但很少有人这么做。通常选择了一个框架后,整个应用就都会选择基于这个框架的组件。

也由于这个问题,在 Rails View 中使用 React/Vue 也显得格格不入,需要到处写初始化代码。根据我的经验,在 Rails View 中引入了 React/Vue 之后最终都会走向前后端分离,因为这些组件和 Rails View 无法交互,与其耦合在一起不如彻底分离。

有的人可能觉得这样很好,但我希望前端组件用于增强而不是替换 Rails View,因为 Rails View 擅长于服务端渲染的部分。

Web Component

最终我把目光放在 Web Component,一个浏览器的标准 API。Web Component 允许开发者创建自定义元素,并且像浏览器内置元素一样使用它。

举个例子,可以这样使用自定义元素:

<form action="/posts" method="post">
  <my-combobox name="tabs" value="Ruby,JavaScript" suggeust-url="/tags/suggests"></my-combobox>
</form>

<my-combobox> 是一个自定义元素,它用起来像浏览器内置元素,也会随着表单提交而提交。

我还没有实现 <my-combobox>,也许会在将来实现。

对于 Rails 项目来说更好的是,可以在 Turbo Stream/Broadcast 环境中使用自定义元素,而不需要额外的初始化代码。

直接用浏览器 API 也可以创建自定义元素,但我建议先从 Lit 库开始。

Lit 简介

Lit 是一个开发 Web Component 的库,主要开发人员来自 Google。根据官网的介绍,Lit 的开发团队参与了 Web Component 标准的定制。

Lit 相比浏览器原生接口提供了更多方便开发者的功能,例如:

  • 样式作用域。
  • 反应式属性。
  • 声明式模版。

以下是 Lit 实现一个计数器组件的例子:

import { LitElement, html, css } from 'lit';

class MyCounter extends LitElement {
  static styles = css`
    label {
      color: green;
    }
  `

  static properties = {
    count: { type: Number }
  }

  constructor() {
    super()
    this.count = 0
  }

  increment() {
    this.count += 1
  }

  render() {
    return html`
        <label>${this.count}</label>
        <button @click="${this.increment}">Increment</button>
    `;
  }
}

customElements.define('my-counter', MyCounter);

要注意的是:

  • 组件内的 Style 只对组件内部元素有效,所以 class name 可以写得很简洁。
  • 通过 ${} 插值,通过 @event 绑定事件。
  • count 属性发生变化时,只会更新需要变更的部分。

这些特性能大大简化前端组件的开发。

实践

最近我开发了项目 Geekslide,Lit 用于 UI 组件和幻灯片编辑/播放。

如果没有 Lit,这样的交互纯手工写会非常麻烦。由于 Web Component 出色的互操作性,我可以只在需要的部分使用 Lit 而不用整个替换 Rails View。

我打算在未来更新更多 Lit 实践方面的内容。

资源

最后推荐一些资源。

学习 Lit 最好的去处就是官方文档:https://lit.dev/docs/

Adobe 的 UI 库有 Web Component 版本:https://opensource.adobe.com/spectrum-web-components/

FontAwsome 团队正在开发一个基于 Web Component 的 UI 库:https://backers.webawesome.com/

总结

如果你需要开发高交互的应用,又不想放弃 Rails View 默认栈,不妨试一下 Web Component / Lit。