Web-компоненты и hugo

Пост создан 04-14-2021

И вот что в итоге получилось Как можно увидеть - хорошего ничего не получилось. Получилось, что стили от темы применяются ко всему на свете кроме нашего компонента, так как он имеет свой shadowRoot. При этом настройки шрифта применяются и получается какая-то мрачная никак не вписывающаяся в общие стили вещь.
Да это я веб-компонент, без поддержки маркдауна и прочего...
Хотя бы вложенный html есть и то хорошо.
При этом все по спецификации. Ссылки внутри слот компонента стилизированы через CSS от темы, пример: lit-element

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

Написали мы значит на коленке такой вот простой код с применением библиотеки lit-element:

import { LitElement, property, customElement, html, css, internalProperty } from 'lit-element';
import { classMap } from 'lit-html/directives/class-map';

@customElement('nitridan-spoiler')
export class Spoiler extends LitElement {
  @internalProperty()
  private collapsed = true;
  @property()
  public hideText: string = 'Hide';
  @property()
  public showText: string = 'Show';

  private expand(e: MouseEvent): void {
    e.preventDefault();
    e.stopPropagation();
    this.collapsed = false;
  }

  private collapse(e: MouseEvent): void {
    e.preventDefault();
    e.stopPropagation();
    this.collapsed = true;
  }

  public render() {
    return html`
      <div> 
        ${this.collapsed
          ? html`<a href='#' @click='${this.expand}'>${this.showText}</a>`
          : html`<a href='#' @click='${this.collapse}'>${this.hideText}</a>`}
        <div class='${classMap({content: true, hidden: this.collapsed, visible: !this.collapsed})}'>
          <slot></slot>
        </div>
      </div>
    `; 
  }

  static get styles() {
    return css`
      a {
        text-decoration: none;
      }

      .content {
        overflow: hidden;
        transition: max-height 0.5s;
      }

      .content.hidden {
        max-height: 0px;
      }

      .content.visible {
        max-height: var(--nitridan-max-spoiler-height, 500px);
      }
    `;
  }
}

Пойдем с хорошего, а его много:

  • lit-element это очень простая библиотека, под ECMAScript + TypeScript, которая работает как с декораторами так и без них, не тяжелый фреймворк.
  • Учитывая, что веб компоненты в здравом уме в ИЕ тащить никто не будет (пожалуйста, не надо, они там тормозят) бандл из билиотеки + 1-2 компонента всегда будет небольшим. Без gz ~20-30кб.
  • Производительность уже меряли, там все не так уж хорошо, гуглите и читайте сами, меряйте тоже сами, но она достаточная если мы не говорим о гриде на тысячи ячеек с кастомными веб-компонентами.
  • Я встроил таки веб-компонент в блог на движке hugo. То есть получилось, что все работает из коробки на базовых стандартах. И мне даже не пришлось включать какой-то бандлинг. Я просто собрал веб-компонент вебпаком. Закинул как статический файл в блог и подключил.
  • Инкапсуляция на саму разметку компонента работает (забудем о шрифтах). Ссылки на странице не красятся в зависимости от того ходили по ним или нет, а ссылки веб-компонента выглядят убого и перекрашиваются.
  • Мы теоретически даже можем встраивать scalajs в сайт написанный на classic asp + vbscript😵

А теперь поговорим о спорном:

  • Во-первых половинчатость инкапсуляции. О ней читать нужно обязательно. Шрифт у нас не дефолтный какой-то. Элементы в слотах отрисованы стилями парент пейджи.
  • Во-вторых вот этот shadowRoot это конечно хорошо и если представить, что мы прям вылизали компонент, описали все переменные для последующей кастомизации и дали его людям в руки это круто. Но скажите честно. Где вы видели third-party работающее без напильника и массы изменений? Где-то shadowRoot помогает нам, где-то нет. Но если мы верстаем страницу целиком сами наверное брать чужой компонент с недовыставленными переменными (как пример выше) это скорее плохо.
  • Интеграция с другими фреймворками далека от идеала, я не смог нормально завести markdown внутри слота компонента с hugo. Зато с html проблем нет. Но не хочу я писать html😡

Ну и собственно как включается эта штука в hugo

<script src="/js/components.js"></script>
<nitridan-spoiler showText="{{ .Get `showText` }}" hideText="{{ .Get `hideText` }}">
	{{ .Inner }}
</nitridan-spoiler>
Нам надо описать .html файл похожий на то, что выше. Подключение скрипта здесь очень криво сделано, пожалуйта, не делайте так. И далее просто можно пользоваться этим в страничках .md как обычным “shortcode”

Рзбираем таки код компонента и lit-element

  1. @customElement(‘nitridan-spoiler’) - этот декоратор просто превращается в customElements.define, читать тут: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
  2. @property() помечает свойства как “реактивные”. То есть когда оно меняется - мы делаем перерисовку разметки методом render.
  3. @internalProperty() - смотри выше, но для того, что не нужно выставлять наружу(чертова инкапсуляция, зачем она вообще нужна? Все public и не паримся). То есть нужно для хранения внутреннего состояния.
  4. Внутри render мы используем библиотеку lit-html. Авторы говорят, что она очень быстро все отрисовывает без всякого виртуал дома опираясь на современные API новых браузеров. Короче не запускайте с тонной полифилов в IE. Умрет.
  5. static get styles() - авторы библиотеки явно верят в CSS in JS. и предлагают описывать стили помещаемые в shadowRoot прямо в TS/JS файл. Вроде идея неплохая, но подсветка кода и отступы в NVIM у меня так и не завелись. Короче эти строчные литералы в коде боль.
  6. И того получается, что каждый раз, когда мы меняем ивент хендлерами свойство внутреннего сотояния происходит перерисовка компонента. Браузер вычисляет, что поменять нужно только ссылку и классы контейнера контента. Уже скомпилированный stylesheet применяется бразуером при смене стилей. Как-то в теории так это работает.

Так когда это надо?

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

  1. Расширение легаси кода вроде asp.net webforms и древнее. Мы можем легко написать современный компонент и внедрить в древний сайт.
  2. Для платформ вроде SalesForce где нужно расширять чужую верстку своими дополнениями. И эта чужая верстка может часто как-то изменяться.
  3. Быстрое прототипирование. Когда дизайн далек от окончания и не совсем понятно как этим в итоге будут пользоваться.

Буду ли я дальше экспериментировать с веб-компонентами и искать им практическое применение?

Однозначно, да. На данный момент у нас есть масса языков, которые можно скомпилировать под бразуер. Есть масса никак нормально несовместимых фреймворков. Web-компоненты выглядят как путь к упорядочиванию этого всего и реальный шанс постепенно оживлять древние страницы написаные еще в далекие 90-е. Да сейчас это все не развито. Но развивается то где есть энтузиасты. Собственно потому я лично буду смотреть в эту сторону в отличие от реально непрактичных мертвых технологий.