Приготовьтесь отправиться в путешествие по изучению прототипирования приложения с помощью веб-компонентов, модулей es6, event target, bit cli и т. д. Вместе мы узнаем, как использовать веб-компоненты и изучим несколько дополнительных особенностей.

В этой статье мы выполним прототипирование RSS reader с использованием веб-компонентов. Конечный результат будет выглядеть следующим образом:

Код можно найти на GitHub.

Почему веб-компоненты??

Поговорим о том, почему стоит выбрать веб-компоненты для UI-стратегии. Есть несколько причин:

  1. Техническая перспективность. Веб-компоненты представляют собой стандарт и поддерживаются браузером. Краткая история сети показывает, что выбор стандарта приносит пользу.
  2. Framework-agnostic. Когда несколько команд работают над одним приложением с несколькими библиотеками, такими как Vue и React, достижение одной функциональности между этими библиотеками представляет трудную задачу. Необходима стандартизация!
  3. Повторно используемая система дизайна. Еще один способ применения framework-agnostic компонентов проявляется при необходимости создания системы дизайна для команды. 
  4. Размер bundle. Несмотря на то, что React является более завершенным с точки зрения использования API и поддержки библиотек, порой размер действительно имеет значение.

Что такое веб-компоненты?

Благодаря веб-компонентам можно развивать инкапсулированный от основного документа компонент. Веб-компоненты предлагают:

  1. Пользовательский элемент — это API Javascript, с помощью которого можно определить новый тип html-тега в соответствии с коллекцией компонентов.
  2. Шаблоны HTML — представляют собой теги <template> и <slot>, с помощью которых можно определить layout шаблона.
  3. Shadow DOM — специфичный для компонента. Это своего рода изолированное окружение для DOM компонента, отделенное от основного документа.

Вместе эти три API позволяют инкапсулировать функциональность компонента и с легкостью отделить его от основного APP. По сути, с его помощью можно расширить api DOM с дополнительными тегами.

Как работает lit?

Lit — это абстракция над оригинальным api, предоставляющая две основные возможности:

Lit-html — библиотека для созданияповторно используемых html-шаблонов в контексте javascript.

Эта библиотека использует функцию под названием теговые шаблоны вместе с es6, которые выглядят следующим образом:

tag `some ${boilerPlate} in my string`

С помощью этой функции можно выполнить парсинг строки с пользовательским элементом. Это ядро lit-html, сочетающее шаблоны в javascript прямо в браузере. В случае с lit, функция render внутри элемента lit может содержать следующее выражение:

// bind function to click
// bind text to button body
html`<button @click=${myFunc}>${buttonText}</button>`

// it can also be composable and conditional
// here we see internal lit components rendered according to a condition.
html`<div>${isReady ? html`<main-app>`: html`<app-load/>` }</div>`

Документацию можно посмотреть здесь.

lit-element — базовый класс для компонентов. Он предоставляет способ определения props, привязку к жизненному циклу компонента и унифицированный интерфейс компонента.

Чтобы разобраться подробнее, рассмотрим компонент nav-bar:

// imports are all es6 modules
import { html, LitElement } from 'https://unpkg.com/lit-element?module'
import { createEvent, eventChangeCurrent } from '../rss/events.js'

// we extend the LitElement component to have a common life cycle.
export class NavBar extends LitElement {

// lit uses this static function to track the properties the component needs to receive
// every time these properties change it will re-render.
  static get properties () {
    return {
      list: { type: Array }, // notice we use the built in Array class as type
      emitter: { type: Object }
    }
  }

  constructor () {
    super()
    this.list = [] // we need to initialize our properties
    this.emitter = {}
    this.changeCurrent = this.changeCurrent.bind(this) // bind changeCurrent before passing it to lit
                                                       // you should always bind once.
  }

    // click handler.
  changeCurrent (e) {
    const { url, name } = e
    // like in our rss client, all communication is done via event emitters
    this.emitter.dispatchEvent(createEvent(eventChangeCurrent, { name, url }))
  }

  render () {
    // we return an html function call to parse our html template
    return html`
      <nav>
        <!-- inner lit components are composed with another template -->
        ${this.list.map((listItem) => html`
        <!-- all custom element must use a '-' in the name, so the browser will ignore them on initial rendering-->
        <nav-item
        <!-- syntax for binding an event handler -->
          @click=${this.changeCurrent}
        <!-- pass in properties as attributes on the -->
          changeCurrent=${this.changeCurrent}
          url=${listItem.url}
          name=${listItem.name}>
        </nav-item>`)}
      </nav>
    `
  }
}

// in most code examples you would see `window.customElements.define`call to
// register the component in the browser. I see this as breaking encapsulation.
// This is not lit faults, it's part of the spec which can improve.

Создание RSS-Reader!

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

Исходный код проекта можно найти в этом репозитории.

Основные ограничения по дизайну:

  1. Lit-element. В этом проекте используются lit-html и lit-element от команды разработчиков polymer. Это отличная библиотека для работы над стандартами веб-компонентов, устраняющая множество проблем при работе с шаблонами, однако стоит отметить, что создание lit было вдохновлено библиотекой hyper, которую также стоит изучить.
  2. Отсутствие Bundle (практически). В попытке изучить несколько новых функций сети, этот проект использует модули es6. Однако это лишь исключение из правила, парсер RSS от Bobby Brennan — это “обычный” пакет браузера.
  3. Подходит только для браузера. У этого проекта отсутствует компонент backend.
  4. Все модули доступны на платформе компонентов bit.devдля повторного использования. bit cli и платформа — это одни из лучших способов обмена компонентами JS и в особенности веб-компонентами.
  5. В этом проекте используются timers и eventTarget вместо workers. Workers не работают с модулями es6.
  6. Этот репозиторий находится на стадии прототипирования, поэтому не содержит тестирования.

Рассмотрим основные entry points приложения. index.html

<html>
    <head>
        <title>RSS Reader</title>
        <style>
            main {
                display: flex;
            }
            nav-bar {
                margin-right: 20px
            }
        </style>
    </head>
    <body>

        <main id="main-app">
            <!-- two main components one for the left panel
                 and one for rendering the rss items  -->
            <nav-bar id="side-bar"></nav-bar>
            <item-list id="main-list"></item-list>
        </main>
        <!-- non es module to have RSS parser-->
        <script src="https://unpkg.com/[email protected]/dist/rss-parser.js"></script>
        <script type="module">
            import { main } from '/source/reader.js'
            main()
        </script>
    </body>
</html>

Главная функция в файле reader.js:

export function main () {
  defineElements(elements) // define all elements in one place
  const store = createStore() // create main store to update for new channels
  hookUpEvents(store) // define main events from the ui to the store.
  topLevelRender(store.getSideBarList(), '#side-bar', store.emitter) // render side-bar.
}

Суть заключается в том, что все части соединяются посредством событий, а каждый компонент приложения независим. Чтобы увидеть остальную часть приложения, загляните в репозиторий.

Общие

  1. index.html — в качестве главного layout проекта.
  2. reader.js — главный файл javascript проекта, устанавливающий event emitters.

Папка Elements — веб-компоненты lit-element.

  1. item-list.js — список элементов feed, отображающий текущий выбранный feed.
  2. nav-bar.js — редактирование и использование feed.
  3. rss-item.js/nav-item.js — представляет фрагмент внутри соответствующих списков.

Папка RSS. Хранилище и возможности rss

  1. events.js — содержит все названия событий и функцию создания событий.
  2. feed-key.js — функция для создания уникального ключа feed в хранилище.
  3. rss-client.js — получает и парсирует rss feed.
  4. rss-store — главное состояние приложения.

Папка Utils

  1. defer-function.js используется для отправки асинхронных событий.
  2. define-elements.js по возможности избегает глобальных веб-компонентов.

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

bit CLI — это основной движок для возможности повторного использования. С помощью инструмента Bit можно написать модульный код, а также управлять исходным кодом и зависимостями.

Рассмотрим еще один компонент. Фрагмент кода для компонента rss client.

//imports include a .js file because these are es6 modules (detailed later)
import { eventRssItems, createEvent } from './events.js'
import { createFeedKey } from './feed-key.js'

// creates a client object with the startFeed, stopFeed, and poll functions
// emitter - communication medium to dispatch events
// data - array of url:string and name:string to identify channels
export function createClient (emitter, data) {
  const feeds = {}
  const client = {
    // Receives a data object with name and url and starts to track it over time
    startFeed: function ({ name, url }) {
      const key = createFeedKey(name, url) // createFeedKey is the primary key for channels in the app.
      feeds[createFeedKey(name, url)] = feeds[key] || start(name, url, emitter)
      return feeds[key]
    },
    // Receives a data object with name and url and stops tracking it.
    stopFeed: function ({ name, url }) {
      const key = createFeedKey(name, url)
      if (feeds[key]) {
        clearTimeout(feeds[key].timer)
        delete feeds[key]
      }
    },
    // poll a feed one time
    poll: async function ({ name, url }) {
      return pollFeed(name, url, emitter)
    }
  }
  // start
  Object.values(data).map((value) => client.startFeed(value))
  return client
}
// creates a timer for a feed  which runs every fixedTimer milliseconds
async function start (name, url, emitter) {
  const fixedTimer = 10 * 1000 // 10 seconds

  async function timerHandler () {
    await pollFeed(name, url, emitter)
    startData.timer = setTimeout(timerHandler, fixedTimer)
  }

  const startData = {
    name,
    url,
    timer: setTimeout(timerHandler, 0)
  }
  return startData
}
// using the RSSParser package parsed a specific feed.
async function pollFeed (name, url, emitter) {
  const feedUrl = `https://cors-anywhere.herokuapp.com/${url}` // hack to avoid cors problem
  const parser = new window.RSSParser()
  const feed = await parser.parseURL(feedUrl)
  feed.name = name
  feed.url = url
  emitter.dispatchEvent(createEvent(eventRssItems, feed))
  return feed
}

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

Некоторые проблемы этого проекта:

  1. `customElements.define` является глобальным. Как было сказано выше, компоненты определены глобально. Помимо этого, во всех рассмотренных мной примерах метод define вызывается внутри модуля, что приводит к инкапсуляции и коллизии имен при росте базы кода компонента в приложении. В попытке переместить все это в одно место я создал компонент define-element.
  2. Не так легко использовать повторно. Чтобы повторно использовать в React компонент, нужно упаковать в него веб-компонент. Это необходимо для контроля над распространением событий и props.
  3. При работе с модулями es6 и выходе из node разрешение модуля трудно определить интуитивно. Можно ожидать, что папка преобразуется в index.js, если рассматривать ее как систему модулей. Однако если представить ее в качестве веб-сервера, возвращающего ассеты, ситуация приобретает смысл. Также добавление этих .js портит внешний вид кода.

Что мы рассмотрели?

Мы изучили прототип приложения RSS reader, а также его структурирование для повышения модульности. Мы узнали, почему стоит использовать веб-компоненты, а также как интегрировать их в приложение. И наконец, мы рассмотрели несколько проблем, связанных с использованием веб-компонентов.

Читайте также:


Перевод статьи Doron Tsur: Prototyping with Web Components: Build an RSS Reader

Предыдущая статьяКак легко оптимизировать Jupyter Notebook. Часть 2
Следующая статьяЯзыки C и C++. Где их используют и зачем?