Написать расширение для Chrome непросто. Это не то же самое, что разработка веб-приложения: не хочется перегружать браузер оверхедом JS, ведь расширения работают одновременно с сайтами. Более того, у нас нет инструментов упаковки или отладки из привычных фреймворков.

Когда я решил заняться созданием расширения для Chrome, то обнаружил: блог-постов и статей об этом довольно мало. И информации оказывается даже ещё меньше, если вам захочется использовать новые инструменты, например TailwindCSS.

В этом руководстве мы напишем расширение для Сhrome с помощью Parcel.js для упаковки и просмотра результатов, а также TailwindCSS для оформления. Кроме того, мы отделим стилизацию расширения от веб-сайта, чтобы избежать конфликта CSS.

Есть несколько типов расширений для Chrome, достойных упоминания:

  • Скрипты содержимого. Наиболее распространённый тип. Они запускаются в контексте веб-страницы и могут изменять её. Именно такое расширение мы и будем создавать.
  • Выпадающее окно (popup). Использует иконку справа от адресной строки, чтобы открыть окно с каким-то HTML.
  • UI с опциями. Пользовательский интерфейс для настройки параметров в качестве расширения. Получить доступ к нему можно, щелкнув правой кнопкой мыши по значку расширения и выбрав пункт “Параметры” или перейдя на страницу расширения из списка расширений Chrome: chrome://extensions.
  • Расширение DevTools. Добавляет функциональность в инструменты разработчика. Оно может добавлять новые панели интерфейса, взаимодействовать с проверяемой страницей, получать информацию о сетевых запросах и многое другое  —  документация Google Chrome.

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

Упаковываем расширение с Parcel.js V2

Parcel.js  —  это упаковщик веб-приложений, не требующий конфигурации. Входным может быть файл любого типа, упаковщик прост в использовании и работает с любыми приложениями, включая расширения Chrome. Создаём папку demo-extension . Удостоверьтесь, что у вас установлены yarn или npm. Здесь будем работать с yarn. Установим Parcel как локальную зависимость и создадим папку src

mkdir demo-extension && cd demo-extension && 
льyarn init -y

yarn add -D parcel@next

mkdir src

Добавляем манифест

Каждому браузерному расширению необходим файл манифеста. Именно там мы определяем версию и метаданные расширения, а также скрипты, которые в нём работают. Контент, фон, всплывающее окна, разрешения, если они нужны и так далее. Вы найдёте полное описание файла манифеста в документации Chrome: https://developer.chrome.com/extensions/manifest. Давайте двинемся дальше и добавим в src файл manifest.json с такими строками:

{
  "name": "Demo extension",
  "description": "An extension built with Parcel and TailwindCSS.",
  "version": "1.0",
  "manifest_version": 2,
}

Прежде чем углубиться в детали работы расширения Chrome, установим и настроим TailwindCSS.

Подключаем TailwindCSS

TailwindCSS  —  это CSS-фреймворк, применяющий служебные классы низкого уровня для создания переиспользуемых и настраиваемых компонентов интерфейса. Tailwind устанавливается двумя способами, самый распространённый —  установка с помощью NPM. Кроме того, сразу же стоит добавить autoprefixer и postcss-import:

yarn add tailwindcss

yarn add -D autoprefixer postcss-import

Они нужны, чтобы добавить префиксы поставщиков к стилям и иметь возможность писать конструкции @import "tailwindcss/base", импортируя файлы Tailwind прямо из node_modules.

Теперь, когда всё установлено, давайте создадим файл postcss.config.js в корневом каталоге. Этот файл  —  конфигурация для PostCSS. Вставим в него такой код:

module.exports = {
  plugins: [
    require("postcss-import"),
    require("tailwindcss"),
    require("autoprefixer"),
  ],
};

Порядок плагинов здесь имеет значение! Это всё, что нужно, чтобы начать использовать TailwindCSS в вашем расширении. Начинаем. Создадим файл style.css в папке src и импортируем в него стили Tailwind:

@import "tailwindcss/base"; 
@import "tailwindcss/utilities";

Очищаем CSS с помощью PurgeCSS

Убедимся, что мы импортируем только те стили, которые используем, включив очистку. Создадим конфигурационный файл Tailwind, запустив такую команду:

$ npx tailwindcss init

Теперь у нас есть tailwind.config.js. Чтобы удалить неиспользуемый CSS, добавляем пути ко всем нашим файлам JS в поле конфигурации purge:

module.exports = {
  purge: [
    './src/**/*.js', ?
  ],
  theme: {},
  variants: {},
  plugins: [],
}

Теперь CSS будут очищены, а неиспользуемые стили удалены при сборке для продакшна.

Включаем горячую перезагрузку

Chrome не перезагружает файлы при внесении изменении, то есть нам нужно нажимать кнопку “Перезагрузить” на странице расширений каждый раз, когда мы хотим посмотреть на результат. К счастью, есть пакет NPM для автоматической перезагрузки:

$ yarn add crx-hotreload

Чтобы использовать его, создадим файл background.js в папке src и импортируем в этот файл crx-hotreload:

import "crx-hotreload";

Наконец, добавим указатель на background.js в manifest.json, чтобы он мог работать с нашим расширением: горячая перезагрузка в продакшне отключена по умолчанию:

{
  "name": "Demo extension",
  "description": "An extension built with Parcel and TailwindCSS.",
  "version": "1.0",
  "manifest_version": 2,
  "background": { ?
    "scripts": ["background.js"]
  },
}

Достаточно конфигураций. Давайте создадим небольшую форму-скрипт в расширении.

Типы скриптов расширения Chrome

Как уже упоминалось, у расширений Chrome есть несколько типов скриптов:

  • Скрипты содержимого —  это сценарии, которые выполняются в контексте посещаемой веб-страницы. Вы можете запустить любой код JavaScript, в противном случае доступный на любой обычной веб-странице, включая доступ к DOM и манипулирование им.
  • Фоновые скрипты  —  это место, где вы можете реагировать на события браузера с доступом к API расширения.

Добавляем скрипт содержимого

Создадим файл content-script.js в папке src. И добавим HTML-форму в только что созданный файл:

import cssText from "bundle-text:../dist/style.css";

const html =
`
<style>${cssText}</style>

<section id="popup" class="font-sans text-black z-50 w-full fixed top-0 right-0 shadow-xl new-event-form bg-white max-w-sm border-2 border-black p-5 rounded-lg border-b-6">
  <header class="flex mb-5 pl-1 items-center justify-between">
    <span class="text-2xl text-black font-extrabold">New event!</span>
  </header>
  <main class="event-name-input mb-6">
    <div class="mb-6">
      <label
        for="event-name"
        class="font-bold pl-1 block mb-1 text-black text-xl"
        >
      Event name
      </label>
      <div class="duration-400 flex bg-white border-black border-2 rounded-lg py-4 px-4 text-black text-xl focus-within:shadow-outline">
<input
          id="event-name"
          name="event-name"
          type="text"
          placeholder="web.dev LIVE"
          class="font-medium w-full focus:outline-none"
          />
      </div>
    </div>
    </div>
    <div class="mb-6">
      <label
        for="event-date"
        class="font-bold pl-1 block mb-1 text-black text-xl"
        >
      Date
      </label>
      <div class="event-date-input duration-400 border-black flex bg-white border-2 rounded-lg py-4 px-4  text-xl focus-within:shadow-outline">
        <input
          id="event-date"
          name="event-date"
          type="date"
          class="font-medium w-full focus:outline-none"
          />
      </div>
    </div>
    <div class=" mb-8">
    <label
      for="event-time-input"
      class="font-bold pl-1 block mb-1  text-xl"
      >
    Time
    </label>
    <div class="inline-flex items-center">
      <input
        id="event-time-input"
        type="time"
        value="17:30"
        class="border-black mr-4 lowercase duration-400 w-auto bg-white text-xl border-2  rounded-lg px-4 py-4 focus:outline-none focus:shadow-outline"
        />
      <div class="inline-flex flex-col">
        <span class="text-xl font-bold">Casablanca</span>
        <span class="text-base font-normal">Africa</span>
      </div>
    </div>
  </main>
  <footer>
  <button 
    class="duration-400 bg-green-400 text-xl py-4 w-full rounded-lg border-2 border-b-6 leading-7 font-extrabold border-black focus:outline-none focus:shadow-outline"
    >
  Save </button>
  </footer
</section>
`

const shadowHost = document.createElement("div");
document.body.insertAdjacentElement("beforebegin", shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

shadowRoot.innerHTML = html

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

Теневой DOM  —  мощная техника инкапсуляции стилей: область применения стиля ограничивается теневым деревом. Таким образом ничего не просачивается на веб-страницу. Кроме того, внешние стили не переопределяют содержимое дерева, хотя переменные CSS всё ещё доступны.

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

Будьте осторожны: единственный способ стилизовать содержимое теневого дерева  —  встроить стили. Parcel V2 из коробки есть функция, благодаря которой вы можете импортировать содержимое одного пакета и использовать его в качестве скомпилированного текста внутри ваших файлов JavaScript. Именно это мы и сделали со своим пакетом style.css. Parcel заменит его во время упаковки.

 Теперь мы можем автоматически встроить CSS в Shadow DOM во время сборки. Конечно, мы должны сообщить браузеру о файле content-script.js, в котором встраивается style.css. Для этого включаем скрипт содержимого в манифест. Обратите внимание на секцию content-scripts ниже первого блока:

{
  "name": "Demo extension",
  "description": "An extension built with Parcel and TailwindCSS.",
  "version": "1.0",
  "manifest_version": 2,
  "background": {
    "scripts": ["background.js"]
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-script.js"],
    }
  ]
}

Чтобы обслуживать наше расширение, добавим несколько скриптов к package.json:

"scripts": {
    "prebuild": "rm -rf dist .cache .parcel-cache",
    "build:tailwind": "tailwindcss build src/style.css -c ./tailwind.config.js -o dist/style.css",
    "watch": "NODE_ENV=development yarn build:tailwind && cp src/manifest.json dist/ && parcel watch --no-hmr src/{background.js,content-script.js}",
    "build": "NODE_ENV=production yarn build:tailwind && cp src/manifest.json dist/ && parcel build src/{background.js,content-script.js}",
  }

Наконец, запускаем yarn watch, переходим в chrome://extensions и убеждаемся, что в правом верхнем углу страницы включен режим разработчика. Нажмите на кнопку “Загрузить распакованный” и выберите папку dist в разделе demo-extension.

Если вы получили ошибку Error: Bundles must have unique filePaths, ее можно исправить, просто удалив main из package.json .

Подготовка к публикации

Прежде чем углубляться в эту тему, давайте добавим новый скрипт в конфигурацию NPM, который поможет сжать файлы расширения в соответствии с требованиями Chrome.

"scripts": {
    "prebuild": "rm -rf dist .cache .parcel-cache",
    "build:tailwind": "tailwindcss build src/style.css -c ./tailwind.config.js -o dist/style.css",
    "watch": "NODE_ENV=development yarn build:tailwind && cp src/manifest.json dist/ && parcel watch --no-hmr src/{background.js,content-script.js}",
    "build": "NODE_ENV=production yarn build:tailwind && cp src/manifest.json dist/ && parcel build src/{background.js,content-script.js}",
  }

Если у вас ещё не установлен zip, пожалуйста, выполните команду:

  • На MacOS: brew install zip.
  • На Linux: sudo apt install zip.
  • На Windows: powershell Compress-Archive -Path .\\dist\\ -Destination .\\chrome-extension.zip.

Теперь всё, что остается,  —  это отправиться в Chrome Web Store Developer Dashboard  —  панель управления разработчика, чтобы настроить учетную запись и опубликовать своё расширение.

Полную версию этого туториала вы найдёте в моём аккаунте Github.

Заключение

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

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

Читайте нас в Telegram, VK и Дзен


Перевод статьи Marouane Rassili: “How to Make a Chrome Extension — a Browser Plugin Development Tutorial”

Предыдущая статьяRust: работа с потоками
Следующая статьяЗлые единицы - добрый Frink