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 中 onclick
、onchange
一类的语法。
<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
}
}
以这个例子来说,程序执行的流程是这样的:
- 用户点击 Add 按钮后,按钮触发的
click
事件触发了对todos
controller 的added
的方法的回调; -
added
方法执行完自身的核心逻辑后,通过调用this.dispatch
方法触发todos:added
事件,注意这里的todos:
前缀是框架自动加上的; -
todos:added
事件的产生,触发了对submitter
controller 的submit
方法的回调。
就这样,通过抽象出不同的 controllers,实现逻辑的分离和解耦,再通过事件机制,将逻辑实现拼装和编排。
理解了上面这 4 个核心概念,就足以使用 stimulus.js
开发出一个交互相对简单的前端逻辑了。当然,stimulus.js
还有其他几个概念,但是在我看来只是一些锦上添花的功能,这里就没必要赘述了。
也谈谈 stimulus.js
不适合的场景
尽管是一个小项目,但是在使用 stimulus.js
的过程中,也遇到了一些觉得比较繁琐的问题,这些问题体现在:
-
缺乏 DOM 操作的封装:因为
stimulus.js
只提供 HTML 元素与 JavaScript 对象之间的绑定,并没有提供对 DOM 操作的封装,所以在需要操作 DOM 的时候,就会经常需要直接使用原生 DOM 对象的操作,比如Element.classList.add()
一类,如果是在早期的浏览器中,还需要担心兼容性问题等,但是好在现在的浏览器兼容性问题已经少了很多,这倒不是太大的问题; -
缺乏前端渲染的支持:因为
stimulus.js
中的绑定并非双向绑定,在一些需要根据 JavaScript 对象渲染不同页面内容或者视觉效果的情况下,如果不借助其他框架的支持,就只能编写各种字符串插值,以及Element.innerHTML = xxxx
的代码,同样效率比较低。
所以,总结来说,如果你的前端页面是一个重交互的页面,可能只使用 stimulus.js
并不是一个明智之选。以我自己来选择的话,如果是一些内容类的轻交互场景,比如博客或者论坛,一般需要交互的就是评论区,简单的文本输入以及追加展示等,我觉得用 stimulus.js
会比较舒服,轻量,又是原汁原味的 DOM;但是其他情况下,我可能会直接上 vue.js
之类功能更全面的框架,最大程度减少在页面与逻辑之间状态同步的代码。
使用 stimulus.js
踩过的坑
-
在 Controllers scope 之外的 action 无法回调到 Controller 的方法
这个问题最开始排查了一些时间,一直没想明白为什么,后来才顿悟,原来是因为踩了 Controller scope 的坑。因为我的 action 声明需要回调的 controller 在当前 DOM 中不在可见范围,于是触发回调失败。 -
先于 controller 初始化前触发的 action 无法回调到 Controller 的方法
这个问题是因为我在代码中声明了一个 action,并且在 controller 中也执行了
dispatch
,但是此时因为目标的 controller 还没有初始化,导致看似代码没有任何语法或者使用错误,但是 action 无可能触发回调成功。