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。