Статьи

Забавное функциональное программирование с помощью Choo Framework

Эта статья была рецензирована Vildan Softic и Yoshua Wuyts . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Поезд идет через мост iDevices

Сегодня мы будем исследовать Choo от @yoshuawuyts — небольшого каркаса, который мог.

Это совершенно новая структура, помогающая создавать одностраничные приложения, которые включают управление состоянием, однонаправленный поток данных, представления и маршрутизатор. С Choo вы будете писать приложения в стиле, аналогичном React и Redux, но с долей стоимости (размера файла) и количества API. Если вы предпочитаете минимальные рамки и любите играть с новыми технологиями на переднем крае, вам понравится исследовать Choo. Так как в другом месте он очень тонкий, он имеет большой смысл для мобильных веб-приложений, в которых размер файла должен быть минимальным.

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

Эта статья будет посвящена Choo v3. На момент написания v4 находится в альфа-версии, так что вам нужно следить за изменениями — этот поезд движется быстро.

Примечание . Эта статья будет наиболее полезна, если вы знакомы с библиотекой декларативного представления, такой как React, и библиотекой управления состоянием, такой как Redux . Если у вас еще нет опыта работы с ними, вы можете найти Choo Docs — Concepts предлагают более подробные объяснения важных концепций.

Попробуйте это дома

Следуйте инструкциям, сняв демо-репозиторий и установив зависимости.

git clone https://github.com/sitepoint-editors/choo-demo
cd choo-demo
npm install

Есть сценарии npm для запуска каждого из примеров, например

 npm run example-1
npm run example-2

Привет чу

Во-первых, нам нужно запросить пакет choo и создать приложение.

Посмотреть файл на GitHub: 1-hello-choo.js

 const choo = require('choo')
const app = choo()

Мы используем модели для размещения нашего состояния и функции для его изменения (редукторы, эффекты и подписки), здесь мы инициализируем наше состояние с помощью свойства title

 app.model({
  state: {
    title: '? Choo!'
  },
  reducers: {}
})

Представления — это функции, которые принимают состояние в качестве входных данных и возвращают один узел DOM. Функция htmlyo-yo .

 const html = require('choo/html')
const myView = (state, prev, send) => html`
  <div>
    <h1>Hello ${state.title}</h1>
    <p>It's a pleasure to meet you.</p>
  </div>
`

Этот синтаксис html`example` Посмотрите эпизод « Давайте напишем код с Кайлом », чтобы подробно рассказать о них.

Маршрутизирует URL-адреса карты к представлениям, в этом случае /

 app.router(route => [
  route('/', myView)
])

Чтобы заставить этот локомотив двигаться, мы вызываем app.start

 const tree = app.start()
document.body.appendChild(tree)

И мы сделали. Запустите npm run example-1

 <div>
  <h1>Hello ? Choo!</h1>
  <p>It's a pleasure to meet you.</p>
</div>

Мы добились существенного прогресса благодаря крошечному API Чу. У нас есть базовая маршрутизация, и мы визуализируем представления с данными из наших моделей. Там не так много, чтобы узнать больше на самом деле.

Подробнее читайте в документах: Модели , Виды

Запуск Choo в браузере

Если вы читаете дома, все примеры используют dev-сервер с именем budo для компиляции исходного кода с помощью browserify и запуска скрипта на простой HTML-странице. Это самый простой способ поиграть с примерами Choo, но вы также можете легко интегрировать Choo с другими упаковщиками или взглянуть на минимальный ванильный подход, если это ваш джем.

Ch-CH-изменения-

Теперь я уверен, что к этому моменту ваш ум взорван, увы, нет смысла использовать Choo для рендеринга статического контента, подобного этому. Choo становится полезным, когда вы меняете состояние во времени и динамические просмотры: это означает реагирование на события, таймеры, сетевые запросы и т. Д.

События в представлении могут быть зарегистрированы с таким атрибутом, как onclickатрибутов событий йо-йо . События могут инициировать действия с помощью функции send

Посмотреть файл на GitHub: 2-state-changes.js

 const myView = (state, prev, send) => {
  function onInput(event) {
    send('updateTitle', event.target.value)
  }

  return html`
    <div>
      <h1>Hello ${state.title}</h1>
      <p>It's a pleasure to meet you.</p>
      <label>May I ask your name?</label>
      <input value=${state.title} oninput=${onInput}>
    </div>
  `
}

Редукторы будут выглядеть знакомо, если вы использовали популярную библиотеку Redux, это функции, которые принимают предыдущее состояние и полезную нагрузку и возвращают новое состояние.

 app.model({
  state: {
    title: '? Choo!'
  },
  reducers: {
    updateTitle: (data, state) => {
      return { title: data }
    }
  }
})

Просмотр обновлений обрабатывается morphdom . Как и в случае с React, вам не нужно беспокоиться о ручном манипулировании DOM, библиотека обрабатывает преобразование DOM между изменениями состояния.

Запустите пример: npm run example-2

Дерево компонентов

Имеет смысл разбить сложный пользовательский интерфейс на небольшие управляемые части пользовательского интерфейса.

Представления могут включать в себя другие представления, передающие необходимые данные, а также функцию send

Наше новое представление будет принимать item<li>updateTitle

Посмотреть файл на GitHub: 3-component-tree.js

 const itemView = (item, send) => html`
  <li>
    <span>Go ahead ${item.name},</span>
    <button onclick=${() => send('updateTitle', item.name)}>make my day</button>
  </li>
`

Представления — это просто функции, поэтому вы можете вызывать их в любом выражении в шаблонном заполнителе ${}

 const myView = (state, prev, send) => html`
  <div>
    <ul>
      ${state.items.map(item => itemView(item, send))}
    </ul>
  </div>
`

Там у вас есть, Choo Views внутри Choo Views.

Запустите пример: npm run example-3

Последствия

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

Примеры эффектов: выполнение запросов XHR (запросов к серверу), вызов нескольких редукторов, сохранение состояния в локальном хранилище.

Посмотреть файл на GitHub: 4-effect.js

 const http = require('choo/http')
app.model({
  state: {
    items: []
  },
  effects: {
    fetchItems: (data, state, send, done) => {
      send('updateItems', [], done)
      fetch('/api/items.json')
        .then(resp => resp.json())
        .then(body => send('updateItems', body.items, done))

    }
  },
  reducers: {
    updateItems: (items, state) => ({ items: items })
  }
})

Эффекты можно вызывать с помощью той же функции send Для представлений существует два важных события жизненного цикла, поэтому вы можете инициировать действия, когда узел DOM добавляется и удаляется из DOM. Это onloadonunload Здесь, как только представление добавляется в DOM, мы fetchItemsconst itemView = (item) => html`<li>${item.name}</li>`

const myView = (state, prev, send) => html`
<div onload=
${() => send(‘fetchItems’)}>
<ul>
${state.items.map(item => itemView(item))}
</ul>
</div>
`

 npm run example-4

Запустите пример: app.start

Подробнее читайте в документации: Эффекты

Подписки

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

Подписки регистрируются на const keyMap = {
37: 'left',
38: 'up',
39: 'right',
40: 'down'
}

app.model({
state: {
pressedKeys: {
left: false,
up: false,
right: false,
down: false
}
},
subscriptions: [
(send, done) => {
function keyChange(keyCode, value) {
const key = keyMap[keyCode]
if (!key) return

const patch = {}
patch[key] = value
send(‘updatePressedKeys’, patch, done)
}
window.addEventListener(‘keydown’, (event) => {
keyChange(event.keyCode, true)
}, false)
window.addEventListener(‘keyup’, (event) => {
keyChange(event.keyCode, false)
}, false)
}
],
reducers: {
updatePressedKeys: (patch, state) => ({
pressedKeys: Object.assign(state.pressedKeys, patch)
})
}
})
Вот пример использования подписок для прослушивания нажатий клавиш и сохранения нажатых клавиш в состоянии.

Посмотреть файл на GitHub: 5-subscription.js

 npm run example-5

Запустите пример: app.router

Подробнее читайте в документах: подписка

Маршрутизация

Ниже вы можете увидеть более полный пример того, как работает маршрутизация в Choo. Здесь send('location:setLocation', { location: href })sheet-router , который поддерживает стандартные и вложенные маршруты. Вы также можете программно обновить маршрут с помощью редуктора местоположения: const homeView = (state, prev, send) => html`
<div>
<h1>Welcome</h1>
<p>Check out your <a href="/inbox">Inbox</a></p>
</div>
`

Посмотреть файл на GitHub: 6-rout.js

Чтобы перейти от просмотра к просмотру, вы можете просто использовать ссылки.

 app.router(route => [
  route('/', homeView),
  route('/inbox', inboxView, [
    route('/:id', mailView),
  ])
])

Сами маршруты можно зарегистрировать так.

 state.params

Динамические части URL могут быть доступны через const mailView = (state, prev, send) => {
const email = state.items.find(item => item.id === state.params.id)
return html`
<div>
${navView(state)}
<h2>
${email.subject}</h2>
<p>
${email.to}</p>
</div>
`

}

 npm run example-6

Запустите пример: onload

Подробнее в документации: Маршрутизатор

Узел состояния и листовые узлы

Представления Choo разработаны как чистые функции, которые принимают данные и возвращают DOM-узлы. React показал, что это может быть отличным способом создания декларативного пользовательского интерфейса, но у него есть свои недостатки. Как вы можете включить компоненты в представление Choo, которые поддерживают свое собственное состояние и изменяют свои собственные узлы DOM? Как можно включить нечистые компоненты в Choo и использовать огромное количество библиотек DOM?

Вот наивная попытка включить визуализацию данных d3 в представление Choo. В функцию const dataVizView = (state) => {
function load(el) {
d3.select(el)
.selectAll('div')
.data(state.data)
.enter()
.append('div')
.style('height', (d)=> d + 'px')
}

return html`
<div onload=
${load}></div>
`

}

 isSameNode

Библиотека различий, которую использует Choo (morphdom), в isSameNode предлагает аварийный люк, который можно использовать для предотвращения повторного рендеринга. Кэш-элемент Choo содержит функции, которые переносят это поведение, чтобы упростить код, необходимый для кэширования и создания виджетов в Choo.

Посмотреть файл на GitHub: 7-friends.js

 const widget = require('cache-element/widget')
const dataViz = widget(update => {
  update(onUpdate)

  const el = html`<div></div>`
  return el

  function onUpdate(state) {
    const bars = d3.select(el)
      .selectAll('div.bar')
      .data(state.data)

    bars.style('height', (d)=> d + 'px')

    bars.enter()
      .append('div')
      .attr('class', 'bar')
      .style('height', (d)=> d + 'px')
  }
})
const dataVizView = (state, prev, send) => dataViz(state)

Запустите пример: npm run example-7

Теперь мы затронули все основные компоненты API-интерфейса Choo, я сказал вам, что он крошечный.

Также есть app.use для расширения работы Choo, позволяя вам перехватывать его поток в разных точках, таких как onActiononStateChange Эти хуки могут быть использованы для создания плагинов или промежуточного программного обеспечения.

Кроме того, рендеринг на стороне сервера может быть выполнен с помощью app.toString (route, state) .

Модульное тестирование

Одним из наиболее известных достоинств функционального программирования является тестируемость, так как же Choo складывается?

Компоненты

Choo Views — это чистые функции, которые принимают состояние в качестве входных данных и возвращают узел DOM, поэтому их легко тестировать. Вот как вы можете визуализировать узел и сделать на нем утверждения с помощью Mocha и Chai.

 const html = require('choo/html')
const myView = (state) => html`
  <div class="my-view">
    ${JSON.stringify(state)}
  </div>
`

describe('Component specs', () => {
  it('should return a DOM node', () => {
    const el = myView({hello: 'yep'})

    expect(el.innerHTML).to.contain('{"hello":"yep"}')
    expect(el.className).to.equal('my-view')
  })
})

Спецификации редуктора

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

 const myReducer = (data, state) => {
  return { title: data }
}

describe('Reducer specs', () => {
  it('should reduce state', () => {
    const prev = { title: 'hello!' }
    const state = myReducer(prev, "? Choo!")

    expect(state.title).to.equal("? Choo!")
  })
})

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

Сильные стороны

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

Требуется немного инструментов. Нет необходимости в JSX или сложных сборочных конвейерах, browserify — это все, что рекомендуется для объединения зависимостей в пакет. Это может быть так же просто, как browserify ./entry.js -o ./bundle.js

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

Минимальный размер 5 КБ означает, что вы можете без проблем включать другие версии Choo или других фреймворков. Это рамки на диете.

Слабые стороны

Это незрелый и будет иметь серьезные изменения. Посмотрите журнал изменений v4 для примера того, как API является движущейся целью. Несмотря на то, что прогресс — это отличная вещь, работа над миграциями между версиями — это потенциальный недостаток.

Возможно, вам придется вручную оптимизировать. Большие библиотеки, такие как React и Angular, которые ожидают владения всем приложением, могут выполнять такие вещи, как оптимизация событий с делегированием вверху дерева. yo-yo

Это не проверено в бою. Когда вы принимаете такую ​​библиотеку, как React, вы можете сделать это с уверенностью, зная, что она используется на некоторых из крупнейших сайтов в Интернете. Вы знаете, что он был тщательно протестирован и будет работать предсказуемо, даже в старых браузерах.

Это новый ребенок на блоке. Популярность означает, что вы можете использовать сеть знаний и использовать готовые компоненты. В последнем примере показано, как включить в библиотеку другие библиотеки, которые управляют частями DOM, но на этом этапе все еще довольно сыро. Патч, который позволил это сделать, был всего несколько дней назад на момент написания этой статьи.

Вывод

Лично мне очень нравится Чу. Он объединяет множество замечательных идей с дружественным API. Сначала я вижу, как использую его в сторонних проектах, чтобы увидеть, как он работает в широком диапазоне сценариев. Я ожидаю достижения ограничений в какой-то момент, но это переломы, когда вы работаете на переднем крае.

Если это вызвало у вас интерес, вы можете прочитать README , ознакомиться с демонстрационными примерами или прочитать Руководство по незавершенному производству для получения дополнительных примеров от автора.

Как вы думаете? Попробуйте и дайте нам знать, как вы поживаете в комментариях ниже.