В этом руководстве мы будем использовать 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:
- событие
oninput
вызывается, когда пользователь вводит текст в поле ввода - вызывается действие
input()
- действие обновляет
input
свойство в состоянии - изменение состояния приводит к вызову функции
view()
и обновлению VDOM - Затем изменения в 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 !
Я надеюсь, вам понравится играть с кодом из этой статьи, и, пожалуйста, поделитесь своими мыслями или вопросами в комментариях ниже.