Задний план

На мой взгляд (от точки зрения разработчика) Google Chrome — лучший браузер на сегодняшний день. Одной из его лучших особенностей является поддержка почти всех последних тенденций и технологий, существующих в мире JS. Он также поддерживает API Shadow DOM, который является темой этого поста. Это имеет больше смысла и смысла в контексте расширений Chrome. В этом посте я покажу вам, как создать простое расширение, которое будет отвечать за добавление небольшой выпадающей кнопки на определенные веб-страницы, действия которых определены в скрипте расширения. Я также покажу вам, как использовать спецификацию ShadowDOM, чтобы изолировать элементы HTML, листы CSS и сценарии JS от целевой веб-страницы.

Окончательный рабочий пример этого руководства можно найти здесь:

Гитхаб

Что такое ShadowDOM?

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

  • Компонент — в течение некоторого времени в мире внешнего интерфейса мы могли заметить постоянное стремление разделить веб-страницы на как можно более мелкие части. Эти мелкие детали можно использовать часто. Здесь на помощь приходят компоненты. Однако не такие, как вы можете видеть, например, в React, а которые предоставляются изначально, веб-браузером — и это то, что мы называем Shadow DOM. Эта технология быстро развивается и будет продолжать развиваться по мере ее систематического развития.
  • Инкапсуляция — тесно связана с веб-компонентами. Он помогает элементам существовать независимо друг от друга, но больше всего не позволяет «вытекать» css за пределы компонента.

Кроме того, ShadowDOM помогает создавать более интуитивно понятные классы CSS, отделяя ваше пространство имен CSS от пространства имен CSS страницы. Это означает, что, например, если страница определяет «большой» класс, вы можете определить свой собственный «большой» класс CSS, и он будет полностью независим от страницы благодаря изолированной области действия ShadowDOM. ShadowDOM также позволяет вам писать более интуитивно понятный JS, потому что document.querySelector также изолирован от страницы.

Наша цель

Мы собираемся создать скрипт, который при установке в браузере Chrome в качестве расширения позволит нам внедрить раскрывающийся список на выбранные страницы. Выпадающий список будет содержать параметры для выбора, например. список желаний, который будет списком недвижимости вашей мечты. Подобный функционал был разработан нами для одного из наших клиентов — HomeAhead.

Проблема

Расширение Chrome без использования ShadowDOM

Улучшенное расширение Chrome с использованием shadowDOM

Снять!

Давайте начнем с загрузки шаблона расширения Chrome, я предлагаю это: https://extensionizr.com/. Затем давайте загрузим пример из настроек chrome://extensions/ (не забудьте, что вы должны быть в режиме разработки, чтобы иметь возможность загружать распакованные файлы). Первым делом нужно отредактировать в manifest.json информацию о сайтах, которые мы хотели бы изменить. Необходимо показать пользователю, какие сайты мы будем контролировать, чтобы пользователь мог предоставить нам необходимые разрешения.

Меняем ключ permissions:

"permissions": [
  "*://www.homes.com/*"
],

В этом случае мы будем делать инъекции на сайт home.com.

Следующим шагом является делегирование определенных сценариев для запуска на предустановленных подстраницах. Ключ content_scripts отвечает за вставку файлов на заданные страницы:

"content_scripts": [
  {
    "matches": [ "*://www.homes.com/property/*", "*://homes.com/property/*" ],
    "js": [ "src/inject/dropdown.js", "src/inject/button.js", "src/inject/homes.js" ],
    "run_at": "document_start"
  }
],

В dropdown.js будет базовый класс, связанный со всей логикой компонента. button.js будет местом, где мы инициализируем базовый Dropdown класс, выбираем держатель для контейнера и назначаем обратные вызовы. Наконец, мы запускаем homes.js — здесь мы ждем полной загрузки страницы и вставляем кнопку.

Код это!

Каждая строка кода JS, которую мы создаем, мы делаем в ES6, также известной как ES2015, без каких-либо транспиляторов. Почему? Потому что Chrome — лучший, и в нем все реализовано :) И, конечно же, без библиотеки jQuery.

Home.js Класс

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

var readyStateCheckInterval = setInterval(function() {
    if (document.readyState === "complete") {
        clearInterval(readyStateCheckInterval);
        const target = document.getElementsByClassName('property-pricetag')[0]
        const button = new Button()
        button.createButtonWrapper(target)
    }
}, 1000);

Здесь мы проверяем с интервалом в 1 секунду полную загрузку страницы. Когда это complete, то благодаря getElementsByClassName мы выбираем элементы DOM, которые будут нашей привязкой для внедрения ShadowDOM.

Класс Button.js

Это простой класс, отвечающий за инициализацию Dropdown и всех его методов, необходимых для работы всех компонентов. В приведенном выше листинге мы видим создание нового объекта класса Button и ниже вызов его метода createButtonWrapper. Это то, что мы реализуем в этом файле

В конструкторе инициализируйте класс Dropdown

this.dropdown = new Dropdown()

и установите обратный вызов для желаемого действия пользователя, в этом случае нажмите на выбранную опцию. Dropdown возвращает то, что вы хотите, например. console.log:

this.dropdown.setOnSelectItemCallback(value => {
  console.log('callback', value)
})

Следующий метод, который мы реализуем, это createButtonWrapper, аргумент которого является узлом к ​​элементу DOM, который мы присоединяем:

createButtonWrapper(DOMTarget) {
  const wrapper = document.createElement('div')
  wrapper.setAttribute('id', 'shadowdom-container')
  DOMTarget.appendChild(wrapper)
  this.dropdown.initShadowDOM(wrapper)
  this.dropdown.render()
}

В предпоследней строке мы передаем DOM-элемент, нашу обертку, которая будет контейнером для изолированного DOM-дерева.

Класс Dropdown.js

Помните нулевое уведомление о jQuery? В этом случае это приведет к излишнему увеличению веса расширения, и, кроме того, мы здесь не занимаемся «ракетной наукой», поэтому мы можем улучшить свои навыки «сырого DOM» и для собственного удовольствия :)

Хотя конструктор прост, стоит упомянуть две вещи:

this.onSelectCallback = noop => noop
this.onTriggerClick = this.onTriggerClick.bind(this)

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

Второй метод, о котором стоит упомянуть, это initDropdownClick(). Он отвечает за щелчок обёртки привязки. Благодаря ShadowDOM мы уверены, что слушатель будет назначен только одному элементу DOM.

const trigger = this.shadow.getElementById('dropdown-trigger')
trigger.addEventListener('click', this.onTriggerClick)

Элемент dropdown-trigger определен в методе createHTML(), в котором мы сохраняем весь HTML-код введенного div в виде строки.

return `
  <style>.wrapper { display: none; }</style>
  <div class="wrapper" id="dropdown-wrapper">
    <div class="label" id="dropdown-trigger">Select target</div>
    <div class="list">
      <div class="option" data-value="one">Option 1</div>
      <div class="option" data-value="two">Option 2</div>
    </div>
 </div>
`

Тег style с none здесь, потому что мы не будем показывать неработающее выпадающее меню конечному пользователю. Почему? Ответ прост, потому что наши CSS определены в отдельном файле. Chrome Extensions API не позволяет нам их включать. Мы вынуждены сделать вызов GET для этого ресурса.

getStyles() {
  const url = chrome.extension.getURL("css/styles.css")
  fetch(url, { method: 'GET' }).then(resp => resp.text()).then(css => {
    this.shadow.innerHTML += `<style>${css}</style>`
    this.initListeners()
  })
}

Чтобы этот метод работал, мы должны обновить manifest.json с дополнительными правами.

"web_accessible_resources": [
  "css/*"
]

Это указывает пути для упакованных ресурсов, которые, как ожидается, можно будет использовать в контексте веб-страницы.

Значение выбранного выпадающего списка перехватывается методом onOptionClick(). Он считывает значение из атрибута data HTML5:

const value = option.getAttribute('data-value')

this.close()
this.onSelectCallback.call(null, value)

В последней строке мы вызываем обратный вызов, который определяется пользователем.

Последнее, что стоит упомянуть, это закрытие раскрывающегося списка при размытии. Теперь, если мы щелкнем за пределами раскрывающегося списка, ничего не произойдет. Вот хитрый способ обработки этого щелчка размытия — мы устанавливаем прослушиватель для всего документа document.addEventListener('click', ...), а затем проверяем, имеет ли кликнутый target атрибут id нашего контейнера, если нет, то мы уверены, что пользователь щелкнул за пределами раскрывающегося списка, и мы может закрыть его.

if (e.target.getAttribute('id') !== 'shadowdom-container') {
   this.close()
}

Чтобы проверить, правильно ли работает наше расширение, нам нужно перейти на страницу с образцом собственности в домене home.com, например. https://www.homes.com/property/119-13th-st-n-texas-city-tx-77590/id-500019320228/

Итак, мы, наконец, подошли к концу. Как видите, благодаря ShadowDOM очень легко создать совершенно отдельный элемент страницы. Это гарантирует, что элемент будет выглядеть одинаково на каждой странице, в которую мы внедряем, поэтому нам не нужно делать избыточные объявления !important в классах css.

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