cover

stimulus.js 初体验

stimulus.js 框架是一个轻量的 JavaScript 框架,由大名鼎鼎的 Basecamp 公司开发,也就是 Ruby on Rails 框架核心开发团队所在的公司。老早就听说了 stimulus.js 框架,但是没有实际使用过。最近刚好在自己的一个小项目中有了实践的机会,有了一些心得体会,总结分享一下。

提醒:如果想快速体验 stimulus.js 做出来的 demo,可以看看这个 todomvc-stimulus

一个克制的前端 JavaScript 框架

谈起对 stimulus.js 框架总的印象,我觉得这是一个非常克制的前端 JavaScript 框架。它聚焦于在 HTML 元素与 JavaScript 对象的绑定这件事情上,并且这种绑定是单向的,不是前端开发中早已非常普遍的双向绑定。除此之外,它没有提供其他额外的功能。

由于它的克制,轻量是它必然而然的第一个优点。其次,配合其所设计的 controller 的概念,可以实现交互逻辑里状态的隔离与解耦。最后,它在 controller 代码的组织上,也让熟悉 Rails 开发的人感到亲切:约定大于配置。每个 controller 的定义,都需要按照约定,一个 controller 对应一个文件,放在 controllers 目录下,且文件名与 controller 的名字一致。

Stimulus.js 的轻量

Stimulus.js 的核心概念非常少,想要上手 stimulus.js 框架的使用,只有 4 个核心概念是需要了解的。

Controllers

Controllers 是声明了诸如 data-controller="todos" 这样的 data 属性的 HTML 元素所绑定的 JavaScript 对象:

<div data-controller="todos"></div>

stimulus.js 会自动为所有此类元素实例化对应的 controller,每个此类元素各自绑定一个实例。以上述例子来说,stimulus.js 会自动查找位于 app/javascript/controllers/todos_controller.js 的文件,并且导入其中导出的默认类,这是一个经典的约定大于配置的做法:

// app/javascript/controllers/todos_controller.js

import { Controller } from 'stimulus';
export default class extends Controller {
  connect() {
  }
}

当然,如果不想使用或者无法使用约定的形式,也可以通过 stimulus.js 提供的函数进行 controller 的显式注册:

application.register("todos", TodosController)

这类元素以及其子孙元素,都是元素绑定的 controller 的可见范围。也就是说,在 stimulus.js 框架中,controller 的各种操作,只能作用于 controller 绑定的元素以及此元素的子孙元素。这个原则同样适用于嵌套 Controllers 的情况下。

Controllers 之间可以通过事件的方式相互协作,这个会在后面讲 Actions 的时候再补充讲一下。

Targets

Targets 是另一种在 HTML 元素与 JavaScript 对象之间实现绑定的方法,但是它作用于具体的 Controller 之下:

<div data-controller="todos">
    <!-- ... -->

    <button data-todos-target="addBtn">Add</button>
</div>

声明 target 的规则是 data-<controller>-target=<target-name>,相应的,需要在 controller 声明绑定的对象:

// app/javascript/controllers/todos_controller.js

import { Controller } from 'stimulus';
export default class extends Controller {
  stitic target = ["addBtn"]

完成了 target 的绑定之后,就可以按照 stimulus.js 对 targets 的约定,在 controllers 方法中使用类似 this.addBtnTarget 或者 this.addBtnTargets (针对有多个 HTML 元素绑定了同一个 Target 的情况)来访问这些绑定的 HTML 元素了。

Controllers 和 Targets 的生命周期回调

上面说的 Controllers 和 Targets,都是 HTML 元素和 JavaScript 对象之间的绑定功能,因为 HTML 元素随着浏览器的加载以及后续的 DOM 操作,就带来了一个问题,这些对象的生命周期是怎样的?

这几个生命周期的回调函数都与 DOM 的变化紧密相关,一般来说看这几个条件:

  • 元素是否存在?
  • 绑定的标识是否存在元素的属性列表中,比如 data-controller 或者 data-<controller>-target

当条件从部分或者全部不满足变为满足时,则 connect 类型的回调函数被调用;相反,如果由于 DOM 的一些操作导致不再满足全部条件时,disconnect 类事件的回调函数被调用。

controllers 可以在 connect() 回调中定义初始化的工作,比如 controller 的一些状态的初始化,相应的,[name]TargetConnected 也可以用于某个 target 的初始化。

Actions

Actions 是 stimulus.js 中的事件回调机制,类似 HTML 中 onclickonchange 一类的语法。

<div data-controller="todos">
    <!-- ... -->

    <button data-todos-target="addBtn" data-action="click->todos#add">Add</button>
</div>

Actions 支持多个事件回调声明,这样同时也方便了实现 Controllers 之间的协作:

<div data-controller="todos submitter">  <!-- 注意,这里使用了多个 controller 绑定 -->
    <!-- ... -->

    <button data-todos-target="addBtn" data-action="click->todos#add todos:added->submitter#submit">Add</button>
</div>
// app/javascript/controllers/todos_controller.js
import { Controller } from 'stimulus';
export default class extends Controller {
  add() {
    // do something to maintain the status of todos controller
    this.dispatch("added", {detail: {todos: [xxxx]}}}})
  }
}

// app/javascript/controllers/submitter_controller.js
import { Controller } from 'stimulus';
export default class extends Controller {
  submit(event) {
    const todos = event.detail.todos;   // extract event data
    // do something else
  }
}

以这个例子来说,程序执行的流程是这样的:

  1. 用户点击 Add 按钮后,按钮触发的 click 事件触发了对 todos controller 的 added 的方法的回调;
  2. added 方法执行完自身的核心逻辑后,通过调用 this.dispatch 方法触发 todos:added 事件,注意这里的 todos: 前缀是框架自动加上的;
  3. todos:added 事件的产生,触发了对 submitter controller 的 submit 方法的回调。

就这样,通过抽象出不同的 controllers,实现逻辑的分离和解耦,再通过事件机制,将逻辑实现拼装和编排。

理解了上面这 4 个核心概念,就足以使用 stimulus.js 开发出一个交互相对简单的前端逻辑了。当然,stimulus.js 还有其他几个概念,但是在我看来只是一些锦上添花的功能,这里就没必要赘述了。

也谈谈 stimulus.js 不适合的场景

尽管是一个小项目,但是在使用 stimulus.js 的过程中,也遇到了一些觉得比较繁琐的问题,这些问题体现在:

  1. 缺乏 DOM 操作的封装:因为 stimulus.js 只提供 HTML 元素与 JavaScript 对象之间的绑定,并没有提供对 DOM 操作的封装,所以在需要操作 DOM 的时候,就会经常需要直接使用原生 DOM 对象的操作,比如 Element.classList.add() 一类,如果是在早期的浏览器中,还需要担心兼容性问题等,但是好在现在的浏览器兼容性问题已经少了很多,这倒不是太大的问题;
  2. 缺乏前端渲染的支持:因为 stimulus.js 中的绑定并非双向绑定,在一些需要根据 JavaScript 对象渲染不同页面内容或者视觉效果的情况下,如果不借助其他框架的支持,就只能编写各种字符串插值,以及 Element.innerHTML = xxxx 的代码,同样效率比较低。

所以,总结来说,如果你的前端页面是一个重交互的页面,可能只使用 stimulus.js 并不是一个明智之选。以我自己来选择的话,如果是一些内容类的轻交互场景,比如博客或者论坛,一般需要交互的就是评论区,简单的文本输入以及追加展示等,我觉得用 stimulus.js 会比较舒服,轻量,又是原汁原味的 DOM;但是其他情况下,我可能会直接上 vue.js 之类功能更全面的框架,最大程度减少在页面与逻辑之间状态同步的代码。

使用 stimulus.js 踩过的坑

  1. 在 Controllers scope 之外的 action 无法回调到 Controller 的方法
    这个问题最开始排查了一些时间,一直没想明白为什么,后来才顿悟,原来是因为踩了 Controller scope 的坑。因为我的 action 声明需要回调的 controller 在当前 DOM 中不在可见范围,于是触发回调失败。
  2. 先于 controller 初始化前触发的 action 无法回调到 Controller 的方法 这个问题是因为我在代码中声明了一个 action,并且在 controller 中也执行了 dispatch,但是此时因为目标的 controller 还没有初始化,导致看似代码没有任何语法或者使用错误,但是 action 无可能触发回调成功。

相关资料链接

2
1