Статьи

Создайте список дел с помощью Hyperapp, 1KB JS Micro-framework

В этом руководстве мы будем использовать Hyperapp для создания приложения со списком дел. Если вы хотите изучить принципы функционального программирования, но не увязнуть в деталях, читайте дальше.

Гиперапп сейчас горячий. Недавно он превысил 11 000 звезд на GitHub и занял 5-е место в разделе «Фреймворк фреймворка» JavaScript Rising Stars 2017 года . Это было также показано на SitePoint недавно, когда он достиг версии 1.0.

Причину популярности Hyperapp можно объяснить его прагматизмом и сверхлегким размером (1,4 кБ), в то же время достигая результатов, аналогичных React и Redux, из коробки.

Итак, что такое HyperApp?

Hyperapp позволяет создавать динамические одностраничные веб-приложения, используя преимущества виртуального DOM для быстрого и эффективного обновления элементов на веб-странице аналогично React. Он также использует один объект, который отвечает за отслеживание состояния приложения, как Redux. Это облегчает управление состоянием приложения и гарантирует, что различные элементы не синхронизируются друг с другом. Основным влиянием Hyperapp была архитектура Elm .

По своей сути Hyperapp состоит из трех основных частей:

  • Гос . Это единое дерево объектов, в котором хранится вся информация о приложении.
  • Действия Это методы, которые используются для изменения и обновления значений в объекте состояния.
  • Посмотреть Это функция, которая возвращает объекты виртуальных узлов, которые компилируются в код HTML. Он может использовать JSX или аналогичный язык шаблонов и имеет доступ к объектам state и actions .

Эти три части взаимодействуют друг с другом, создавая динамическое приложение. Действия инициируются событиями на странице. Затем действие обновляет состояние, которое затем запускает обновление представления. Эти изменения вносятся в Virtual DOM, который Hyperapp использует для обновления фактического DOM на веб-странице.

Начиная

Чтобы начать как можно быстрее, мы будем использовать CodePen для разработки нашего приложения. Вам необходимо убедиться, что препроцессор JavaScript установлен в Babel, а пакет Hyperapp загружен как внешний ресурс по следующей ссылке:

 https://unpkg.com/hyperapp 

Чтобы использовать Hyperapp, нам нужно импортировать функцию app а также метод h , который Hyperapp использует для создания узлов VDOM. Добавьте следующий код на панель JavaScript в CodePen:

 const { h, app } = hyperapp; 

Мы будем использовать JSX для кода представления. Чтобы Hyperapp знал это, нам нужно добавить следующий код в код:

 /** @jsx h */ 

Метод app() используется для инициализации приложения:

 const main = app(state, actions, view, document.body); 

Он принимает объекты state и actions качестве первых двух параметров, функцию view() качестве третьего параметра, а последний параметр — это элемент HTML, в который приложение должно быть вставлено в разметку. По соглашению обычно это <body> , представленный document.body .

Чтобы упростить начало работы, я создал шаблонный шаблон кода Hyperapp на CodePen, который содержит все элементы, упомянутые выше. Это может быть разветвлен, нажав на эту ссылку .

Привет Гиперапп!

Давайте поиграем с Hyperapp и посмотрим, как все это работает. Функция view() принимает объекты state и actions качестве аргументов и возвращает объект Virtual DOM. Мы собираемся использовать JSX, а это значит, что мы можем написать код, который будет больше похож на HTML. Вот пример, который вернет заголовок:

 const view = (state, actions) => ( <h1>Hello Hyperapp!</h1> ); 

Это фактически вернет следующий объект VDOM:

 { name: "h1", props: {}, children: "Hello Hyperapp!" } 

Функция view() вызывается каждый раз, когда изменяется объект state . Затем Hyperapp создаст новое дерево Virtual DOM на основе любых произошедших изменений. Затем Hyperapp позаботится об обновлении актуальной веб-страницы наиболее эффективным способом, сравнивая различия в новом Virtual DOM со старым, хранящимся в памяти.

Компоненты

Компоненты — это чистые функции, которые возвращают виртуальные узлы. Их можно использовать для создания многократно используемых блоков кода, которые затем можно вставить в представление. Они могут принимать параметры обычным способом, как любая функция, но они не имеют доступа к объектам state и actions же, как это делает представление.

В приведенном ниже примере мы создаем компонент с именем Hello() который принимает объект в качестве параметра. Мы извлекаем значение name из этого объекта, используя деструктурирование, прежде чем возвращать заголовок, содержащий это значение:

 const Hello = ({name}) => <h1>Hello {name}</h1>; 

Теперь мы можем ссылаться на этот компонент в представлении, как если бы это был элемент HTML с названием <Hello /> . Мы можем передавать данные этому элементу так же, как мы можем передавать реквизиты компоненту React:

 const view = (state, actions) => ( <Hello name="Hyperapp" /> ); 

Обратите внимание, что, поскольку мы используем JSX, имена компонентов должны начинаться с заглавных букв или содержать точку.

состояние

Состояние представляет собой обычный старый объект JavaScript, который содержит информацию о приложении. Это «единственный источник правды» для приложения, и его можно изменить только с помощью действий.

Давайте создадим объект состояния для нашего приложения и установим свойство с именем name :

 const state = { name: "Hyperapp" }; 

Функция просмотра теперь имеет доступ к этому свойству. Обновите код до следующего:

 const view = (state, actions) => ( <Hello name={state.name} /> ); 

Поскольку представление может обращаться к объекту state , мы можем использовать его свойство name как атрибут компонента <Hello /> .

действия

Действия — это функции, используемые для обновления объекта состояния. Они написаны в определенной форме, которая возвращает другую функцию карри, которая принимает текущее состояние и возвращает обновленный объект частичного состояния. Это частично стилистически, но также гарантирует, что объект state остается неизменным. Абсолютно новый объект state создается путем объединения результатов действия с предыдущим состоянием. Это приведет к view функции представления и обновлению HTML.

В приведенном ниже примере показано, как создать действие с именем changeName() . Эта функция принимает аргумент с именем name и возвращает карри-функцию, которая используется для обновления свойства name в объекте state с этим новым именем.

 const actions = { changeName: name => state => ({name: name}) }; 

Чтобы увидеть это действие, мы можем создать кнопку в представлении и использовать обработчик события onclick для вызова действия с аргументом «Batman». Для этого обновите функцию view следующим образом:

 const view = (state, actions) => ( <div> <Hello name={state.name} /> <button onclick={() => actions.changeName('Batman')}>I'm Batman</button> </div> ); 

Теперь попробуйте нажать на кнопку и посмотрите, как меняется имя!

Вы можете увидеть живой пример здесь .

Hyperlist

Теперь пришло время построить что-то более существенное. Мы собираемся создать простое приложение со списком дел, которое позволит вам создавать список, добавлять новые элементы, помечать их как завершенные и удалять элементы.

Прежде всего, нам нужно начать новую ручку на CodePen. Добавьте следующий код или просто разветвите мою ручку HyperBoiler:

 const { h, app } = hyperapp; /** @jsx h */ const state = { }; const actions = { }; const view = (state, actions) => ( ); const main = app(state, actions, view, document.body); 

Вы также должны добавить следующее в раздел CSS и установить его в SCSS:

 // fonts @import url("https://fonts.googleapis.com/css?family=Racing+Sans+One"); $base-fonts: Helvetica Neue, sans-serif; $heading-font: Racing Sans One, sans-serif; // colors $primary-color: #00caff; $secondary-color: hotpink; $bg-color: #222; * { margin: 0; padding: 0; box-sizing: border-box; } body { padding-top: 50px; background: $bg-color; color: $primary-color; display: flex; height: 100vh; justify-content: center; font-family: $base-fonts; } h1 { color: $secondary-color; & strong{ color: $primary-color; } font-family: $heading-font; font-weight: 100; font-size: 4.2em; text-align: center; } a{ color: $primary-color; } .flex{ display: flex; align-items: top; margin: 20px 0; input { border: 1px solid $primary-color; background-color: $primary-color; font-size: 1.5em; font-weight: 200; width: 50vw; height: 62px; padding: 15px 20px; margin: 0; outline: 0; &::-webkit-input-placeholder { color: $bg-color; } &::-moz-placeholder { color: $bg-color; } &::-ms-input-placeholder { color: $bg-color; } &:hover, &:focus, &:active { background: $primary-color; } } button { height: 62px; font-size: 1.8em; padding: 5px 15px; margin: 0 3px; } } ul#list { display: flex; flex-direction: column; padding: 0; margin: 1.2em; width: 50vw; li { font-size: 1.8em; vertical-align: bottom; &.completed{ color: $secondary-color; text-decoration: line-through; button{ color: $primary-color; } } button { visibility: hidden; background: none; border: none; color: $secondary-color; outline: none; font-size: 0.8em; font-weight: 50; padding-top: 0.3em; margin-left: 5px; } &:hover{ button{ visibility: visible; } } } } button { background: $bg-color; border-radius: 0px; border: 1px solid $primary-color; color: $primary-color; font-weight: 100; outline: none; padding: 5px; margin: 0; &:hover, &:disabled { background: $primary-color; color: #111; } &:active { outline: 2px solid $primary-color; } &:focus { border: 1px solid $primary-color; } } 

Это просто добавляет немного стиля и фирменного стиля Hyperapp в приложение.

Теперь давайте начнем создавать собственное приложение!

Начальное состояние и вид

Для начала, мы собираемся настроить объект начального state и простой вид.

При создании объекта начального state полезно подумать о том, какие данные и информацию ваше приложение будет хотеть отслеживать на протяжении своего жизненного цикла. В случае нашего списка нам понадобится массив для хранения задач, а также строка, представляющая все, что записано в поле ввода, куда вводятся фактические задачи. Это будет выглядеть следующим образом:

 const state = { items: [], input: '', }; 

Далее мы создадим функцию view() . Для начала сосредоточимся на коде, необходимом для добавления элемента. Добавьте следующий код:

 const view = (state, actions) => ( <div> <h1><strong>Hyper</strong>List</h1> <AddItem add={actions.add} input={actions.input} value={state.input} /> </div> ); 

Это отобразит заголовок, а также элемент с именем <AddItem /> . Это не новый элемент HTML, а компонент, который нам нужно создать. Давайте сделаем это сейчас:

 const AddItem = ({ add, input, value }) => ( <div class='flex'> <input type="text" value={value} onkeyup={e => (e.keyCode === 13 ? add() : null)} oninput={e => input({ value: e.target.value })} /> <button onclick={add}></button> </div> ); 

Это возвращает элемент <input> который будет использоваться для ввода наших задач, а также элемент <button> который будет использоваться для добавления их в список. Компонент принимает объект в качестве аргумента, из которого мы извлекаем три свойства: add , input и value .

Как и следовало ожидать, функция add() будет использоваться для добавления элемента в наш список задач. Эта функция вызывается, если нажата клавиша Enter (она имеет KeyCode 13) или если нажата кнопка. Функция input() используется для обновления значения текущего элемента в состоянии и вызывается всякий раз, когда текстовое поле получает пользовательский ввод. Наконец, свойство value — это то, что пользователь ввел в поле ввода.

Обратите внимание, что функции input() и add() являются действиями, передаваемыми как реквизиты компоненту <AddItem /> :

 <AddItem add={actions.add} input={actions.input} value={state.input} /> 

Вы также можете видеть, что value prop берется из свойства input . Таким образом, текст, отображаемый в поле ввода, фактически сохраняется в состоянии и обновляется при каждом нажатии клавиши.

Чтобы склеить все вместе, нам нужно добавить действие input :

 const actions = { input: ({ value }) => ({ input: value }) } 

Теперь, если вы начнете печатать внутри поля ввода, вы должны увидеть, что оно отображает то, что вы печатаете. Это демонстрирует цикл Hyperapp:

  1. событие oninput вызывается, когда пользователь вводит текст в поле ввода
  2. вызывается действие input()
  3. действие обновляет input свойство в состоянии
  4. изменение состояния приводит к вызову функции view() и обновлению VDOM
  5. Затем изменения в VDOM вносятся в фактический DOM, и страница повторно отображается для отображения нажатой клавиши.

Попробуйте, и вы должны увидеть, что набрано появиться в поле ввода. К сожалению, нажатие Enter или нажатие на кнопку «+» в настоящий момент ничего не делает. Это потому, что нам нужно создать действие, которое добавляет элементы в наш список.

Добавление задачи

Прежде чем мы рассмотрим создание элемента списка, нам нужно подумать, как они будут представлены. Нотация объектов JavaScript идеальна, поскольку позволяет хранить информацию в виде пар ключ-значение. Нам нужно подумать о том, какими свойствами может обладать элемент списка. Например, ему нужно значение, которое описывает, что нужно сделать. Также необходимо свойство, которое указывает, был ли элемент завершен или нет. Примером может быть:

 { value: 'Buy milk', completed: false, id: 123456 } 

Обратите внимание, что объект также содержит свойство с именем id . Это связано с тем, что узлам VDOM в Hyperapp требуется уникальный ключ для их идентификации. Мы будем использовать временную метку для этого .

Теперь мы можем приступить к созданию действия для добавления элементов. Наша первая задача — сбросить поле ввода на пустое. Это делается путем сброса input свойства в пустую строку. Затем нам нужно добавить новый объект в массив items . Это делается с Array.concat метода Array.concat . Это действует аналогично Array.push() , но возвращает новый массив, а не изменяет массив, на который он действует. Помните, мы хотим создать новый объект состояния и затем объединить его с текущим состоянием, а не просто изменять текущее состояние напрямую. Свойство value устанавливается равным значению, содержащемуся в state.input , который представляет то, что было введено в поле ввода:

 add: () => state => ({ input: '', items: state.items.concat({ value: state.input, completed: false, id: Date.now() }) }) 

Обратите внимание, что это действие содержит два разных состояния. Текущее состояние, которое представлено аргументом, переданным второй функции. Также есть новое состояние, это возвращаемое значение второй функции.

Чтобы продемонстрировать это в действии, давайте представим, что приложение только что запустило пустой список элементов, и пользователь ввел текст «Купить молоко» в поле ввода и нажал Enter , запустив действие add() .

Перед действием состояние выглядит так:

 state = { input: 'Buy milk', items: [] } 

Этот объект передается в качестве аргумента в действие add() , которое будет возвращать следующий объект состояния:

 state = { input: '', items: [{ value: 'Buy milk', completed: false, id: 1521630421067 }] } 

Теперь мы можем добавлять элементы в массив items в состоянии, но не видим их! Чтобы это исправить, нам нужно обновить наш взгляд. Прежде всего нам нужно создать компонент для отображения элементов в списке:

 const ListItem = ({ value, id }) => <li id={id} key={id}>{value}</li>; 

При этом используется элемент <li> для отображения значения, которое предоставляется в качестве аргумента. Также обратите внимание, что атрибуты id и key имеют одинаковое значение, которое является уникальным идентификатором элемента. Атрибут key используется внутри Hyperapp, поэтому не отображается в отображаемом HTML, поэтому полезно также отображать ту же информацию, используя атрибут id , тем более что этот атрибут имеет то же условие уникальности.

Теперь, когда у нас есть компонент для наших элементов списка, нам нужно фактически отобразить их. JSX делает это довольно просто, так как он будет циклически перебирать массив значений и отображать каждое из них по очереди. Проблема состоит в том, что state.items не включает код JSX, поэтому нам нужно использовать Array.map для изменения каждого объекта элемента в массиве в код JSX, например, так:

 state.items.map(item => ( <ListItem id={item.id} value={item.value} /> )); 

Это будет перебирать каждый объект в массиве state.items и создавать новый массив, содержащий ListItem компоненты ListItem . Теперь нам просто нужно добавить это к представлению. Обновите функцию view() для кода ниже:

 const view = (state, actions) => ( <div> <h1><strong>Hyper</strong>List</h1> <AddItem add={actions.add} input={actions.input} value={state.input} /> <ul id='list'> { state.items.map(item => ( <ListItem id={item.id} value={item.value} /> )) } </ul> </div> ); 

Это просто помещает новый массив компонентов ListItem в пару тегов <ul> чтобы они отображались как неупорядоченный список.

Теперь, если вы попытаетесь добавить элементы, вы должны увидеть их в списке под полем ввода!

Пометить задачу как выполненную

Наша следующая задача — иметь возможность переключать completed свойство задачи. Нажатие на незавершенную задачу должно обновить ее completed свойство до true , а нажатие на завершенную задачу должно переключить ее completed свойство обратно в false .

Это можно сделать с помощью следующего действия:

 toggle: id => state => ({ items: state.items.map(item => ( id === item.id ? Object.assign({}, item, { completed: !item.completed }) : item )) }) 

В этом действии много чего происходит. Прежде всего, он принимает параметр с именем id , который ссылается на уникальный идентификатор каждой задачи. Затем действие перебирает все элементы в массиве, проверяя, соответствует ли предоставленное значение id свойству id каждого объекта элемента списка. Если это так, он изменяет completed свойство на противоположное тому, что в настоящее время использует оператор отрицания ! , Это изменение выполняется с помощью Object.assign() , который создает новый объект и выполняет поверхностное объединение со старым объектом и обновленными свойствами. Помните, мы никогда не обновляем объекты в состоянии напрямую. Вместо этого мы создаем новую версию состояния, которая перезаписывает текущее состояние.

Теперь нам нужно связать это действие с представлением. Мы делаем это, обновляя компонент ListItem чтобы он имел обработчик события onclick который будет вызывать только что созданное toggle действие. Обновите ListItem компонента ListItem чтобы он выглядел следующим образом:

 const ListItem = ({ value, id, completed, toggle, destroy }) => ( <li class={completed && "completed"} id={id} key={id} onclick={e => toggle(id)}>{value}</li> ); 

Острые глаза среди вас заметят, что компонент получил некоторые дополнительные параметры, а также есть дополнительный код в списке атрибутов <li> :

 class={completed && "completed"} 

Это общий шаблон в Hyperapp, который используется для вставки дополнительных фрагментов кода, когда выполняются определенные условия. Он использует короткое замыкание или ленивую оценку, чтобы установить класс как завершенный, если completed аргумент равен true. Это связано с тем, что при использовании && возвращаемое значение операции будет вторым операндом, если оба операнда имеют значение true. Поскольку строка "completed" всегда истинно, она будет возвращена, если первый операнд — completed аргумент — истинно. Это означает, что, если задача была выполнена, она будет иметь класс «выполнено» и может быть соответственно стилизована.

Наша последняя работа — обновить код в функции view() чтобы добавить дополнительный аргумент в компонент <ListItem /> :

 const view = (state, actions) => ( <div> <h1><strong>Hyper</strong>List</h1> <AddItem add={actions.add} input={actions.input} value={state.input} /> <ul id='list'> { state.items.map(item => ( <ListItem id={item.id} value={item.value} completed={item.completed} toggle={actions.toggle} /> )) } </ul> </div> ); 

Теперь, если вы добавите некоторые элементы и попробуете нажать на них, вы увидите, что они помечены как завершенные, и через них появляется строка. Нажмите еще раз, и они вернутся к завершению.

Удалить задачу

Наше приложение списка работает в настоящее время довольно хорошо, но было бы хорошо, если бы мы могли удалить любые элементы, которые нам больше не нужны в списке.

Наша первая задача — добавить действие destroy() , которое удалит элемент из массива items в состоянии. Мы не можем сделать это с помощью Array.slice() , так как это деструктивный метод, который действует на исходный массив. Вместо этого мы используем метод filter() , который возвращает новый массив, который содержит все объекты item, которые проходят указанное условие. Это условие заключается в том, что свойство id не равно идентификатору, переданному в качестве аргумента действия destroy() . Другими словами, он возвращает новый массив, в котором нет элемента, от которого мы хотим избавиться. Этот новый список заменит старый при обновлении состояния.

Добавьте следующий код к объекту actions :

 destroy: id => state => ({ items: state.items.filter(item => item.id !== id) }) 

Теперь нам снова нужно обновить компонент ListItem чтобы добавить механизм для запуска этого действия. Мы сделаем это, добавив кнопку с обработчиком события onclick :

 const ListItem = ({ value, id, completed, toggle, destroy }) => ( <li class={completed && "completed"} id={id} key={id} onclick={e => toggle(id)}> {value} <button onclick={ () => destroy(id) }>x</button> </li> ); 

Обратите внимание, что нам также нужно добавить еще один параметр с именем destroy который представляет действие, которое мы хотим использовать при нажатии кнопки. Это связано с тем, что компоненты не имеют прямого доступа к объекту actions в том же виде, что и представление, поэтому представлению необходимо явно передавать любые действия.

И наконец, нам нужно обновить представление, чтобы передать actions.destroy в качестве аргумента компоненту <ListItem /> :

 const view = (state, actions) => ( <div> <h1><strong>Hyper</strong>List</h1> <AddItem add={actions.add} input={actions.input} value={state.input} /> <ul id='list'> {state.items.map(item => ( <ListItem id={item.id} value={item.value} completed={item.completed} toggle={actions.toggle} destroy={actions.destroy} /> ))} </ul> </div> ); 

Теперь, если вы добавляете некоторые элементы в свой список, вы должны заметить кнопку «х», когда вы наводите на них курсор мыши. Нажмите на это, и они должны исчезнуть в эфир!

Удалить все выполненные задачи

Последняя функция, которую мы добавим к нашему списку приложений, — это возможность сразу удалить все выполненные задачи. При этом используется тот же метод filter() который мы использовали ранее — возвращает массив, который содержит только объекты item с completed значением свойства false . Добавьте следующий код к объекту actions :

 clearAllCompleted: ({items}) => ({ items: items.filter(item => !item.completed) }) 

Чтобы реализовать это, нам просто нужно добавить кнопку с обработчиком события onclick чтобы вызвать это действие в нижней части представления:

 const view = (state, actions) => ( <div> <h1><strong>Hyper</strong>List</h1> <AddItem add={actions.add} input={actions.input} value={state.input} /> <ul id='list'> {state.items.map(item => ( <ListItem id={item.id} value={item.value} completed={item.completed} toggle={actions.toggle} destroy={actions.destroy} /> ))} </ul> <button onclick={() => actions.clearAllCompleted({ items: state.items })}> Clear completed items </button> </div> ); 

Теперь добавьте некоторые элементы, отметьте некоторые из них как завершенные, затем нажмите кнопку, чтобы убрать их все. Потрясающие!

Вот и все, ребята

Это подводит нас к концу этого урока. Мы собрали простое приложение со списком дел, которое выполняет большинство задач, которые вы ожидаете от такого приложения. Если вы ищете вдохновение и хотите добавить функциональность, вы можете посмотреть на добавление приоритетов, изменение порядка элементов с помощью перетаскивания или добавление возможности иметь более одного списка.

Я надеюсь, что это руководство помогло вам понять, как работает Hyperapp. Если вы хотите углубиться в Hyperapp, я бы порекомендовал прочитать документы, а также взглянуть на исходный код . Это не очень долго, и даст вам полезную информацию о том, как все работает в фоновом режиме. Вы также можете задать дополнительные вопросы о группе Hyperapp Slack . Это одна из самых дружелюбных групп, которые я использовал, и мне помогли знающие участники. Вы также обнаружите, что Хорхе Букаран , создатель Hyperapp, часто зависает там и предлагает помощь и совет.

Использование CodePen делает разработку приложений Hyperapp действительно быстрой и простой, но в конечном итоге вы захотите создавать свои собственные приложения локально, а также развертывать их в Интернете. Для получения советов о том, как это сделать, ознакомьтесь с моей следующей статьей о комплектации приложения Hyperapp и его развертывании на страницах GitHub !

Я надеюсь, вам понравится играть с кодом из этой статьи, и, пожалуйста, поделитесь своими мыслями или вопросами в комментариях ниже.