Существует множество JavaScript-фреймворков. Иногда я даже начинаю думать, что я единственный, кто еще не создал каркас. Некоторые решения, такие как Angular , являются большими и сложными, тогда как некоторые, такие как Backbone (который является скорее библиотекой, чем фреймворком), довольно просты и предоставляют лишь несколько инструментов для ускорения процесса разработки.
В сегодняшней статье я хотел бы представить вам совершенно новый фреймворк под названием Stimulus . Он был создан командой Basecamp во главе с Дэвидом Хейнемайером Ханссоном, популярным разработчиком, который был отцом Ruby on Rails .
Стимул — это маленькая структура, которая никогда не была предназначена для того, чтобы вырасти во что-то большое. Он имеет свою собственную философию и отношение к развитию интерфейса, что может понравиться или не понравиться некоторым программистам. Стимул является молодым, но версия 1 уже выпущена, поэтому она должна быть безопасной для использования в производстве. Я немного поиграл с этим фреймворком, и мне очень понравилась его простота и элегантность. Надеюсь, вам тоже понравится!
В этом посте мы обсудим основы Stimulus при создании одностраничного приложения с асинхронной загрузкой данных, событиями, сохранением состояния и другими общими вещами.
Исходный код можно найти на GitHub .
Введение в Стимул
Стимул был создан разработчиками из Basecamp. Вместо того чтобы создавать одностраничные приложения JavaScript, они решили выбрать величественный монолит на базе Turbolinks и немного JavaScript. Этот код JavaScript превратился в небольшой и скромный фреймворк, который не требует от вас тратить часы на изучение всех его концепций и предостережений.
Стимул в основном предназначен для присоединения к существующим элементам DOM и некоторой работы с ними. Однако возможно также и динамическое отображение содержимого. В целом, этот фреймворк сильно отличается от других популярных решений, так как, например, он сохраняет состояние в HTML, а не в объектах JavaScript. Некоторым разработчикам это может показаться неудобным, но они дают Stimulus шанс, поскольку он действительно может вас удивить.
Фреймворк имеет только три основных понятия, которые вы должны запомнить, а именно:
-
Контроллеры : JS-классы с некоторыми методами и обратными вызовами, которые присоединяются к DOM. Вложение происходит, когда на странице появляется атрибут «magic»
data-controller
. Документация объясняет, что этот атрибут является мостом между HTML и JavaScript, так же как классы служат мостами между HTML и CSS. Один контроллер может быть подключен к нескольким элементам, а один элемент может получать питание от нескольких контроллеров. - Действия : методы, которые будут вызываться для определенных событий. Они определены в специальных атрибутах
data-action
. - Цели : важные элементы, к которым можно легко получить доступ и которыми можно манипулировать. Они указываются с помощью атрибутов
data-target
.
Как видите, перечисленные выше атрибуты позволяют очень просто и естественно отделить контент от логики поведения. Позже в этой статье мы увидим все эти концепции в действии и заметим, как легко читать HTML-документ и понимать, что происходит.
Начальная загрузка приложения стимула
Stimulus может быть легко установлен в виде пакета NPM или загружен непосредственно через тег script
как описано в документации . Также обратите внимание, что по умолчанию эта платформа интегрируется с менеджером ресурсов Webpack , который поддерживает такие полезные функции, как автозагрузка контроллера. Вы можете свободно использовать любую другую систему сборки, но в этом случае потребуется дополнительная работа.
Самый быстрый способ начать работу со Stimulus — использовать этот стартовый проект, в котором есть веб-сервер Express и уже подключен Babel . Это также зависит от пряжи , поэтому обязательно установите его. Чтобы клонировать проект и установить все его зависимости, запустите:
1
2
3
|
git clone https://github.com/stimulusjs/stimulus-starter.git
cd stimulus-starter
yarn install
|
Если вы предпочитаете не устанавливать что-либо локально, вы можете сделать ремикс этого проекта на Glitch и выполнить все кодирование прямо в браузере.
Отлично — у нас все готово и мы можем перейти к следующему разделу!
Некоторая разметка
Предположим, мы создаем небольшое одностраничное приложение, которое представляет список сотрудников и загружает такую информацию, как их имя, фотография, должность, зарплата, дата рождения и т. Д.
Начнем со списка сотрудников. Вся разметка, которую мы собираемся написать, должна быть помещена в файл public/index.html
, в котором уже есть какой-то очень минимальный HTML. На данный момент мы закодируем всех наших сотрудников следующим образом:
01
02
03
04
05
06
07
08
09
10
|
<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
который определяет пустой класс:
1
2
3
4
5
|
import { Controller } from «stimulus»
export default class extends Controller {
}
|
Давайте переименуем этот файл в employees_controller.js
. Нам не нужно специально требовать этого, потому что контроллеры загружаются автоматически благодаря следующим строкам кода в файле src/index.js
:
1
2
3
|
const application = Application.start()
const context = require.context(«./controllers», true, /\.js$/)
application.load(definitionsFromContext(context))
|
Следующим шагом является подключение нашего контроллера к DOM. Для этого установите атрибут data-controller
и присвойте ему идентификатор (в нашем случае это employees
):
1
2
3
4
5
|
<div data-controller=»employees»>
<ul>
<!— your list —>
</ul>
</div>
|
Это оно! Контроллер теперь подключен к DOM.
Обратные вызовы жизненного цикла
О контроллерах важно знать, что у них есть три обратных вызова жизненного цикла, которые запускаются при определенных условиях:
-
initialize
: этот обратный вызов происходит только один раз, когда создается экземпляр контроллера. -
connect
: срабатывает всякий раз, когда мы подключаем контроллер к элементу DOM. Поскольку один контроллер может быть подключен к нескольким элементам на странице, этот обратный вызов может выполняться несколько раз. -
disconnect
: как вы уже, наверное, догадались, этот обратный вызов выполняется всякий раз, когда контроллер отключается от элемента DOM.
Ничего сложного, правда? Давайте используем обратные вызовы initialize()
и connect()
чтобы убедиться, что наш контроллер действительно работает:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
// src/controllers/employees_controller.js
export default class extends Controller {
initialize() {
console.log(‘Initialized’)
console.log(this)
}
connect() {
console.log(‘Connected’)
console.log(this)
}
}
|
Затем запустите сервер, запустив:
1
|
yarn start
|
Перейдите по http://localhost:9000
. Откройте консоль вашего браузера и убедитесь, что оба сообщения отображаются. Это означает, что все работает как положено!
Добавление событий
Следующая основная концепция Стимул — это события . События используются для реагирования на различные действия пользователя на странице: щелчок, зависание, фокусировка и т. Д. Stimulus не пытается заново изобретать велосипед, а его система событий основана на общих событиях JS.
Например, давайте свяжем событие click с нашими сотрудниками. Всякий раз, когда происходит это событие, я хотел бы вызвать еще не существующий метод choose()
для employees_controller
:
1
2
3
4
5
6
|
<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
является наиболее распространенным событием, его можно смело пропустить:
1
|
<li><a href=»#» data-action=»employees#choose»>John Doe</a></li>
|
В этом случае click
будет использоваться неявно.
Далее, давайте закодируем метод choose()
. Я не хочу, чтобы выполнялось действие по умолчанию (которое, очевидно, открывает новую страницу, указанную в атрибуте href
), поэтому давайте предотвратим это:
1
2
3
4
5
6
7
8
9
|
// src/controllers/employees_controller.js
// callbacks here…
choose(e) {
e.preventDefault()
console.log(this)
console.log(e)
}
|
Это специальный объект события, который содержит полную информацию о сработавшем событии. Заметьте, кстати, что this
возвращает сам контроллер, а не отдельную ссылку! Чтобы получить доступ к элементу, который действует как цель события, используйте e.target
.
Перезагрузите страницу, нажмите на элемент списка и наблюдайте за результатом!
Работа с государством
Теперь, когда мы связали обработчик событий клика с сотрудниками, я хотел бы сохранить выбранного в данный момент человека. Почему? Сохраняя эту информацию, мы можем предотвратить повторный выбор одного и того же сотрудника. Это позже позволит нам избежать загрузки одной и той же информации несколько раз.
Стимул поручает нам сохранить состояние в API данных , что кажется вполне разумным. Прежде всего, давайте предоставим несколько произвольных идентификаторов для каждого сотрудника, используя атрибут data-id
:
1
2
3
4
5
6
|
<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>
|
Далее нам нужно получить идентификатор и сохранить его. Использование API данных очень распространено в Stimulus, поэтому для каждого контроллера предоставляется специальный объект this.data
. С его помощью мы можем запустить следующие методы:
-
this.data.get('name')
: получить значение по его атрибуту. -
this.data.set('name', value)
: установить значение для некоторого атрибута. -
this.data.has('name')
: проверить, существует ли атрибут (возвращает логическое значение).
К сожалению, эти ярлыки недоступны для целей событий click, поэтому мы должны придерживаться getAttribute()
в их случае:
1
2
3
4
5
|
// src/controllers/employees_controller.js
choose(e) {
e.preventDefault()
this.data.set(«current-employee», e.target.getAttribute(‘data-id’))
}
|
Но мы можем сделать еще лучше, создав геттер и сеттер для currentEmployee
:
01
02
03
04
05
06
07
08
09
10
|
// 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
и this.currentEmployee
тем, чтобы предоставленный идентификатор не this.currentEmployee
с уже сохраненным.
Теперь вы можете переписать метод choose()
следующим образом:
1
2
3
4
5
6
|
// 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}
(который называется дескриптором цели ):
01
02
03
04
05
06
07
08
09
10
11
12
13
|
<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>
|
Теперь сообщите Стимулу об этих новых целях, определив новое статическое значение:
1
2
3
4
5
6
7
|
// src/controllers/employees_controller.js
export default class extends Controller {
static targets = [ «employee» ]
// …
}
|
Как нам получить доступ к целям сейчас? Это так же просто, как сказать this.employeeTarget
(чтобы получить первый элемент) или this.employeeTargets
(чтобы получить все элементы):
1
2
3
4
5
6
7
|
// 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 с легкостью, основываясь на некоторых критериях:
01
02
03
04
05
06
07
08
09
10
|
// 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
. Если он соответствует, элементу назначается класс .chosen
. В противном случае этот класс удаляется. Вы также можете извлечь if (this.currentEmployee !== id) {
из установщика и использовать его вместо метода if (this.currentEmployee !== id) {
chosen()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// 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
:
1
2
3
4
5
|
.chosen {
font-weight: bold;
text-decoration: none;
cursor: default;
}
|
Перезагрузите страницу еще раз, нажмите на человека и убедитесь, что он выделен правильно.
Загрузка данных асинхронно
Наша следующая задача — загрузить информацию о выбранном сотруднике. В реальном приложении вам нужно будет настроить хостинг-провайдера , серверную часть, работающую на чем-то вроде Django или Rails , и конечную точку API, которая отвечает JSON, содержащим все необходимые данные. Но мы собираемся сделать вещи немного проще и сосредоточиться только на стороне клиента. Создайте каталог employees
в public
папке. Затем добавьте четыре файла, содержащие данные для отдельных сотрудников:
1.json
1
2
3
4
5
6
7
8
|
{
«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
1
2
3
4
5
6
7
8
|
{
«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
1
2
3
4
5
6
7
8
|
{
«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
1
2
3
4
5
6
7
8
|
{
«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()
отдельный loadInfoFor()
:
1
2
3
4
5
6
7
|
// src/controllers/employees_controller.js
loadInfoFor(employee_id) {
fetch(`employees/${employee_id}.json`)
.then(response => response.text())
.then(json => { this.displayInfo(json) })
}
|
Этот метод принимает идентификатор сотрудника и отправляет запрос асинхронной выборки на указанный URI. Есть также два обещания: одно для извлечения тела и другое для отображения загруженной информации (мы добавим соответствующий метод через мгновение).
Используйте этот новый метод внутри choose()
:
01
02
03
04
05
06
07
08
09
10
11
|
// src/controllers/employees_controller.js
choose(e) {
e.preventDefault()
const id = e.target.getAttribute(‘data-id’)
if (this.currentEmployee !== id) {
this.loadInfoFor(id)
// …
}
}
|
Прежде чем кодировать метод displayInfo()
, нам нужен элемент для фактической визуализации данных. Почему бы нам не воспользоваться целями еще раз?
1
2
3
4
5
6
7
8
|
<!— public/index.html —>
<div data-controller=»employees»>
<div data-target=»employees.info»></div>
<ul>
<!— … —>
</ul>
</div>
|
Определите цель:
1
2
3
4
5
6
|
// src/controllers/employees_controller.js
export default class extends Controller {
static targets = [ «employee», «info» ]
// …
}
|
А теперь используйте его для отображения всей информации:
1
2
3
4
5
6
7
|
// 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
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
[
{
«id»: «1»,
«name»: «John Doe»
},
{
«id»: «2»,
«name»: «Alice Smith»
},
{
«id»: «3»,
«name»: «Will Brown»
},
{
«id»: «4»,
«name»: «Ann Grey»
}
]
|
Теперь public/index.html
файл public/index.html
, удалив жестко запрограммированный список и добавив атрибут data-employees-url
(обратите внимание, что мы должны предоставить имя контроллера, иначе API данных не будет работать):
1
2
3
|
<div data-controller=»employees» data-employees-url=»/employees.json»>
<div data-target=»employees.info»></div>
</div>
|
Как только контроллер подключен к DOM, он должен отправить запрос на выборку для составления списка сотрудников. Это означает, что обратный вызов connect()
является идеальным местом для этого:
1
2
3
4
5
|
// src/controllers/employees_controller.js
connect() {
this.loadFrom(this.data.get(‘url’), this.displayEmployees)
}
|
Я предлагаю создать более общий метод loadFrom()
который принимает URL-адрес для загрузки данных и обратный вызов для фактического отображения этих данных:
1
2
3
4
5
6
7
|
// src/controllers/employees_controller.js
loadFrom(url, callback) {
fetch(url)
.then(response => response.text())
.then(json => { callback.call( this, JSON.parse(json) ) })
}
|
Настройте метод choose()
чтобы воспользоваться loadFrom()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// 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()
:
1
2
3
4
5
6
|
// 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()
метод displayEmployees()
:
01
02
03
04
05
06
07
08
09
10
|
// 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
}
|
Это оно! Теперь мы динамически отображаем наш список сотрудников на основе данных, возвращаемых сервером.
Вывод
В этой статье мы рассмотрели скромный JavaScript-фреймворк под названием Stimulus. Мы увидели, как создать новое приложение, добавить контроллер с кучей обратных вызовов и действий, а также представить события и действия. Также мы выполнили некоторую асинхронную загрузку данных с помощью запросов на выборку.
В общем-то, это и есть основы Stimulus — он действительно не ожидает, что у вас будут какие-то тайные знания для создания веб-приложений. Конечно, фреймворк, вероятно, будет иметь некоторые новые функции в будущем, но разработчики не планируют превращать его в огромного монстра с сотнями инструментов.
Если вы хотите найти больше примеров использования Стимул, вы также можете проверить этот крошечный справочник . А если вы ищете дополнительные ресурсы JavaScript для изучения или использования в своей работе, посмотрите, что у нас есть на Envato Market .
Вам понравился Стимул? Заинтересованы ли вы в попытках создать реальное приложение на основе этого фреймворка? Поделитесь своими мыслями в комментариях!
Как всегда, я благодарю вас за то, что вы остались со мной и до следующего раза.