Быстрый старт с WebComponents

в 11:21, , рубрики: javascript, mocha, quickstart, web components

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

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

Все эксперименты приводимые далее проверялись в Chrome и Firefox может быть даже не самых новых версий.

Итак начнем.

Для начала создадим директорию для проекта и перейдем в него.

mkdir mywebcomp
cd mywebcomp

Выполним:

npm init 

в этом каталоге ответив на все вопросы по умолчанию.

Создадим в каталоге файл index.html с самым простым содержимым.

<html lang="en">

<body>

</body>
</html>

Добавим тег для элемента, имя должно обязательно содержать дефис, это сигнал для подсистемы CusomElements для попытки определения этого элемента как надстраивающего стандартные.

<html lang="en">

<body>

<my-webcomp></my-webcomp>

</body>
</html>

Добавим класс обработчик в теге script.

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           this.insertAdjacentHTML('beforeEnd', `<div>Hello</div>`)
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

В модульном теге script, мы определили новый класс который c помощью метода customElements.define() определили за тегом my-webcomp. А добавив код в метод connectedCallback() мы обеспечили его вызов при добавлении реализации компонента в дерево. Результат уже можно посмотреть в браузере:

Быстрый старт с WebComponents - 1

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

<html lang="en">

<body>

<template id="myWebCompTemplate">
   <div>Hello</div>
</template>

<my-webcomp></my-webcomp>

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let tplEl = document.querySelector('#myWebCompTemplate');
           let html = document.importNode(tplEl.content, true);
           this.appendChild(html);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

</body>
</html>

В коде компонента вставку строки c html мы заменили на получение элемента шаблона по id. Импорт, т.е. создание копии этого элемента и прицепление к содержимому текущего.

id назван в нотации camelCase т.к. все айдишники элементов прокидываются в глобальное пространство имен и при использовании дефисов или других спец. символов доступ к ним может быть менее элегантен. Т.е. мы могли бы вместо:

           let tplEl = document.querySelector('#myWebCompTemplate');
           let html = document.importNode(tplEl.content, true);

написать в одну строчку:

let html = document.importNode(myWebCompTemplate.content, true);

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

<my-webcomp id="myWebComp"></my-webcomp>

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let html = document.importNode(myWebCompTemplate.content, true);
           this.appendChild(html);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>
<script type="module">
   myWebComp.showMessage();
</script>

При таком раскладе алерт будет показываться сразу при загрузке страницы.

Теперь повесим обработчик клика мышки для нашего компонента который будет выводить алерт сообщение.

<my-webcomp id="myWebComp" onclick="this.showMessage(event)"></my-webcomp>

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let html = document.importNode(myWebCompTemplate.content, true);
           this.appendChild(html);
       }
       showMessage(event) {
           alert("This is the message");
           console.log(event);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

Теперь при нажатии на сообщение у нас будет реакция на действия пользователя.

Быстрый старт с WebComponents - 2

Аргументом метода showMessage() также объявляется объект event, который хранит данные о событии, например координаты клика или ссылку на сам элемент.

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

Добавим второй экземпляр элемента и определим для каждого из них разные свойства greet-name значения которых будут выводиться при нажатии на элемент.

<my-webcomp id="myWebComp" greet-name="John" onclick="this.showMessage(event)"></my-webcomp>
<my-webcomp id="myWebComp2" greet-name="Josh" onclick="this.showMessage(event)"></my-webcomp>
<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let html = document.importNode(myWebCompTemplate.content, true);
           this.appendChild(html);
       }
       showMessage(event) {
           alert("This is the message " + this.getAttribute('greet-name'));
           console.log(event);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

Теперь при нажатии на первый будет выводиться “This is the message for John”, а на второй “This is the message for Josh”.

Может так случиться что атрибут надо будет использовать не в обработке события, а прямо отрендерить в шаблон, для этого мы добавим id целевому элементу и подставим значение из апи сразу после рендеринга копии объекта шаблона.

<template id="myWebCompTemplate">
   <div id="helloLabel">Hello</div>
</template>

<my-webcomp id="myWebComp" greet-name="John" onclick="this.showMessage(event)"></my-webcomp>
<my-webcomp id="myWebComp2" greet-name="Josh" onclick="this.showMessage(event)"></my-webcomp>

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let html = document.importNode(myWebCompTemplate.content, true);
           this.appendChild(html);
           this.querySelector('#helloLabel').textContent += ' ' + this.getAttribute('greet-name');
       }
       showMessage(event) {
           alert("This is the message " + this.getAttribute('greet-name'));
           console.log(event);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

Получается вот так:

Быстрый старт с WebComponents - 3

Вместо .textContent может быть .innerHTML или можно вызвать у объекта из селектора тот же метод .insertAdjacentHTML() мы делали в самом начале.

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

           this.attachShadow({mode: 'open'});

Теперь правда все DOM обращения к this придется заменить на this.shadowRoot благо их пока не так много.

<script type="module">
   class MyWebComp extends HTMLElement {
       connectedCallback() {
           let html = document.importNode(myWebCompTemplate.content, true);
           this.attachShadow({mode: 'open'});
           this.shadowRoot.appendChild(html);
           this.shadowRoot.querySelector('#helloLabel').textContent += ' ' + this.getAttribute('greet-name');
       }
       showMessage(event) {
           alert("This is the message " + this.getAttribute('greet-name'));
           console.log(event);
       }
   }
   customElements.define('my-webcomp', MyWebComp);
</script>

Визуально работа этого кода опять никак не изменится, но теперь в глобальном пространстве имен не будет никакого helloLabel, а у на странице уже 2 элемента с таким идентификатором. А получить доступ к ним можно будет например вот так:

myWebComp.shadowRoot.getElementById('helloLabel');

и то если вы не закроете дерево передав соответствующий атрибут в методе .attachShadow().

У нас получилось довольно много кода и размещать его прямо в html файле тоже не очень правильно. Поэтому создадим файл my-webcomp.js и перенесем в него наш класс предварив инструкцией export, а в теге script добавим импорт этого класса, чтобы получилось вот такое:

<script type="module">
   import { MyWebComp } from "./my-webcomp.js";

   customElements.define('my-webcomp', MyWebComp);
   myWebComp.shadowRoot.getElementById('helloLabel');
</script>

На работоспособности это никак не скажется, но теперь вся бизнес логика у нас отдельно в .js, конфигурация осуществляется в html атрибутами, а модульную асинхронную инициализацию берут на себя механизмы модульной и компонентной системы браузера.

Правда с этого момента открывать index.html как локальный для разработки не получится, т.к. браузер заблокирует загрузку скрипта с файловой системы. Если у вас есть nodejs можно поставить простейший веб-сервер:

npm i http-server -g

и запускать его командой http-server в каталоге с проектом, при запуске он подскажет хост и порт с которого можно открывать страницу

http://127.0.0.1:8080 

Которая и будет отныне адресом отладочной страницы с нашим элементом.

Немаловажным для разработки является написание тестов, они помогают удерживать код работоспособным в ходе развития его компонентов. Для тестирования веб-компонентов можно использовать mocha в режиме запуска из браузера.

Добавим пакеты.

npm i mocha chai wct-mocha --save-dev

Для этого создадим каталог test и добавим в него файл all.html такого содержимого:

<!doctype html>
<html>
<head>
   <meta charset="utf-8">
   <script src="../node_modules/mocha/mocha.js"></script>
   <script src="../node_modules/chai/chai.js"></script>
   <script src="../node_modules/wct-mocha/wct-mocha.js"></script>
</head>
<body>

<script>

   WCT.loadSuites([
       'my-webcomp.tests.html',
   ]);

</script>
</body>
</html>

Скопируем наш index.html в test/ задав ему имя my-webcomp.tests.html и добавим такое же содержимое head как и в all.html.

Однако, нам обычную инициализацию и манипуляции теперь надо будет заменить на производимую в рамках запуска тестов:

<script type="module">
   import { MyWebComp } from "../my-webcomp.js";

   customElements.define('my-webcomp', MyWebComp);
   suite('MyWebComp', () => {
       test('is MyWebComp', () => {
           const element = document.getElementById('myWebComp');
           chai.assert.instanceOf(element, MyWebComp);
       });
   });
</script>

Теперь при заходе на

http://127.0.0.1:8080/test/all.html

будет показан отчет о выполнении тестов.

Быстрый старт с WebComponents - 4

Но может потребоваться запускать тесты автоматизировано и в разных браузеров для этого надо поставить специальную утилиту:

sudo npm install --global web-component-tester --unsafe-perm

и запускать вот так:

wct --npm

Эту строчку можно добавить в секцию test файла package.json и запускать как:

npm test

Быстрый старт с WebComponents - 5

Рекомендуется тестировать каждый значимый метод класса в рамках отдельного теста. Если надо сделать общую для каждого инициализацию ее следует определить в методе suiteSetup():

suiteSetup(() => {

});

внутри suite.

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

npm publish 

или хотя бы

git push

Книг на тему пока не так уж много, но вся расширенная документация легко находится по словам Web Components, CustomElements, ShadowDOM, Native Template Tag, Custom Events.

Сверить свой код можно с репозиторием: https://bitbucket.org/techminded/mywebcomp

Автор: syncro

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js