有很多javascript框架。有时我什至开始认为我是唯一一个还没有创建框架的人。一些解决方案,比如angular,庞大而复杂,而一些,比如backbone(它更像是一个库而不是一个框架),非常简单,只提供了一些工具来加速开发过程。
在今天的文章中,我想向您介绍一个名为Stimulus的全新框架。它是由 David Heinemeier Hansson 领导的Basecamp团队创建的,他是一位受欢迎的开发人员,他是 Ruby on Rails之父。
Stimulus 是一个小框架,从未打算成长为大框架。它对前端开发有自己的理念和态度,一些程序员可能喜欢或不喜欢。Stimulus 还很年轻,但版本 1 已经发布,因此在生产中使用应该是安全的。我已经玩过这个框架了,真的很喜欢它的简单和优雅。希望你也会喜欢它!
在这篇文章中,我们将讨论Stimulus的基础知识,同时创建一个具有异步数据加载、事件、状态持久性和其他常见事物的单页应用程序。
源代码可以在GitHub上找到。
刺激简介
Stimulus由 Basecamp 的开发人员创建。他们没有创建单页 JavaScript 应用程序,而是决定选择一个 由Turbolinks和一些 javaScript提供支持的庞大单体。这段 JavaScript 代码演变成一个小而适中的框架,不需要您花费数小时学习它的所有概念和注意事项。
Stimulus 主要用于将自身附加到现有的dom元素并以某种方式与它们一起工作。但是,也可以动态呈现内容。总而言之,这个框架与其他流行的解决方案有很大不同,例如,它在 html 中保留状态,而不是在 JavaScript 对象中。一些开发人员可能会觉得这很不方便,但请给 Stimulus 一个机会,因为它真的可能会让您大吃一惊。
该框架只有三个您应该记住的主要概念,它们是:
控制器:带有一些附加到 DOM 的方法和回调的JS 类。当页面上出现“魔术”属性时,附件就会发生。
data-controller
文档解释说这个属性是 HTML 和 JavaScript 之间的桥梁,就像类充当 HTML 和css之间的桥梁一样。一个控制器可以连接多个元素,一个元素可以由多个控制器供电。Actions:在特定事件上调用的方法。它们在特殊
data-action
属性中定义。目标:可以轻松访问和操作的重要元素。它们是在
data-target
属性的帮助下指定的。
如您所见,上面列出的属性 允许您以非常简单自然的方式将内容与行为逻辑分开。在本文后面,我们将看到所有这些概念的实际应用,并注意阅读 HTML 文档和理解发生的事情是多么容易。
引导刺激应用程序
Stimulus 可以很容易地作为NPM 包script
安装或通过标签直接加载, 如文档中所述。另请注意,默认情况下,此框架与 webpack 资产管理器集成,它支持控制器自动加载等好东西。您可以自由使用任何其他构建系统,但在这种情况下 需要做更多的工作。
开始使用 Stimulus 的最快方法是利用这个已经连接了Express Web 服务器和Babel的启动项目。它也依赖于Yarn,所以一定要安装它。要克隆项目并安装其所有依赖项,请运行:
git clone https://github.com/stimulusjs/stimulus-starter.git cd stimulus-starter yarn install
如果你不想在本地安装任何东西,你可以在 Glitch 上重新混合这个项目,并在你的浏览器中完成所有的编码。
太好了——我们都准备好了,可以继续下一部分了!
一些标记
假设我们正在创建一个小型单页应用程序,它显示员工列表并加载诸如姓名、照片、职位、薪水、生日等信息。
让我们从员工名单开始。我们要编写的所有标记都应该放在public/index.html
文件中,该文件已经包含一些非常小的 HTML。目前,我们将通过以下方式对所有员工进行硬编码:
<h1>Our employees</h1> <div> <ul> <li><a href="#">John Doe</a></li> <li><a href="#">Alice Smith</a></li> <li><a href="#">Will Brown</a></li> <li><a href="#">Ann Grey</a></li> </ul> </div>
好的!现在让我们添加一点刺激魔法。
创建控制器
正如官方文档所解释的,Stimulus 的主要目的是将 JavaScript 对象(称为控制器)连接到 DOM 元素。然后,控制器将使页面栩栩如生。按照惯例,控制器的名称应该以后缀结尾_controller
(Rails 开发人员应该非常熟悉)。
有一个已经可用的控制器目录,称为src/controllers
. 在里面,你会发现一个 hello_controller.js
定义了一个空类的文件:
import { Controller } from "stimulus" export default class extends Controller { }
让我们将此文件重命名为employees_controller.js
. 我们不需要特别要求它,因为控制器会自动加载,这要归功于src/index.js
文件中的以下代码行:
const application = Application.start() const context = require.context("./controllers", true, /\.js$/) application.load(definitionsFromContext(context))
下一步是将我们的控制器连接到 DOM。为此,设置一个data-controller
属性并为其分配一个标识符(employees
在我们的例子中):
<div data-controller="employees"> <ul> <!-- your list --> </ul> </div>
而已!控制器现在已附加到 DOM。
生命周期回调
关于控制器需要了解的一件重要事情是,它们具有三个在特定条件下触发的生命周期回调:
initialize
: 这个回调只发生一次,当控制器被实例化时。connect
: 每当我们将控制器连接到 DOM 元素时触发。由于一个控制器可能会连接到页面上的多个元素,因此该回调可能会运行多次。disconnect
: 你可能已经猜到了,只要控制器与 DOM 元素断开连接,这个回调就会运行。
没什么复杂的,对吧?让我们利用initialize()
和connect()
回调来确保我们的控制器确实有效:
// src/controllers/employees_controller.js export default class extends Controller { initialize() { console.log('Initialized') console.log(this) } connect() { console.log('Connected') console.log(this) } }
接下来,通过运行启动服务器:
yarn start
导航到http://localhost:9000
。打开浏览器的控制台并确保显示两条消息。这意味着一切都按预期工作!
添加事件
下一个核心刺激概念是事件。事件用于响应页面上的各种用户操作:单击、悬停、聚焦等。Stimulus 不尝试重新发明自行车,其事件系统基于通用 JS 事件。
例如,让我们将点击事件绑定到我们的员工。每当发生此事件时,我想调用尚未存在choose()
的方法employees_controller
:
<ul> <li><a href="#" data-action="click->employees#choose">John Doe</a></li> <li><a href="#" data-action="click->employees#choose">Alice Smith</a></li> <li><a href="#" data-action="click->employees#choose">Will Brown</a></li> <li><a href="#" data-action="click->employees#choose">Ann Grey</a></li> </ul>
大概,你可以自己理解这里发生了什么。
data-action
是将事件绑定到元素并解释应该调用什么动作的特殊属性。click
,当然是事件的名称。employees
是我们控制器的标识符。choose
是我们要调用的方法的名称。
由于click
是最常见的事件,因此可以安全地省略:
<li><a href="#" data-action="employees#choose">John Doe</a></li>
在这种情况下, click
将隐式使用。
接下来,让我们编写choose()
方法。我不希望发生默认操作(显然,打开href
属性中指定的新页面),所以让我们阻止它:
// src/controllers/employees_controller.js // callbacks here... choose(e) { e.preventDefault() console.log(this) console.log(e) }
e
是包含有关触发事件的完整信息的特殊事件对象。请注意,顺便说一句,它this
返回控制器本身,而不是单个链接!为了获得对作为事件目标的元素的访问权限,请使用e.target
.
重新加载页面,单击列表项,然后观察结果!
与***合作
现在我们已经为员工绑定了一个点击事件处理程序,我想存储当前选择的人。为什么?存储此信息后,我们可以防止第二次选择同一员工。这将使我们稍后也可以避免多次加载相同的信息。
Stimulus 指示我们在Data api中保持状态,这似乎很合理。首先,让我们使用 data-id
属性为每个员工提供一些任意的 id:
<ul> <li><a href="#" data-id="1" data-action="employees#choose">John Doe</a></li> <li><a href="#" data-id="2" data-action="click->employees#choose">Alice Smith</a></li> <li><a href="#" data-id="3" data-action="click->employees#choose">Will Brown</a></li> <li><a href="#" data-id="4" data-action="click->employees#choose">Ann Grey</a></li> </ul>
接下来,我们需要获取id 并将其持久化。在 Stimulus 中使用 Data api很常见,因此this.data
为每个控制器提供了一个特殊的对象。在它的帮助下,我们可以运行以下方法:
this.data.get('name')
:通过其属性获取值。this.data.set('name', value)
:设置某个属性下的值。this.data.has('name')
: 检查属性是否存在(返回一个布尔值)。
不幸的是,这些快捷方式不适用于点击事件的目标,因此我们必须坚持使用getAttribute()
它们:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() this.data.set("current-employee", e.target.getAttribute('data-id')) }
但是我们可以通过创建一个 getter 和一个 setter 来做得更好currentEmployee
:
// src/controllers/employees_controller.js get currentEmployee() { return this.data.get("current-employee") } set currentEmployee(id) { if (this.currentEmployee !== id) { this.data.set("current-employee", id) } }
请注意我们如何使用 this.currentEmployee
getter 并确保提供的 id 与已存储的不同。
现在您可以通过以下方式重写该choose()
方法:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() this.currentEmployee = e.target.getAttribute('data-id') }
重新加载页面以确保一切正常。您还不会注意到任何视觉上的变化,但在 Inspector 工具的帮助下,您会注意到当您单击链接时,ul
该属性的值会发生变化。data-employees-current-employee
属性名称中的employees
部分是控制器的标识符,并且会自动添加。
现在让我们继续并突出显示当前选择的员工。
使用目标
选择员工后,我想为相应的元素分配一个.chosen
类。当然,我们可能已经通过使用一些 JS 选择器函数解决了这个任务,但是 Stimulus 提供了一个更简洁的解决方案。
满足目标,允许您在页面上标记一个或多个重要元素。然后可以根据需要轻松访问和操作这些元素。为了创建一个目标,添加一个data-target
值为 的属性{controller}.{target_name}
(称为 目标描述符):
<ul data-controller="employees"> <li><a href="#" data-target="employees.employee" data-id="1" data-action="employees#choose">John Doe</a></li> <li><a href="#" data-target="employees.employee" data-id="2" data-action="click->employees#choose">Alice Smith</a></li> <li><a href="#" data-target="employees.employee" data-id="3" data-action="click->employees#choose">Will Brown</a></li> <li><a href="#" data-target="employees.employee" data-id="4" data-action="click->employees#choose">Ann Grey</a></li> </ul>
现在通过定义一个新的静态值让 Stimulus 了解这些新目标:
// src/controllers/employees_controller.js export default class extends Controller { static targets = [ "employee" ] // ... }
我们现在如何访问目标?就像说this.employeeTarget
(获取第一个元素)或this.employeeTargets
(获取所有元素)一样简单:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() this.currentEmployee = e.target.getAttribute('data-id') console.log(this.employeeTargets) console.log(this.employeeTarget) }
伟大的!这些目标现在如何帮助我们?好吧,我们可以根据一些标准使用它们轻松地添加和删除 CSS 类:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() this.currentEmployee = e.target.getAttribute('data-id') this.employeeTargets.forEach((el, i) => { el.classList.toggle("chosen", this.currentEmployee === el.getAttribute("data-id")) }) }
这个想法很简单:我们遍历一个目标数组,并将每个目标与data-id
存储在this.currentEmployee
. 如果它匹配es,则为元素分配.chosen
类。否则,将删除此类。您也可以 if (this.currentEmployee !== id) {
从 setter 中提取条件并在chosen()
方法中使用它:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() const id = e.target.getAttribute('data-id') if (this.currentEmployee !== id) { // <--- this.currentEmployee = id this.employeeTargets.forEach((el, i) => { el.classList.toggle("chosen", id === el.getAttribute("data-id")) }) } }
看起来不错!最后,我们将为.chosen
类中的类提供一些非常简单的样式public/main.css
:
.chosen { font-weight: bold; text-decoration: none; cursor: default; }
再次重新加载页面,单击一个人,并确保该人被正确突出显示。
异步加载数据
我们的下一个任务是加载有关所选员工的信息。在现实世界的应用程序中,您必须设置一个 托管服务提供商、一个由Django或Rails等支持的后端,以及一个使用包含所有必要数据的 JSON 进行响应的API 端点。但是我们将让事情变得更简单一些,并且只关注客户端。在文件夹下创建一个employees
目录public
。接下来,添加四个包含个别员工数据的文件:
1.json
{ "name": "John Doe", "gender": "male", "age": "40", "position": "CEO", "salary": "$120.000/year", "image": "https://burst.shopifycdn.com/photos/couple-in-love-at-sunset_373x.jpg" }
2.json
{ "name": "Alice Smith", "gender": "female", "age": "32", "position": "CTO", "salary": "$100.000/year", "image": "https://burst.shopifycdn.com/photos/woman-listening-at-team-meeting_373x.jpg" }
3.json
{ "name": "Will Brown", "gender": "male", "age": "30", "position": "Tech Lead", "salary": "$80.000/year", "image": "https://burst.shopifycdn.com/photos/casual-urban-menswear_373x.jpg" }
4.json
{ "name": "Ann Grey", "gender": "female", "age": "25", "position": "Junior Dev", "salary": "$20.000/year", "image": "https://burst.shopifycdn.com/photos/woman-using-tablet_373x.jpg" }
所有照片均取自 Shopify 名为Burst的免费图库摄影。
我们的数据已准备就绪,等待加载!为此,我们将编写一个单独的 loadInfoFor()
方法:
// src/controllers/employees_controller.js loadInfoFor(employee_id) { fetch(`employees/${employee_id}.json`) .then(response => response.text()) .then(json => { this.displayInfo(json) }) }
此方法接受员工的 id 并向给定的 URI发送异步获取请求。还有两个 Promise:一个是获取正文,另一个是显示加载的信息(稍后我们将添加相应的方法)。
在里面使用这个新方法choose()
:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() const id = e.target.getAttribute('data-id') if (this.currentEmployee !== id) { this.loadInfoFor(id) // ... } }
在对方法进行编码之前displayInfo()
,我们需要一个元素来实际呈现数据。为什么我们不再次利用目标?
<!-- public/index.html --> <div data-controller="employees"> <div data-target="employees.info"></div> <ul> <!-- ... --> </ul> </div>
定义目标:
// src/controllers/employees_controller.js export default class extends Controller { static targets = [ "employee", "info" ] // ... }
现在利用它来显示所有信息:
// src/controllers/employees_controller.js displayInfo(raw_json) { const info = JSON.parse(raw_json) const html = `<ul><li>Name: ${info.name}</li><li>Gender: ${info.gender}</li><li>Age: ${info.age}</li><li>Position: ${info.position}</li><li>Salary: ${info.salary}</li><li><img src="${info.image}"></li></ul>` this.infoTarget.innerHTML = html }
当然,您可以自由地使用像handlebars这样的模板引擎,但对于这种简单的情况,这可能是矫枉过正的。
现在重新加载页面并选择其中一名员工。他的简历和图片应该几乎立即加载,这意味着我们的应用程序运行正常!
员工动态列表
使用上述方法,我们可以更进一步,即时加载员工列表,而不是对其进行硬编码。
准备文件内的数据public/employees.json
:
[ { "id": "1", "name": "John Doe" }, { "id": "2", "name": "Alice Smith" }, { "id": "3", "name": "Will Brown" }, { "id": "4", "name": "Ann Grey" } ]
现在public/index.html
通过删除硬编码列表并添加一个data-employees-url
属性来调整文件(请注意,我们必须提供控制器的名称,否则 Data API 将无法工作):
<div data-controller="employees" data-employees-url="/employees.json"> <div data-target="employees.info"></div></div>
一旦控制器被附加到 DOM,它应该发送一个获取请求来构建一个员工列表。这意味着connect()
回调是执行此操作的理想场所:
// src/controllers/employees_controller.js connect() { this.loadFrom(this.data.get('url'), this.displayEmployees) }
我建议我们创建一个更通用的loadFrom()
方法,该方法接受一个从 URL 加载数据和一个回调来实际呈现这些数据:
// src/controllers/employees_controller.js loadFrom(url, callback) { fetch(url) .then(response => response.text()) .then(json => { callback.call( this, JSON.parse(json) ) }) }
调整choose()
方法以利用loadFrom()
:
// src/controllers/employees_controller.js choose(e) { e.preventDefault() const id = e.target.getAttribute('data-id') if (this.currentEmployee !== id) { this.loadFrom(`employees/${id}.json`, this.displayInfo) // <--- this.currentEmployee = id this.employeeTargets.forEach((el, i) => { el.classList.toggle("chosen", id === el.getAttribute("data-id")) }) } }
displayInfo()
也可以简化,因为现在正在解析 JSON loadFrom()
:
// src/controllers/employees_controller.js displayInfo(info) { const html = `<ul><li>Name: ${info.name}</li><li>Gender: ${info.gender}</li><li>Age: ${info.age}</li><li>Position: ${info.position}</li><li>Salary: ${info.salary}</li><li><img src="${info.image}"></li></ul>` this.infoTarget.innerHTML = html }
删除 loadInfoFor()
并编码该displayEmployees()
方法:
// src/controllers/employees_controller.js displayEmployees(employees) { let html = "<ul>" employees.forEach((el) => { html += `<li><a href="#" data-target="employees.employee" data-id="${el.id}" data-action="employees#choose">${el.name}</a></li>` }) html += "</ul>" this.element.innerHTML += html }
而已!我们现在根据服务器返回的数据动态呈现我们的员工列表。
结论
在本文中,我们介绍了一个名为 Stimulus 的适度 JavaScript 框架。我们已经看到了如何创建一个新的应用程序,添加一个带有一堆回调和动作的控制器,以及引入事件和动作。此外,我们在获取请求的帮助下完成了一些异步数据加载。
- 一些标记
- 生命周期回调
- 员工动态列表