Я чувствую себя предателем, говоря это, учитывая мою довольно долгую историю с Vue и React, но я думаю, что у меня есть новый фреймворк для внешнего интерфейса. Hyperapp - это все, чем я хотел, чтобы Elm был - с ним легко создавать код, он хорошо организован, а состояние обрабатывается безупречно. При этом он не настолько готов к производству, как вышеупомянутые фреймворки, но когда он готов, я вижу, что он огромен.
Давайте начнем с нуля при создании приложения Hyperapp - с инструментами, которые все еще появляются, я обычно углубляюсь. Я сделаю то же самое для Hyperapp.
Начиная
Несколько недель назад я увидел несколько статей о Hyperapp, когда они выпустили версию 1.0 и набрали 10 000 звезд на GitHub. Я бегло посмотрел на счетчик «привет, мир» в их документации. Мне очень понравилось, насколько чисто и просто это выглядело, и я захотел попробовать!
const { h, app } = hyperapp const state = { count: 0 } const actions = { down: value => state => ({ count: state.count - value }), up: value => state => ({ count: state.count + value }) } const view = (state, actions) => h("div", {}, [ h("h1", {}, state.count), h("button", { onclick: () => actions.down(1) }, "–"), h("button", { onclick: () => actions.up(1) }, "+") ]) window.main = app(state, actions, view, document.body)
Вы также можете использовать JSX вместо вызова функции h
для создания элементов. Это то, что я сделал, так как привык к этому в React! Я просмотрел примеры на Hyperapp's Codepen. В итоге я использовал шаблон, так что мне не пришлось настраивать веб-пакет для переноса JSX или заниматься настройкой. Это было потрясающе, у меня не было никаких проблем с его использованием, и он шел с файловой структурой, которую мне нравилось использовать.
Hyperapp использует архитектуру, вдохновленную Elm’s - в нем есть представления, модели и обновления. Он также следует функциональной философии, подобной Elm. Это означает, что состояние неизменяемо и действия не имеют побочных эффектов. Управление состоянием было больше похоже на Redux, чем на стандартный React, поскольку состояние централизовано, а не зависит от компонента. Кроме того, вы должны использовать преобразователи для создания нечистых функций. Архитектура и настройка были удобными, и у меня не было с этим никаких проблем.
Поскольку в прошлом я работал с Elm, React, Redux, Vue и Vuex, я распознал закономерности и почувствовал себя прекрасно, когда перешел к финальному проекту после прочтения документации (которая является минимальной) и просмотра примеров кода.
Финальный проект
Я хотел создать что-то, что будет опираться на API - что может быть относительно беспорядочным процессом в Redux. У меня не было ни одного в голове, поэтому я просмотрел этот список, чтобы попытаться найти его. В итоге я использовал FavQs API - у меня возникла идея сделать вращающийся список цитат с возможностью поиска по тегам в разных кавычках. Это позволило бы мне немного взаимодействовать с государством.
Первый код, который я написал, был моделью для государства. Я установил начальные свойства для атрибутов, которые мне нужны в моем проекте:
export default { quotes: [], term: '', index: 0 }
Здесь было бы неплохо ввести что-то вроде TypeScript или Flow. Я уверен, что их можно достаточно легко интегрировать в проект гиперпространства.
Котировки были массивом котировок, возвращаемых из API, термин был термином поиска, если пользователь указал это, а затем индекс был текущим индексом цитаты, на которую смотрел пользователь.
У меня был файл конфигурации, в котором я определил некоторые константы для использования повсюду:
export const API_URL = 'https://favqs.com/api/quotes/' export const COLORS = ['#DBEBFF', '#FFBBDD', '#e6f9ff', '#BBBBFF', '#F7FFFD', '#fff8e1'] export const FONT_COLORS = ['#62D0FF', '#FF62B0', '#33ccff', '#5757FF', '#03EBA6', '#ffb300']
Я также сделал служебный файл, содержащий запросы Axios (минималистской библиотеки AJAX) для моих поисков:
import axios from 'axios' import { API_URL } from './constants' const getRequest = url => { return axios.get(url, { headers: {'Authorization': `Token token="XXXXXXXX"`} }).catch( err => console.log(err) ) } export default { getAll: _ => getRequest(API_URL), getQuery: query => getRequest(API_URL + `?filter=${query}&type=tag`) }
Вышеупомянутые файлы не зависят от фреймворка, но я хотел включить их для контекста.
Потенциально самый важный файл содержал действия:
import request from '../config/request' export default { getQuotes: quotes => (state, actions) => request.getAll().then( actions.setQuotes), submitSearch: quotes => (state, actions) => request.getQuery( state.term).then(actions.setQuotes), setQuotes: res => ({ quotes: res.data.quotes.filter( quote => quote.body && quote.body.length < 150) }), updateSearch: ({ term }) => ({ term }), next: e => ({ index, quotes }) => ({ index: index + 1 }), prev: e => ({ index, quotes }) => ({ index: index - 1 }) }
Я использовал преобразователи для getQuotes
и submitSearch
- это означает, что я просто выполняю функцию из функции, а не из значения. Это позволяет использовать нечистые функции внутри вложенной функции, особенно потому, что данные из API менее предсказуемы, чем того требует функциональное программирование. Поскольку выполнение запросов Axios занимает немного времени, состояние фактически не обновляется до тех пор, пока не будет вызван метод setQuotes
после получения данных из API. Остальные действия относительно просты! Обработчики событий сначала принимают событие, а затем текущее состояние - я нашел это немного «волшебным», но в целом опыт с действиями был очень плавным.
Наконец, я создал представления. Основной вид выглядел так:
import { h, app } from 'hyperapp' import Search from './Search' import Quote from './Quote' import { COLORS, FONT_COLORS } from '../config/constants' const quote = (quotes, index) => quotes[index] const color = index => COLORS[index % COLORS.length] const fontColor = index => FONT_COLORS[index % FONT_COLORS.length] export default ({ quotes, index }, { getQuotes, updateSearch, submitSearch, next, prev }) => <div oncreate={getQuotes} className={ quotes ? 'body' : 'body hidden' } style={{ 'backgroundColor': color(index), 'color': fontColor(index) }} > <div className='centered-content'> <div className='container'> { index > 0 && <div onclick={prev} className='direction left' style={{ 'color': fontColor(index) }}> < </div> } { quotes.length > 0 && <Quote quote={quote(quotes, index)} /> } { index < quotes.length - 1 && <div onclick={next} className='direction right' style={{ 'color': fontColor(index) }}> > </div> } <Search updateSearch={updateSearch} submitSearch={submitSearch} /> </div> </div> </div>
Он выглядит практически идентично функциональному компоненту в React! Обработчики событий пишутся в нижнем регистре, но в остальном JSX такой же. Методы жизненного цикла также немного отличаются. Обычно я бы использовал метод componentDidMount
в React для выполнения запроса API, но здесь вместо этого я использовал атрибут oncreate
. По сути, они делают одно и то же, но синтаксис отличается. Я также не видел документации по подпискам, которые важны в Elm. Они позволяют использовать веб-сокеты и добавлять прослушиватели глобальных событий. Тем не менее, некоторые из проблем GitHub упоминали их, поэтому я предполагаю, что они реализованы, но еще не включены в документацию.
Еще у меня было два «подкомпонента», цитата из них была очень простой:
import { h, app } from 'hyperapp' export default ({ quote }) => <div className='quote'> <h1>{quote.body}</h1> <h4>{quote.author}</h4> </div>
Поисковый тоже был:
import { h, app } from 'hyperapp' export default ({ updateSearch, submitSearch }) => <div className='search'> <input onkeyup={ e => { e.keyCode === 13 ? submitSearch() : updateSearch({ term: e.target.value }) } } placeholder='Search quote tags...' /> </div>
Наконец, index.js
объединил элементы из других файлов, чтобы состояние можно было использовать в действиях и представлениях.
import { app } from 'hyperapp' import actions from './actions' import state from './state' import view from './components/View' app(state, actions, view, document.querySelector('.hyperapp-root'))
Эта привязка по сути идентична тому, как Elm объединяет элементы!
Мне понравилось разбивать код на несколько файлов, и я подумал, что он действительно масштабируемый. Я определенно мог представить себе, как в будущем создам что-то большее с помощью HyperApp.
Следующие шаги
Опять же, Hyperapp - один из моих любимых инструментов, которым я недавно научился - рядом, возможно, с Golang. Я обнаружил, что это почти идеальный союз инструментов, которые я использовал в прошлом. Кроме того, это крошечная библиотека, и она очень эффективна, что особенно интересно по сравнению с Angular, о котором я узнал на прошлой неделе! Его API настолько минималистичен, и он так хорошо обеспечивает функциональное программирование. Я определенно рекомендую изучить его как средство реагирования с помощью Redux. Я бы снова на 100% использовал HyperApp, мне это показалось простым, и мне очень понравилась элегантность кода. Я действительно надеюсь, что сообщество продолжает расширяться, документация улучшается, и будет реализована функция перемотки, подобная Redux / Elm. В остальном у меня был отличный опыт работы с Hyperapp, и я уже планирую использовать его снова!
Часть моей серии Обучаясь новому
Другие похожие статьи: