Статьи

Введение в структуру стимула

Существует множество 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 .

Вам понравился Стимул? Заинтересованы ли вы в попытках создать реальное приложение на основе этого фреймворка? Поделитесь своими мыслями в комментариях!

Как всегда, я благодарю вас за то, что вы остались со мной и до следующего раза.