Hyperapp — это библиотека JavaScript для создания многофункциональных веб-приложений. Он сочетает в себе прагматичный подход Elm к управлению состоянием с механизмом VDOM, который поддерживает ключевые обновления и события жизненного цикла — и все это без зависимостей. Дайте или возьмите несколько байтов, весь исходный код, уменьшенный и сжатый, занимает около 1 КБ.
В этом руководстве я познакомлю вас с Hyperapp и расскажу о нескольких примерах кода, которые помогут сразу приступить к работе. Я предполагаю некоторое знакомство с HTML и JavaScript, но предыдущий опыт работы с другими платформами не требуется.
Привет, мир
Мы начнем с простой демонстрации, которая показывает, как все движущиеся части работают вместе.
Вы можете попробовать код онлайн тоже.
import { h, app } from "hyperapp" // @jsx h const state = { count: 0 } const actions = { down: () => state => ({ count: state.count - 1 }), up: () => state => ({ count: state.count + 1 }) } const view = (state, actions) => ( <div> <h1>{state.count}</h1> <button onclick={actions.down}>-</button> <button onclick={actions.up}>+</button> </div> ) app(state, actions, view, document.body)
Примерно так выглядит каждое приложение Hyperapp. Единый объект состояния, действия, которые заполняют состояние, и представление, которое переводит состояние и действия в пользовательский интерфейс.
Внутри функции приложения мы создаем копию вашего состояния и действий (было бы невежливо изменять объекты, которые нам не принадлежат) и передавали их в представление. Мы также переносим ваши действия, чтобы они каждый раз перерисовывали приложение при каждом изменении состояния.
app(state, actions, view, document.body)
Состояние представляет собой простой объект JavaScript, который описывает модель данных вашего приложения. Это также неизменно. Чтобы изменить его, нужно определить действия и вызвать их.
const state = { count: 0 }
Внутри представления вы можете отобразить свойства состояния, использовать его, чтобы определить, какие части вашего интерфейса должны быть показаны или скрыты, и т. Д.
<h1>{state.count}</h1>
Вы также можете прикреплять действия к событиям DOM или вызывать действия в своих встроенных обработчиках событий.
<button onclick={actions.down}>-</button> <button onclick={actions.up}>+</button>
Действия не изменяют состояние напрямую, а возвращают новый фрагмент состояния. Если вы попытаетесь изменить состояние внутри действия, а затем вернуть его, представление не будет перерисовано, как вы могли ожидать.
const actions = { down: () => state => ({ count: state.count - 1 }), up: () => state => ({ count: state.count + 1 }) }
Вызов приложения возвращает объект действия, связанный с циклом обновления состояния просмотра состояния. Вы также получаете этот объект внутри функции просмотра и в действиях. Экспонировать этот объект для внешнего мира полезно, потому что он позволяет вам общаться с вашим приложением из другой программы, фреймворка или ванильного JavaScript.
const main = app(state, actions, view, document.body) setTimeout(main.up, 1000)
Примечание о JSX
Я буду использовать JSX в остальной части этого документа для ознакомления, но вы не обязаны использовать JSX с Hyperapp. Альтернативы включают встроенную функцию h
, @ hyperapp / html , hyperx и t7 .
Вот тот же пример сверху, используя @ hyperapp / html .
import { app } from "hyperapp" import { div, h1, button } from "@hyperapp/html" const state = { count: 0 } const actions = { down: () => state => ({ count: state.count - 1 }), up: () => state => ({ count: state.count + 1 }) } const view = (state, actions) => div([ h1(state.count), button({ onclick: actions.down }, "–"), button({ onclick: actions.up }, "+") ]) app(state, actions, view, document.body)
Виртуальный ДОМ
Виртуальный DOM — это описание того, как DOM должен выглядеть, используя дерево вложенных объектов JavaScript, известных как виртуальные узлы.
{ name: "div", props: { id: "app" }, children: [{ name: "h1", props: null, children: ["Hi."] }] }
Виртуальное дерево DOM вашего приложения создается с нуля на каждом цикле рендеринга. Это означает, что мы вызываем функцию представления каждый раз, когда изменяется состояние, и используем новое вычисленное дерево для обновления фактического DOM.
Мы пытаемся сделать это как можно меньше операций DOM, сравнивая новый виртуальный DOM с предыдущим. Это приводит к высокой эффективности, поскольку обычно требуется изменить лишь небольшой процент узлов, а изменение реальных узлов DOM обходится дороже по сравнению с пересчетом виртуального DOM.
Чтобы помочь вам создать виртуальные узлы более компактным способом, Hyperapp предоставляет функцию h
.
import { h } from "hyperapp" const node = h( "div", { id: "app" }, [h("h1", null, "Hi.")] )
Еще один способ создания виртуальных узлов — с помощью JSX . JSX — это расширение языка JavaScript, используемое для представления динамического HTML.
import { h } from "hyperapp" const node = ( <div id="app"> <h1>Hi.</h1> </div> )
Браузеры не понимают JSX, поэтому нам нужно скомпилировать его в вызовы функций h
, отсюда и оператор import h
. Давайте посмотрим, как этот процесс работает с помощью Babel .
Сначала установите зависимости:
npm i babel-cli babel-plugin-transform-react-jsx
Затем создайте файл .babelrc
:
{ "plugins": [ [ "transform-react-jsx", { "pragma": "h" } ] ] }
И скомпилируйте код из командной строки:
npm run babel src/index.js > index.js
Если вы предпочитаете не использовать систему сборки, вы также можете загрузить Hyperapp из CDN, например unpkg, и он будет доступен глобально через объект window.hyperapp
.
Примеры
Gif Search Box
В этом примере я покажу вам, как асинхронно обновлять состояние с помощью API Giphy для построения окна поиска Gif.
Чтобы вызвать побочные эффекты, мы вызываем действия внутри других действий, в рамках обратного вызова или когда обещание разрешено.
Действия, которые возвращают null
, undefined
или объект Promise
, не вызывают повторную визуализацию представления. Если действие возвращает обещание, мы передадим обещание вызывающей стороне, что позволит вам создать асинхронные действия, как в следующем примере.
import { h, app } from "hyperapp" // @jsx h const GIPHY_API_KEY = "dc6zaTOxFJmzC" const state = { url: "", query: "", isFetching: false } const actions = { downloadGif: query => async (state, actions) => { actions.toggleFetching(true) actions.setUrl( await fetch( `//api.giphy.com/v1/gifs/search?q=${query}&api_key=${GIPHY_API_KEY}` ) .then(data => data.json()) .then(({ data }) => (data[0] ? data[0].images.original.url : "")) ) actions.toggleFetching(false) }, setUrl: url => ({ url }), setQuery: query => ({ query }), toggleFetching: isFetching => ({ isFetching }) } const view = (state, actions) => ( <div> <input type="text" placeholder="Type here..." autofocus onkeyup={({ target: { value } }) =/> { if (value !== state.query) { actions.setQuery(value) if (!state.isFetching) { actions.downloadGif(value) } } }} /> <div class="container"> <img src={state.url} style={{ display: state.isFetching || state.url === "" ? "none" : "block" }} /> </div> </div> ) app(state, actions, view, document.body)
В этом состоянии хранится строка для URL-адреса Gif, поискового запроса и логического флага, чтобы узнать, когда браузер загружает новый Gif.
const state = { url: "", query: "", isFetching: false }
Флаг isFetching
используется, чтобы скрыть Gif, когда браузер занят. Без него последний загруженный Gif будет отображаться при запросе другого.
<img src={state.url} style={{ display: state.isFetching || state.url === "" ? "none" : "block" }} />
Представление состоит из ввода текста и элемента img
для отображения Gif.
Для обработки пользовательского ввода onkeyup
событие onkeyup
, но onkeydown
или oninput
будут работать.
При каждом нажатии клавиши actions.downloadGif
вызывается и запрашивается новый Gif, но только если выборка еще не ожидала и ввод текста не пуст.
if (value !== state.query) { actions.setQuery(value) if (!state.isFetching) { actions.downloadGif(value) } }
Внутри actions.downloadGif
мы используем API fetch, чтобы запросить Gif URL у Giphy.
Когда fetch
завершена, мы получаем полезную нагрузку с информацией Gif внутри обещания .
actions.toggleFetching(true) actions.setUrl( await fetch( `//api.giphy.com/v1/gifs/search?q=${query}&api_key=${GIPHY_API_KEY}` ) .then(data => data.json()) .then(({ data }) => (data[0] ? data[0].images.original.url : "")) ) actions.toggleFetching(false)
Как только данные получены, вызывается actions.toggleFetching
(что позволяет делать дальнейшие запросы на выборку), и состояние обновляется путем передачи извлеченного URL-адреса Gif в actions.setUrl
.
TweetBox Clone
В этом примере я покажу вам, как создавать пользовательские компоненты, чтобы организовать пользовательский интерфейс в многоразовую разметку и создать простой клон TweetBox.
import { h, app } from "hyperapp" // @jsx h const MAX_LENGTH = 140 const OFFSET = 10 const OverflowWidget = ({ text, offset, count }) => ( <div class="overflow"> <h1>Whoops! Too long.</h1> <p> ...{text.slice(0, offset)} <span class="overflow-text">{text.slice(count)}</span> </p> </div> ) const Tweetbox = ({ count, text, update }) => ( <div> <div class="container"> <ul class="flex-outer"> <li> <textarea placeholder="What's up?" value={text} oninput={update}></textarea> </li> <li class="flex-inner"> <span class={count > OFFSET ? "overflow-count" : "overflow-count-alert"} > {count} </span> <button onclick={() => alert(text)} disabled={count >= MAX_LENGTH || count < 0} > Tweet </button> </li> </ul> {count < 0 && ( <OverflowWidget text={text.slice(count - OFFSET)} offset={OFFSET} count={count} /> )} </div> </div> ) const state = { text: "", count: MAX_LENGTH } const view = (state, actions) => ( <tweetbox text={state.text} count={state.count} update={e => actions.update(e.target.value)} /> ) const actions = { update: text => state => ({ text, count: state.count + state.text.length - text.length }) } app(state, actions, view, document.body)
В этом состоянии хранится текст сообщения и количество оставшихся символов, инициализированное в MAX_LENGTH
.
const state = { text: "", count: MAX_LENGTH }
Представление состоит из нашего компонента TweetBox. Мы используем атрибуты / реквизиты, чтобы передать данные в виджет.
const view = (state, actions) => ( </tweetbox><tweetbox text={state.text} count={state.count} update={e => actions.update(e.target.value)} /> )
Когда пользователь вводит данные, мы вызываем actions.update()
чтобы обновить текущий текст и вычислить оставшиеся символы.
update: text => state => ({ text, count: state.count + state.text.length - text.length })
Вычитание длины текущего текста из длины предыдущего текста говорит нам о том, как изменилось количество оставшихся символов. Следовательно, новый счет оставшихся символов равен старому счету плюс вышеупомянутая разница.
Когда ввод пуст, эта операция равна (MAX_LENGTH - text.length)
.
Когда state.count
становится меньше 0, мы знаем, что state.text
должен быть длиннее MAX_LENGTH
, поэтому мы можем отключить кнопку твита и отобразить компонент OverflowWidget.
<button onclick={() => alert(text)} disabled={count >= MAX_LENGTH || count < 0}> Tweet </button>
Кнопка твита также отключена, когда state.count === MAX_LENGTH
, потому что это означает, что мы не state.count === MAX_LENGTH
никаких символов.
Тег OverflowWidget отображает недопустимую часть сообщения и несколько соседних символов для контекста. Константа OFFSET
сообщает нам, сколько дополнительных символов нужно state.text
из state.text
.
<overflowwidget text={text.slice(count - OFFSET)} offset={OFFSET} count={count}></overflowwidget>
OFFSET
в OverflowWidget, мы можем дополнительно разрезать text
и применять класс overflow-text
к определенной переполненной части.
<span class="overflow-text">{text.slice(count)}</span>
Сравнение с React
На концептуальном уровне у Hyperapp и React много общего. Обе библиотеки используют виртуальный DOM, события жизненного цикла и согласование на основе ключей. Hyperapp выглядит и чувствует себя во многом как React и Redux, но с меньшим количеством шаблонов.
Реакт популяризировал идею представления как функции государства. Hyperapp развивает эту идею на шаг вперед благодаря встроенному решению для управления состоянием, разработанным в духе Elm.
Hyperapp отвергает идею локального состояния компонентов, полагаясь только на чисто функциональные компоненты. Это означает высокую возможность повторного использования, дешевое запоминание и простое тестирование.
Последние мысли
Поскольку Hyperapp настолько мал, он быстрее передается по сети и быстрее разбирается, чем любая другая альтернатива. Это означает меньше понятий для изучения, меньше ошибок и большую стабильность структуры.
Я никогда не был фанатом больших фреймворков. Не потому что они не очень хороши, а потому что я хочу написать свой собственный JavaScript, а не тот JavaScript, который фреймворк хочет, чтобы я использовал. Мясо этого, я хочу передаваемые навыки. Я хочу развивать навыки в JavaScript, а не навыки в рамках.
Чтобы узнать больше о Hyperapp, ознакомьтесь с официальной документацией и следите за обновлениями и анонсами в Twitter .