Как написать плагин для chrome

Время на прочтение
15 мин

Количество просмотров 11K

Введение

Расширения браузера — это web-приложения, которые устанавливаются в web-браузер, чтобы расширить его возможности. Обычно, для того чтобы воспользоваться расширением, пользователю нужно найти его в Chrome Web Store и установить.

В этой статье я покажу как создать расширение для браузера Google Chrome с нуля. Это расширение будет использовать API браузера, для того чтобы получить доступ к содержимому web-страницы любой открытой вкладки. С помощью этих API можно не только читать информацию с открытых web-сайтов, но и взаимодействовать с этими страницами, например, переходить по ссылкам или нажимать на кнопки. Таким образом расширения браузера могут использоваться для широкого круга задач автоматизации на стороне клиента, таких как web-scrape или даже автоматизированное тестирование frontend.

Мы создадим расширение, которое называется Image Grabber. Оно будет содержать интерфейс для подключения к web-странице и для извлечения из нее информации о всех изображениях. Далее, при нажатии на кнопку «GRAB NOW» список абсолютных URL этих изображений будет скопирован в буфер обмена. В этом процессе вы познакомитесь с фундаментальными строительными блоками, которые в дальнейшем можно будет использовать для создания других расширений.

Расширения, создаваемые таким образом для браузера Chrome совместимы с другими браузерами, основанными на движке Chromium и могут быть установлены, например, в Yandex-браузер или Opera.

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

Это только первая часть истории. Во второй части я покажу, как расширить интерфейс, чтобы выгрузить все или выбранные изображения в виде ZIP-архива, а также, как опубликовать свое расширение в Chrome Web Store.

Базовая структура расширения

Расширение Google Chrome — это web-приложение, содержащее любое количество HTML-страниц, файлов CSS и изображений, а также, файл manifest.json, который определяет как расширение выглядит и работает. Все эти файлы должны быть в одной папке.

Минимальное расширение состоит только из одного файла manifest.json. Вот пример этого файла, который вы можете использовать как шаблон при начале создания любого расширения:

{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {},
    "action": {},
    "permissions": [],
    "background":{}
}

Здесь только manifest_version должен быть равен 3. Остальные поля заполняются произвольно в зависимости от назначения расширения: name — название расширения, description — краткое описание, version — версия. Остальные параметры мы будем заполнять по ходу разработки интерфейса. Полный список параметров manifest.json можно найти в официальной документации.

Папка с одним файлом manifest.json — это минимальное расширение, которое может быть установлено в Google Chrome и запущено. Оно ничего не будет делать, однако именно с установки нужно начинать. Поэтому создайте папку image_grabber, затем добавьте в нее текстовый файл manifest.json с содержимым приведенным выше.

Установка расширения

В процессе разработки расширения, оно представляет собой папку с файлами. В терминологии Google Chrome это называется «unpacked extension». После завершения разработки, его нужно упаковать в ZIP-архив и загрузить в Chrome Web Store, откуда оно потом может быть установлено в браузер.

Однако на этапе разработки, «unpacked extension» тоже можно установить, просто как папку с файлами. Для этого нужно ввести chrome://extensions в браузере, чтобы открыть Chrome Extensions Manager:

Это покажет список уже установленных расширений. Чтобы устанавливать в него «unpacked extensions», включите флажок «Developer mode» в правом верхнем углу. После этого должна отобразиться панель управления расширениями.

Затем нажмите первую кнопку Load unpacked и укажите папку, в которой находится расширение с файлом manifest.json. В нашем случае это папка image_grabber. Расширение должно отобразиться в списке:

Эта панель должна показывать данные расширения такие как имя, описание и версию, ранее указанные в manifest.json, а также уникальный идентификатор, присвоенный этому расширению. Каждый раз после изменения manifest.json и файлов, непосредственно указанных в нем, нужно обновлять расширение в браузере, нажимая на кнопку «Reload«:

Чтобы запустить и использовать установленное расширение в браузере, нажмите кнопку Extensions на панели инструментов Google Chrome рядом с URL и найдите «Image Grabber» в списке установленных расширений:

Также можно нажать иконку Pin в списке рядом с расширением, чтобы добавить кнопку данного расширения на панель Chrome, рядом с другими расширениями:

Вот как выглядит минимальное расширение — кнопка с серым прямоугольником и буквой внутри. При нажатии на эту кнопку ничего не происходит. Давайте заполним остальные поля файла manifest.json, чтобы изменить картинку расширения и заставить его выполнять нужные действия.

Добавление иконок

Параметр icons файла manifest.json принимает объект JavaScript, ключами которого являются размеры иконок, а значениями пути к файлам с этими иконками. Иконка это картинка с расширением PNG (для прозрачности). Расширение должно иметь иконки разных размеров: 16×16, 32×32, 48×48 и 128×128. Я создал иконки для всех этих размеров и поместил их в папку «icons» внутри папки с расширением. Сделайте то же самое. Имена файлов могут быть любыми:

{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {},
    "permissions": [],
    "background":{}
}

Как видно, здесь указаны относительные пути к файлам в папке icons.

После изменения manifest.json нажмите кнопку «Reload» на панели расширения Image Grabber в Chrome Extensions Manager, чтобы обновить установленное расширение. Если все сделано как описано выше, то картинка на кнопке расширения должна измениться:

Так значительно лучше, однако когда мы нажимаем на эту кнопку все еще ничего не происходит. Пришло время добавить «действия» (actions) к расширению.

Создание интерфейса расширения

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

  • В фоне, при запуске расширения и при дальнейшей его работе

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

Расширение может использовать обе эти возможности одновременно.

Чтобы запускать действия в фоне, нужно создать Javascript-файл и указать путь к нему в параметре «background» файла manifest.json. Этот скрипт может содержать функции обработчики различных событий браузера и жизненного цикла самого расширения. Background-скрипт слушает эти события и запускает написанные для них функции обработчики.

Однако в этом расширении мы этим пользоваться не будем и параметр «background» останется пустым. Он присутствует только для того, чтобы показать что это возможно и что данный файл manifest.json может использоваться для создания расширений любого типа. Однако расширение Image Grabber выполняет действия только когда пользователь нажимает кнопку «GRAB NOW» из интерфейса.

Соответственно мы создадим интерфейс для расширения. Интерфейс расширения это фактически Web-сайт, состоящий из HTML-страниц с элементами управления, CSS-таблиц стилей и скриптов, которые реагируют на события элементов управления из этих HTML-страниц и выполняют определенные действия, используя в частности API расширений Google Chrome.

Интерфейс должен содержать главную страницу, которая появляется при нажатии на кнопку расширения. Эта страница может появляться либо в новой вкладке браузера, либо внутри всплывающего окна, как было показано на видео. Именно так и будет реализован интерфейс Image Grabber. Чтобы создать интерфейс всплывающего окна, внесите следующие изменения в manifest.json:

{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {
        "default_popup":"popup.html"
    },
    "permissions": [],
    "background":{}
}

Здесь определено, что основное действие (action) расширения это всплывающее окно (popup), которое содержит страницу popup.html.

Теперь создайте файл popup.html с заголовком Image Grabber и кнопкой «GRAB NOW» и поместите его в папку с расширением:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
    </body>
</html>

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

Чтобы увидеть, что изменилось, снова обновите расширение в Chrome Extensions Manager. Если все было сделано как написано выше, то при нажатии на иконку расширения будет появляться содержимое файла popup.html во всплывающем окне:

Работает!! Но выглядит не очень. Теперь нужно стилизовать этот интерфейс с помощью CSS.

Создайте файл popup.css со следующим содержимым и поместите его в папку с расширением:

body {
    text-align:center;
    width:200px;
}

button {
    width:100%;
    color:white;
    background:linear-gradient(#01a9e1, #5bc4bc);
    border-width:0px;
    border-radius:20px;
    padding:5px;
    font-weight: bold;
    cursor:pointer;
}

Этот файл определяет стили для кнопки и для body всей страницы: все содержимое выровнено по центру, а также, ширина содержимого будет 200 пикселей.

Добавьте ссылку на файл с этими стилями в popup.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link rel="stylesheet" type="text/css" href="popup.css"/>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
    </body>
</html>

Теперь при нажатии на кнопку расширения появится стилизованный интерфейс:

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

Чтобы придать интерфейсу завершенность, добавим JavaScript-код, который будет реагировать на нажатия кнопки «GRAB NOW«. Создайте файл popup.js в папке с расширением со следующим содержимым:

const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    alert("CLICKED");
})

и добавьте ссылку на этот файл в popup.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Image Grabber</title>
        <link rel="stylesheet" type="text/css" href="popup.css"/>
    </head>
    <body>
        <h1>Image Grabber</h1>
        <button id="grabBtn">GRAB NOW</button>
        <script src="popup.js"></script>
    </body>
</html>

Таким образом мы добавили событие onClick для кнопки с идентификатором «grabBtn«. Теперь, при нажатии на кнопку «GRAB NOW» в интерфейсе, должно появляться предупреждение «CLICKED».

В результате мы имеем такую файловую систему расширения:

Это все файлы, других больше добавлять не будем. Такую структуру можно использовать как базу для создания расширений Google Chrome с интерфейсом основанном на всплывающих окнах.

Теперь реализуем бизнес логику расширения: при нажатии на кнопку «GRAB NOW» расширение должно извлечь список путей ко всем картинкам текущей страницы в браузере и скопировать этот список в буфер обмена.

Функция «GRAB NOW»

Используя Javascript в расширении можно делать все то же самое, что и при использовании JavaScript на web-сайте, например открывать различные web-страницы из текущей или делать AJAX HTTP-запросы к различным серверам. Но в дополнении к этому, Javascript-код в расширении браузера может использовать различные API Chrome для взаимодействия с компонентами самого браузера. Большинство этих API доступно через пространство имен chrome. В частности, расширение Image Grabber будет использовать следующие API:

  • chrome.tabs — Chrome Tabs API. Будет использоваться для доступа к активной вкладке браузера.

  • chrome.scripting — Chrome Scripting API. Будет использоваться для внедрения кода JavaScript на web-страницу активной вкладки и для исполнения этого кода в контексте этой страницы.

Получение необходимых разрешений

По умолчанию, из соображений безопасности, расширение Chrome не имеет доступа ко всем API браузера. Необходимо запросить этот доступ через механизм разрешений. Для этого нужно указать список требуемых разрешений в параметре permissions в файле manifest.json. Существует множество различных разрешений, которые описаны здесь: https://developer.chrome.com/docs/extensions/mv3/declare_permissions/. Для Image Grabber нужно указать следующие из них:

  • activeTab — разрешение для получения доступа к текущей вкладке браузера.

  • scripting — разрешение для получения доступа к Chrome Scripting API для исполнения скриптов в контексте Web-страницы, открытой в браузере.

Добавьте эти разрешения в параметр permissions в файле manifest.json:

{
    "name": "Image Grabber",
    "description": "Extract all images from current web page",
    "version": "1.0",
    "manifest_version": 3,
    "icons": {
        "16":"icons/16.png",
        "32":"icons/32.png",
        "48":"icons/48.png",
        "128":"icons/128.png"
    },
    "action": {
        "default_popup":"popup.html",
    },
    "permissions": ["scripting", "activeTab"],
    "background":{}
}

и обновите расширение в Chrome Extensions Manager.

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

Получение информации об активной вкладке браузера

Для получения информации о вкладках браузера используется функция chrome.tabs.query, которая определена следующим образом:

chrome.tabs.query(queryObject,callback)
  • queryObject — объект запроса в котором указываются параметры поиска нужных вкладок.

  • callback — функция обратного вызова, которая выполняется после выполнения этого запроса. В нее передается массив tabs, содержащий все найденные вкладки, соответствующие критерию запроса. Каждый элемент этого массива — это объект типа Tab, содержащий информацию о вкладке, включая ее уникальный идентификатор. Этот идентификатор будет использоваться для исполнения Javascript-кода на этой вкладке.

Здесь я не буду полностью описывать синтаксис queryObject, а также все поля возвращаемых объектов Tabs. Эту информацию можно найти в официальной документации по chrome.tabs: https://developer.chrome.com/docs/extensions/reference/tabs/.

Для расширения Image Grabber нужно получить только активную вкладку. Для этой цели queryObject должен быть определен как: {active: true} .

Теперь давайте изменим код обработчика нажатия кнопки grabBtn в popup.js чтобы он получал активную вкладку и ее идентификатор, когда пользователь нажимает на кнопку «GRAB NOW» в интерфейсе расширения:

const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    chrome.tabs.query({active: true}, (tabs) => {
        const tab = tabs[0];
        if (tab) {
            alert(tab.id)
        } else {
            alert("There are no active tabs")
        }
    })
})

Этот код выполняет запрос ко всем вкладкам, которые активны. Может быть только одна такая вкладка, поэтому к ней можно обратиться как tabs[0]. Если такая вкладка существует, то функция отображает ее id, а если не существует, то показывает сообщение об ошибке.

Если все сделано как описано выше, то при нажатии кнопки GRAB NOW должен появляться идентификатор текущей вкладки браузера. Далее мы изменим этот код таким образом, чтобы этот идентификатор использовался для доступа к web-странице, открытой на этой вкладке.

Извлечение изображений из текущей страницы

Расширение может взаимодействовать с открытыми страницами с помощью интерфейса Chrome Scripting API, доступного через chrome.scripting. В частности, мы будем использовать функцию для внедрения своего кода в текущую web-страницу и для исполнения этого кода в ее контексте. При запуске этот код будет иметь доступ к DOM-дереву этой страницы и сможет делать нужные нам действия с любыми HTML-тэгами.

Для внедрения скрипта в страницу Web-браузера используется функция executeScript, определенная следующим образом:

chrome.scripting.executeScript(injectSpec,callback)

Рассмотрим ее параметры.

injectSpec

Это объект типа ScriptInjection. В нем определяется какой Javascript-код, в какую web-страницу и каким образом внедрить. В частности, в параметре target указывается идентификатор вкладки браузера со страницей, которая нам нужна. Мы получили его ранее. Остальные параметры указывают каким образом передать скрипт на эту страницу. Возможно использовать следующие параметры для передачи скрипта:

  • files — указывается список путей к JS-файлам, которые нужно внедрить, относительно корневой папки расширения

  • func — указывается Javascript-функция, которую нужно внедрить. Функция должна быть предварительно написана.

Скрипт, который планируется внедрить, будет извлекать список изображений с web-страницы и возвращать список URL этих изображений. Это небольшой скрипт, поэтому достаточно создать функцию для него. Она будет называться grabImages. Эта функция должна извлекать список URL изображений web-страницы и возвращать его. Возвращаемое значение этой функции будет передано расширению.

В итоге injectSpec будет определен следующим образом:

{
    target:{ tabId: tab.id, allFrames: true },
    func: grabImages,
}, 

Здесь функция, указанная в параметре func будет внедрена на страницу, определенную параметром target. В параметре target указывается полученный на предыдущем этапе идентификатор вкладки, а также, устанавливается параметр allFrames. Этот дополнительный параметр говорит что скрипт должен выполняться во всех фреймах web-страницы, в случае если они на этой странице есть.

Саму функцию grabImages мы напишем чуть позже.

callback

Это функция обратного вызова, которая будет вызвана после того, как внедренный скрипт исполнится на странице и во всех ее фреймах. В качестве параметра в ней будет массив результатов, которые вернула функция grabImages для каждого фрейма (возможно это будет всего один элемент, если фреймов нет). Каждый элемент этого массива — это объект типа InjectionResult. Он в частности содержит свойство result. Именно оно содержит то, что возвращает функция grabImages, т.е. список URL-ов.

Теперь соберем все вместе в следующем коде:

const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    chrome.tabs.query({active: true}, function(tabs) {
        var tab = tabs[0];
        if (tab) {
            chrome.scripting.executeScript(
                {
                    target:{tabId: tab.id, allFrames: true},
                    func:grabImages
                },
                onResult
            )
        } else {
            alert("There are no active tabs")
        }
    })
})

function grabImages() {
    // TODO - Запросить список изображений
    // и вернуть список их URL-ов
}

function onResult(frames) {
    // TODO - Объединить списки URL-ов, полученных из каждого фрейма в один,
    // затем объединить их в строку, разделенную символом перевода строки
    // и скопировать в буфер обмена
}

Функция grabImages может быть определена следующим образом:

/**
 * Функция исполняется на удаленной странице браузера,
 * получает список изображений и возвращает массив
 * путей к ним
 * 
 *  @return Array массив строк
 */
function grabImages() {
    const images = document.querySelectorAll("img");
    return Array.from(images).map(image=>image.src);    
}

Здесь все просто: получаем список DOM-элементов <img> и извлекаем свойство src каждого из них в итоговый массив. Считаю нужным напомнить, что объект document, указанный в этой функции указывает на содержимое не той HTML-страницы, где находится функция grabImages, а удаленной web-страницы, в которую эта функция будет внедрена.

После того как данная функция выполнится в каждом фрейме, результаты будут объединены в единый массив и переданы в функцию onResult. Эта функция может быть определена следующим образом:

/**
 * Выполняется после того как вызовы grabImages 
 * выполнены во всех фреймах удаленной web-страницы.
 * Функция объединяет результаты в строку и копирует  
 * список путей к изображениям в буфер обмена
 * 
 * @param {[]InjectionResult} frames Массив результатов
 * функции grabImages
 */
function onResult(frames) {
    // Если результатов нет
    if (!frames || !frames.length) { 
        alert("Could not retrieve images from specified page");
        return;
    }
    // Объединить списки URL из каждого фрейма в один массив
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Скопировать в буфер обмена полученный массив  
    // объединив его в строку, используя символ перевода строки 
    // как разделитель
    window.navigator.clipboard
          .writeText(imageUrls.join("n"))
          .then(()=>{
             // закрыть окно расширения после 
             // завершения
             window.close();
          });
}

Следует упомянуть что не все вкладки браузера — это вкладки с web-страницами. Например, может быть вкладка свойств браузера. Страницы на таких вкладках не имеют свойства document. В этом случае функция grabImages не выполнится и не вернет результатов. Этот случай обрабатывается в самом начале функции. Затем массив массивов результатов объединяется в единый плоский массив используя концепцию map/reduce, затем функция window.navigator.clipboard используется, чтобы скопировать его в буфер обмена. Предварительно массив результатов преобразовывается в строку, разделенную символом перевода строки.

Завершающие штрихи

Здесь я описал только незначительную часть Chrome Scripting API и описал только в контексте данного расширения. Полная документация по Chrome Scripting API доступна здесь: https://developer.chrome.com/docs/extensions/reference/scripting/.

Теперь немного почистим код. Здесь я считаю нужным часть функции обработчика grabBtn, которая выполняет chrome.scripting.executeScript вынести в отдельную функцию execScript. В результате файл popup.js выглядит так:

const grabBtn = document.getElementById("grabBtn");
grabBtn.addEventListener("click",() => {    
    // Получить активную вкладку браузера
    chrome.tabs.query({active: true}, function(tabs) {
        var tab = tabs[0];
        // и если она есть, то выполнить на ней скрипт
        if (tab) {
            execScript(tab);
        } else {
            alert("There are no active tabs")
        }
    })
})

/**
 * Выполняет функцию grabImages() на веб-странице указанной
 * вкладки и во всех ее фреймах,
 * @param tab {Tab} Объект вкладки браузера
 */
function execScript(tab) {
    // Выполнить функцию на странице указанной вкладки
    // и передать результат ее выполнения в функцию onResult
    chrome.scripting.executeScript(
        {
            target:{tabId: tab.id, allFrames: true},
            func:grabImages
        },
        onResult
    )
}

/**
 * Получает список абсолютных путей всех картинок
 * на удаленной странице
 * 
 *  @return Array Массив URL
 */
function grabImages() {
    const images = document.querySelectorAll("img");
    return Array.from(images).map(image=>image.src);    
}

/**
 * Выполняется после того как вызовы grabImages 
 * выполнены во всех фреймах удаленной web-страницы.
 * Функция объединяет результаты в строку и копирует  
 * список путей к изображениям в буфер обмена
 * 
 * @param {[]InjectionResult} frames Массив результатов
 * функции grabImages
 */
function onResult(frames) {
    // Если результатов нет
    if (!frames || !frames.length) { 
        alert("Could not retrieve images from specified page");
        return;
    }
    // Объединить списки URL из каждого фрейма в один массив
    const imageUrls = frames.map(frame=>frame.result)
                            .reduce((r1,r2)=>r1.concat(r2));
    // Скопировать в буфер обмена полученный массив  
    // объединив его в строку, используя возврат каретки 
    // как разделитель  
    window.navigator.clipboard
          .writeText(imageUrls.join("n"))
          .then(()=>{
             // закрыть окно расширения после 
             // завершения
             window.close();
          });
}

Заключение

Теперь, если нажать по иконке расширения Image Grabber и затем кликнуть кнопку GRAB NOW, то в буфере обмена будет список URL-ов всех изображений текущей web-страницы. Можно вставить его в любой текстовый редактор.

Для начала, возможно, неплохо, но практическая ценность такого расширения не велика. Поэтому в следующей части я покажу как выгрузить все эти изображения в виде zip-архива, а также, создать дополнительный интерфейс, для того чтобы выбрать какие картинки добавлять в этот ZIP-архив, а какие нет. Также опишу процесс публикации готового расширения в Chrome Store.

Поэтому подписывайтесь, думаю к концу следующей недели вторая часть будет доступна вместе с полным исходным кодом.

UPD

Вторая часть опубликована и доступна здесь.

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

Расширение или плагин для Chrome – это приложение, которое добавляет новые возможности браузеру. В магазине Chrome порядка 200 тысяч расширений для разработчиков, маркетологов, дизайнеров и других пользователей браузера.

Идея

Больше года назад я заметил, что ссылки «Входящие» и «Написать письмо» на главной странице почты, которой я часто пользуюсь, стали открываться в новом окне. По ответу эксперта стало понятно, что придется привыкать к этому спорному нововведению.

Я привыкнуть не смог: каждый раз возвращаться и закрывать вкладку браузера неудобно. Так появилась идея простого расширения для Chrome, которое вернет прежнее поведение при открытии Входящих и написании нового письма.

SimilarWeb показывает 1,141 млрд посещений главной страницы за август. Если хотя бы 10% из них приходится на десктопный браузер и целевое действие, то это 141 млн открытых вкладок в месяц. На возврат и закрытие вкладки уходит не меньше секунды. Получается, мы собираемся сэкономить 31,5 тысячи человеко-часов в месяц.

Шаг 1. Подготовка расширения

Приготовим минимальный стартовый набор.

1. Создайте папку проекта. Назовите ее именем вашего расширения.

2. Создайте javascript-файл, в котором будет реализована логика приложения. Я назвал его app.js.

3. Добавьте в созданный файл команду для тестирования: вывод строки в консоль или в диалоговое окно браузера.

alert(‘Hello from my extension!’);

4. Создайте manifest.json – служебный файл, который должен быть в каждом расширении.

{
«manifest_version»: 2,
«name»: «Mail link healer»,
«version»: «1.0»,
«description»: «Ссылка «Входящие» и «Написать письмо» снова открываются в той же вкладке!»,
«icons»: {
«128»: «128.png»
},
«content_scripts»: [
{
«matches»: [ «<all_urls>» ],
«js»: [ «app.js» ]
}
]
}

Обязательные параметры, должны быть в каждом manifest.json:

  • manifest_version – целое число, обозначает версию формата манифеста. Для современных версий браузера значение равно 2;

  • name – название расширения, до 45 символов. К названию стоит подойти ответственно – оно отображается в магазине, при установке и на странице управления расширениями (chrome://extensions);

    version – строка с версией расширения. Содержит до четырех чисел, разделенных точками, например 2.19.7.0.

Необязательные параметры:

  • description – простой текст, до 132 символов. Отображается в магазине и на странице управления расширениями;

  • icons – список из иконок расширения;

  • content_scripts – скрипты, которые загружает приложение. Мы прописали запуск app.js на всех веб-страницах. Позже мы изменим этот параметр, чтобы запускать скрипт только на целевой странице. Строго говоря, в нашем случае content_scripts – обязательный параметр. Если его не задать, расширение не сделает ничего полезного.

5. Подготовьте иконку в формате png размером 128х128 пикселей и положите ее в папку проекта. Эта версия отображается при установке и в магазине расширений. Рекомендуют также добавить иконку размером 48х48 пикселей – она отображается на странице управления расширениями. И можно добавить версию 16х16 пикселей, которая будет использоваться как фавикон.

К этому моменту в нашем проекте есть иконка расширения, manifest.json со служебной информацией и app.js, в котором позже появится код приложения.

.
├── 128.png
├── app.js
├── manifest.json

Примечание: расширение может состоять из нескольких JS-файлов, HTML-верстки и CSS-стилей.

Теперь можно проверить, что мы выполнили минимальные требования к расширению.

Шаг 2. Промежуточное тестирование

1. Войдите в свой аккаунт в браузере Chrome.

2. Перейдете на страницу chrome://extensions.

3. В правом верхнем углу включите Режим разработчика.

4. Нажмите Загрузить распакованное расширение и выберите папку проекта. В результате вы увидите похожую картинку:

Расширение после загрузки в браузер.

5. В новой вкладке браузера откройте любую веб-страницу. Отлично, работает!

alert из app.js сработал. Можно двигаться дальше.

Теперь мы готовы добавить полезное действие.

Шаг 3. Код приложения

Проверим код ссылок «Входящие» и «Написать письмо» на странице почты с помощью инструментов разработчика браузера. Нажимаем правую кнопку на ссылке и выбираем Посмотреть код. Видно, что ссылки открываются в новой вкладке из-за атрибута target=”_blank”.

Кусочек кода в этой рамке доставляет столько неудобств. Избавимся от него!

Получается, с помощью javascript нам нужно выбрать две ссылки и удалить этот атрибут.

1. В app.js сохраните ссылки в переменные:

const inboxLink = document.getElementById(‘mailbox:unreadEmails:link’); // ссылка на Входящие
const composeLink = document.getElementById(‘mailbox:write_letter’); // ссылка «Написать письмо»

Здесь мы выбираем элементы по их идентификатору (id).

2. Если такие ссылки найдены, удалите у них атрибут target.

if (inboxLink && composeLink) {
// если ссылки есть на странице
// удаляем у них атрибут ‘target’
inboxLink.removeAttribute(‘target’);
composeLink.removeAttribute(‘target’);
}

Похоже, этого кода достаточно для решения проблемы.

Заодно давайте пропишем в manifest.json правило, по которому расширение будет запускать наш скрипт только на целевой странице. Для этого в параметре content_scripts в ключе matches пропишем конкретный URL:

«content_scripts»: [
{
«matches»: [ «https://mail.ru/*» ],
«js»: [ «app.js» ]
}
]

Звездочку в конце URL я поставил, чтобы скрипт срабатывал на всех страницах, начинающихся с https://mail.ru/.

Кстати, после обновления расширения стоит обновить версию и в manifest.json:

«version»: «1.1»,

После внесения правок важно обновить расширение в браузере. Для этого нажмите на стрелку на странице управления расширениями (chrome://extensions/).

Быстрый способ обновить расширение.

Готово! Проверим расширение в боевых условиях.

Шаг 4. Финальное тестирование

Открываем целевую страницу и переходим по ссылкам «Входящие» и «Написать письмо». Победа! Ссылки открываются в той же вкладке, как в старые добрые времена.

Шаг 5. Публикация расширения

Этот шаг необязательный. Во-первых, возможно, вы не хотите делать доступным для всех свое расширение. А во-вторых, возможно, не захотите платить за это $5. Такой регистрационный сбор должен оплатить разработчик Chrome Web Store.

  1. Создайте zip-архив с файлами проекта.
  2. Зарегистрируйтесь как разработчик Chrome Web Store. Для этого войдите в консоль разработчика под своим аккаунтом Chrome, примите условия соглашения и Политику конфиденциальности.
  3. Оплатите регистрационный сбор $5. Оплатить можно картой, как в обычных интернет-магазинах.
  4. На дашборде разработчика в разделе Аккаунт добавьте адрес электронной почты. Без этого не получится отправить расширение на проверку.
  5. В личном кабинет разработчика нажмите Добавить продукт и загрузите zip-архив проекта.
  6. В разделе Описание продукта заполните описание расширения, выберите категорию и язык. Добавьте хотя бы один скриншот расширения.
  7. В разделе Конфиденциальность коротко опишите единственное назначение расширения, обоснование для использования разрешения и отметьте, что не используете разрешение «удаленный код».
  8. В разделе Цель и распространение задайте, кому будет доступно расширение.
  9. Нажмите Отправить на проверку в правом верхнем углу.

Это всё. Остается ждать результат проверки, которая может занять несколько недель.

Я отправил расширение на проверку параллельно с публикацией статьи. Как только получу ответ, добавлю комментарий со ссылкой на опубликованное расширение.

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

В конце 2020 года мы делали проект со снежинками — писали специальный скрипт, который запускал падающий снег на сайтах. Если бы мы хотели сделать такой снег на любом своём сайте, это не составило бы труда: добавляешь скрипт в код страницы, и готово.

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

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

👉 Что такое расширение

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

Примеры того, что может сделать расширение: 

  • Сохранить ваши пароли и другие личные данные,
  • Выдрать из страницы скрытые данные (например, ссылку на скачивание музыки).
  • Менять что-то на странице, например, отключать рекламу, переводить текст, удалять ненужное.
  • Собирать статистику вашей работы в интернете.
  • Подключаться к другим сервисам (почте, чатам, файлообменникам) и взаимодействовать с ними из браузера.

В этой статье

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

Манифест

В каждом расширении для браузера должен быть манифест — документ, в котором написано:

  • как называется расширение;
  • к чему расширение может получить доступ;
  • какие скрипты будут работать в фоне;
  • как должна выглядеть иконка расширения;
  • что показать или что выполнить, когда пользователь нажмёт на иконку расширения.

Манифест задаёт общие правила для всего расширения, поэтому манифест — единственный обязательный компонент. Можно обойтись без иконок и скриптов, но манифест обязательно должен быть.Каждый манифест хранится в файле manifest.json — создадим пустой файл с таким именем и напишем внутри такое:

{
«name»: «Запускаем снежинки на любом сайте»,
«description»: «Проект журнала Код»,
«version»: «1.0»,
«manifest_version»: 3
}

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

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

chrome://extensions/

Мы попадаем на страницу, которая нам покажет все установленные расширения:

Делаем своё расширение для браузера за 10 минут

Чтобы добавить своё расширение, в правом верхнем углу включаем режим разработчика, а затем нажимаем «Загрузить распакованное расширение»:

Делаем своё расширение для браузера за 10 минут

Теперь выбираем папку, в которой лежит наш манифест:

Делаем своё расширение для браузера за 10 минут

Отлично, мы только что добавили в браузер новое расширение:

Делаем своё расширение для браузера за 10 минут

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

Чтобы было проще работать и тестировать расширение, закрепим его на панели браузера:

Делаем своё расширение для браузера за 10 минут

Иконки

У расширения есть две иконки, которыми мы можем управлять: 

  1. Картинка в карточке расширения на странице настроек.
  2. Иконка на панели браузера.

Чтобы не рисовать всё с нуля, скачаем папку с иконками из того же руководства Google и положим её в ту же папку, что и манифест:

Теперь добавим иконки в манифест. За картинку в карточке отвечает блок icon, а за иконку на панели —  блок action. Разные размеры картинки нужны для того, чтобы на разных мониторах с любой плотностью пикселей иконки выглядели хорошо:

{
  "name": "Запускаем снежинки на любом сайте",
  "description": "Проект журнала Код",
  "version": "1.0",
  "manifest_version": 3,

  "action": {
    "default_icon": {
      "16": "/images/get_started16.png",
      "32": "/images/get_started32.png",
      "48": "/images/get_started48.png",
      "128": "/images/get_started128.png"
    }
  },
  "icons": {
    "16": "/images/get_started16.png",
    "32": "/images/get_started32.png",
    "48": "/images/get_started48.png",
    "128": "/images/get_started128.png"
  }
}

Сохраняем манифест, обновляем расширение на странице настроек и смотрим результат:

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

Настраиваем разрешения

Разрешения — это то, что браузер позволяет делать расширению со страницами и с их содержимым. Для запуска снежинок нам нужно сделать две вещи:

  1. Понять, какая вкладка сейчас активная, чтобы запустить снежинки именно на ней.
  2. Запустить наш скрипт со снежинками.

Чтобы получить доступ к активной вкладке и к запуску скриптов, добавим в манифест такую строку:

"permissions": ["activeTab", "scripting"],

Показываем меню

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

Чтобы сделать всплывающее меню, добавим в манифест в раздел action такую строку:

    "default_popup": "popup.html",

Она означает, что при нажатии на иконку мы увидим рядом с ней мини-страничку, на которой что-то будет.Создадим в той же папке расширения файл popup.html и добавим в него такой код:

<!DOCTYPE html>
<html lang="ru">
  <head>
  	<meta charset="UTF-8">
    <style type="text/css">

    	/* задаём размеры кнопки и размер текста на кнопке  */
    	button {
	      font-size: 12px;
		  height: 40px;
		  width: 80px;
		}
    </style>
  </head>
  <body>
  	<!-- создаём кнопку на странице -->
    <button id="snow">Запустить снежинки</button>
    <!-- подключаем скрипт, который обработает нажатие на эту кнопку -->
    <script src="popup.js"></script>
  </body>
</html>

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

Показываем меню расширения

Сохраняем манифест, обновляем его на странице настроек и видим, что у нашего расширения появилось меню с кнопкой:

Показываем меню расширения

Запускаем снежинки

Вся магия будет происходить в файле popup.js — откроем его и добавим такой код:

// получаем доступ к кнопке
let snow = document.getElementById("snow");
// когда кнопка нажата — находим активную вкладку и запускаем нужную функцию
snow.addEventListener("click", async () => {
  // получаем доступ к активной вкладке
  let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  // выполняем скрипт
  chrome.scripting.executeScript({
  	// скрипт будет выполняться во вкладке, которую нашли на предыдущем этапе
    target: { tabId: tab.id },
    // вызываем функцию, в которой лежит запуск снежинок
    function: snowFall,
  });
});

// запускаем снег
function snowFall() {
}

Последнее, что нам осталось сделать, — положить в функцию snowFall() полный код скрипта из проекта со снежинками и сохранить файл.

Проверка

В прошлый раз мы не смогли запустить скрипт на любой странице Яндекса — мешала политика безопасности. Теперь всё работает:

Проверяем расширение

Скачать упакованное расширение. Перед установкой его нужно распаковать в любую папку.

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

В этой статье мы рассмотрим создание самого простого расширения — запускатора избранных сайтов. Хотя приложение и будет примитивным, оно всё-таки раскроет процесс создания и загрузки расширения для google Chrome.

Желательно знать HTML, CSS и JS (если придётся расширить набор функций) на самом базовом уровне, чтобы понимать материал лучше, но в любом случае мы будем объяснять код.

В каждом расширении для Chrome должен быть файл manifest.json. Он служит только для описания функций приложения, общего описания, номера версии и разрешений. Более подробно вы сможете ознакомиться с этим файлом в блоге команды разработчиков Chrome.

Давайте же внесём свой вклад в развитие web

Здесь всё очень просто:

{
	"manifest_version": 2,
	"name": "Tproger Launcher",
	"description": "Запускатор представительств Tproger",
	"version": "1.0.0",
	"icons": {"128": "icon_128.png"},
	"browser_action": {
		"default_icon": "icon.png",
		"default_popup": "popup.html"
	},
	"permissions": ["activeTab"]
}

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

Для начала давайте напишем базовый HTML-код:

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>

</body>
</html>

Выше мы написали каркас для плагина, сейчас он полностью пуст и нужно указать название, ссылки на иконки и шрифт. Это можно сделать с помощью тега link, обратите внимание, он не закрывается:

<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8">	
   <title>Tproger Media Quick Launcher</title>
   <!--ссылаемся на шрифты, используемые в документе-->
   <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
   <!--здесь мы ссылаемся на стили, которые будем использовать в документе, а именно стиль иконок-->
   <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
    crossorigin="anonymous">
</head>
<body>

</body>
</html>

Не забывайте указывать кодировку, иначе не отобразятся кириллические буквы.

Перейдём ко второму блоку кода, а именно к тегу body и его содержимому.

Так как наше расширение — модальное окно, давайте соответствующим образом назовём контейнеры. Сначала добавим контейнер шапки расширения, в которой укажем ссылку к иконке, напишем название и добавим номер версии.

<!--объяснение тега-->
<body>
  <!--контейнер, содержащий название, логотип и номер версии-->
  <div class="modal-header">
    <h1 class="logo">
      <img class="logo-icon" src="images/tproger-logo.png"> Запускатор Tproger
      <span class="version">(1.0.0)</span>
   </h1>
  </div>
  <!--Конец контейнера-->

Переходим к следующему контейнеру. Он содержит описание функций расширений.

<!--контейнер, содержащий описание функций расширения-->
  <div class="modal-content">
    <p>Быстрый доступ к контентным площадкам Типичного Программиста</p>
  </div>
  <!--Конец контейнера-->

Далее следует контейнер modal-icons, внутри которого ещё 5 контейнеров.

<!--контейнер, содержащий контейнеры с иконками-->
  <div class="modal-icons">
    <div class="flex-container">
      <div class="flex">
        <!--target="_blank" — это служит для открытия новой вкладки при клике по иконке-->
        <a href="#" target="_blank">
          <i class="fa fa-globe"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-telegram"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-facebook"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-vk"></i>
        </a>
      </div>
    </div>
  </div>

Для каждой иконки мы выделили отдельный контейнер с классом flex, чтобы знать, к каким элементам будем применять Flexbox.

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

Стили

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

<style>
    /* Модальная структура документа */
    /*общие настройки для всего документа*/
    html,
    body {
      font-family: 'Open Sans', sans-serif;
      font-size: 14px;
      margin: 0;
      min-height: 180px;
      padding: 0;
      width: 380px;
    }
    /*задаём настройки для заголовков первого уровня*/
    h1 {
      font-family: 'Menlo', monospace;
      font-size: 22px;
      font-weight: 400;
      margin: 0;
      color: #2f5876;
    }
    a:link,
    a:visited {
      color: #000000;
      outline: 0;
      text-decoration: none;
    }
    /*задаём ширину картинки*/
    img {
      width: 30px; /*ширина изображений*/
    }
    .modal-header {
      align-items: center; /*выравнивание элементов по центру*/
      border-bottom: 0.5px solid #dadada; /*свойства нижней разделительной линии*/
      height: 50px;
    }
    .modal-content {
      padding: 0 22px; /*отступы сверху и снизу, сверху и слева*/
    }
    .modal-icons {
      border-top: 0.5px solid #dadada; /*свойства верхней разделительной линии*/
      height: 50px;
      width: 100%;
    }
    .logo {
      padding: 16px; /*отступы со всех сторон*/
    }
    .logo-icon {
      vertical-align: text-bottom; /*выравнивание по нижней части текста*/
      margin-right: 12px; /*задётся отступ элементов от изображения*/
    }
    .version {
      color: #444;
      font-size: 18px;
    }

Основные настройки документа заданы, давайте перейдём к следующему фрагменту кода, в котором как раз и будет применён Flexbox, о котором шла речь в начале статьи.

.flex-container {
      display: flex; /*отображает контейнер в виде блочного элемента*/
      justify-content: space-between; /*равномерное выравнивание элементов*/
      padding: 10px 22px;
    }
    /*задаём настройки для контейнеров с иконками*/
    .flex {
      opacity: 1; /*параметр непрозрачности иконок*/
      width: 120px;
    }
    .flex:hover {
      opacity: 0.4; /*уровень непрозрачности при наведении курсора на элемент*/
    }
    .flex .fa {
      font-size: 40px;
      color: #2f5876;
    }
  </style>
  <!--конец объяснения блока со стилями-->

Мы постарались как можно подробнее объяснить в комментариях относительно сложные моменты. А сейчас нам нужно лишь загрузить наше расширение в браузер Chrome и оно будет работать, а если пройдёт модерацию, то появится в магазине расширений (плагинов).

Теперь давайте добавим файл с расширением .js, если вдруг потребуется расширить функции дополнения для браузера.

<head>
   <meta charset="utf-8"> 
   <title>Tproger Media Quick Launcher</title>
  <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
  <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
    crossorigin="anonymous">
    <style type="text/css">
      /*здесь мы прописали стили*/
    </style>
  <!--конец объяснения блока со стилями-->
  <!--здесь ссылаемся на файл .js в нашей папке с кодом и изображениями-->
  <script src="popup.js"></script>
</head>

Проверка кода и публикация

Прежде чем опубликовать, проверьте ещё раз весь код. Если вы делали всё так, как мы, то у должно было получиться следующее:

<!DOCTYPE html>
<html>
<!--Начало первого блока объяснения кода-->
<head>
  <meta charset="utf-8">
  <title>Запускатор Tproger</title>
  <!--ссылаемся на шрифты, используемые в документе-->
  <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" type="text/css">
  <!--здесь мы ссылаемся на стили, которые будем использовать в документе, а именно стиль иконок-->
  <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
    crossorigin="anonymous">
    <!--начало блока со стилями для страницы-->
  <style>
    /* Модальная структура документа */
    /*общие настройки для всего документа*/
    html,
    body {
      font-family: 'Open Sans', sans-serif;
      font-size: 14px;
      margin: 0;
      min-height: 180px;
      padding: 0;
      width: 380px;
    }
    /*задаём настройки для заголовков первого уровня*/
    h1 {
      font-family: 'Menlo', monospace;
      font-size: 22px;
      font-weight: 400;
      margin: 0;
      color: #2f5876;
    }
    a:link,
    a:visited {
      color: #000000;
      outline: 0;
      text-decoration: none;
    }
    /*задаём ширину картинки*/
    img {
      width: 30px; /*ширина изображений*/
    }
    .modal-header {
      align-items: center; /*выравнивание элементов по центру*/
      border-bottom: 0.5px solid #dadada; /*свойства нижней разделительной линии*/
      height: 50px;
    }
    .modal-content {
      padding: 0 22px; /*отступы сверху и снизу, сверху и слева*/
    }
    .modal-icons {
      border-top: 0.5px solid #dadada; /*свойства верхней разделительной линии*/
      height: 50px;
      width: 100%;
    }
    .logo {
      padding: 16px; /*отступы со всех сторон*/
    }
    .logo-icon {
      vertical-align: text-bottom; /*выравнивание по нижней части текста*/
      margin-right: 12px; /*задётся отступ элементов от изображения*/
    }
    .version {
      color: #444;
      font-size: 18px;
    }
    .flex-container {
      display: flex; /*отображает контейнер в виде блочного элемента*/
      justify-content: space-between; /*равномерное выравнивание элементов*/
      padding: 10px 22px;
    }
    /*задаём настройки для контейнеров с иконками*/
    .flex {
      opacity: 1; /*параметр непрозрачности иконок*/
      width: 120px;
    }
    .flex:hover {
      opacity: 0.4; /*уровень непрозрачности при наведении курсора на элемент*/
    }
    .flex .fa {
      font-size: 40px;
      color: #2f5876;
    }
  </style>
  <!--конец объяснения блока со стилями-->
  <script src="popup.js"></script>
</head>
<!--объяснение тега <body>-->
<body>
  <!--контейнер, содержащий название, логотип и номер версии-->
  <div class="modal-header">
    <h1 class="logo">
      <img class="logo-icon" src="images/tproger-logo.ico">Запускатор Tproger
      <span class="version">(1.0.0)</span>
    </h1>
  </div>
  <!--Конец контейнера-->
  <!--контейнер, содержащий описание функций расширения-->
  <div class="modal-content">
    <p>Быстрый доступ к контентным площадкам Типичного Программиста</p>
  </div>
  <!--Конец контейнера-->
  <!--контейнер, содержащий контейнеры с иконками-->
  <div class="modal-icons">
    <div class="flex-container">
      <div class="flex">
        <!--target="_blank" — это служит для открытия новой влкадки при клике по иконке-->
        <a href="#" target="_blank">
          <i class="fa fa-globe"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-telegram"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-vk"></i>
        </a>
      </div>
      <div class="flex">
        <a href="#" target="_blank">
          <i class="fa fa-facebook"></i>
        </a>
      </div>
    </div>
  </div>
</body>

</html>

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

И далее следуем инструкциям на скриншотах ниже.

Для загрузки расширения в магазин нам нужно зайти в меню, навести мышку на «дополнительные настройки», а затем выбрать «расширения» или ввести в адресной строке chrome://extensions/.

Далее нажимаем на «загрузить распакованное расширение» и выбираем папку с файлами.

После того, как вы выбрали папку с файлами, она загрузится в браузер и превратится в расширение, которое будет доступно за пределами правой части адресной строки.

Надеемся, что всё работает правильно и вы понимаете структуру расширений для Chrome.

Основано на видео с канала «Traversy Media»

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

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

Подготовка

  • Если в файле манифеста указан целевой сайт, на который ссылается приложение или расширение, подтвердите право собственности на этот сайт в Google Search Console.
  • Если приложение или расширение размещено на частном сервере, можно указать, кто имеет право публиковать его в интернет-магазине Chrome. Вы можете отключить подтверждение права собственности на сторонние сайты, на которые ссылается приложение. Подробнее…

Шаг 1. Создайте приложение или расширение

Ниже в качестве примера используется приложение-закладка. Инструкции по созданию более сложных приложений и расширений Chrome приведены в руководстве по началу работы.

  1. Создайте на компьютере папку, в которой будут храниться файлы приложения или расширения. Присвойте ей имя приложения.
  2. Создайте файл манифеста.
    1. Создайте в текстовом редакторе файл JavaScript Object Notation (JSON). Посмотрите пример файла JSON для приложения-закладки.
    2. Проверьте, правильно ли отформатирован код в файле JSON, с помощью специального инструмента, например JSONLint.
  3. Поместите файл manifest.json в папку приложения или расширения.
  4. Создайте логотип.
    1. Изображение должно иметь размер 128 х 128 пикселей.
    2. Сохраните файл логотипа под названием 128.png в папке приложения.

Шаг 2. Протестируйте приложение или расширение

Разработчики могут тестировать свои приложения и расширения в браузере Chrome или на устройствах ChromeOS.

  1. Выберите тип тестового устройства.
    • Приложения: войдите в свой аккаунт Google на устройстве Chrome.
    • Расширения: войдите в свой аккаунт Google на устройстве Chrome или в браузере Chrome на компьютере Windows, Mac или Linux.
  2. Сохраните папку с файлами приложения или расширения на тестовом устройстве.
  3. Откройте страницу chrome://extensions.
  4. В правом верхнем углу включите режим разработчика.
  5. Нажмите Загрузить распакованное расширение.
  6. Найдите и выберите папку приложения или расширения.
  7. Откройте в Chrome новую вкладку затем нажмите Приложения затем выберите приложение или расширение и проверьте, корректно ли оно загружается и работает.
  8. При необходимости внесите правки в файл manifest.json, снова разместите папку приложения на сервере и выполните тестирование ещё раз. Эти действия следует повторять до тех пор, пока продукт не будет работать без ошибок.

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

  1. В браузере Chrome нажмите ЕщёзатемДополнительные инструментызатемИнструменты разработчика.
  2. Проверьте правильность сведений, таких как идентификатор приложения и номер версии.

Шаг 3 (необязательно). Создайте коллекцию приложений

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

Шаг 4. Опубликуйте приложение или расширение в интернет-магазине Chrome

Разработчик может сделать приложение или расширение общедоступным либо ограничить доступ к нему. При публикации в интернет-магазине Chrome можно выбрать один из четырех вариантов.

  • Общедоступное: кто угодно может найти и установить приложение.
  • Доступ по ссылке: установить приложение или расширение можно только по ссылке. Оно не включается в результаты поиска в интернет-магазине Chrome. Ссылку можно отправить пользователям как в домене организации, так и за его пределами.
  • Частное: найти и установить приложение или расширение могут только пользователи в вашем домене. Кроме этого, можно предоставить доступ к продукту только доверенным тестировщикам, чьи имена указаны в панели инструментов разработчика.
  • Публикация в группе: доступно только при оплате единовременного регистрационного взноса разработчика или при условии, что хотя бы одно расширение добавлено в ваш список и доступ к нему закрыт. Подробнее о том, как настроить публикацию в группе…

Чтобы добавить приложение или расширение в интернет-магазин Chrome, создайте ZIP-архив соответствующей папки, а затем опубликуйте продукт.

Шаг 5. Настройте правила работы с приложением или расширением

В консоли администратора Google можно управлять использованием приложений и расширений на устройствах Chrome и в браузере Chrome на компьютерах Windows, Mac и Linux в вашей организации. Подробнее о том, как настроить правила Chrome для отдельного приложения и для нескольких приложений…

Администратор Microsoft Windows может контролировать работу приложений и расширений в браузере Chrome на управляемых устройствах Windows с помощью настроек групповой политики. Подробнее о том, как установить и настроить шаблоны правил Chrome на управляемых компьютерах…

Статьи по теме

  • Как разрешить или заблокировать приложения и расширения
  • Как настроить правила Chrome для пользователей или браузеров
  • Как настроить автоматическую установку приложений и расширений
  • Публикация и распространение через интернет-магазин Chrome: начало работы
  • Как создать свое первое приложение

Google, а также другие связанные знаки и логотипы являются товарными знаками компании Google LLC. Все другие названия компаний и продуктов являются товарными знаками соответствующих компаний.

Эта информация оказалась полезной?

Как можно улучшить эту статью?

Welcome

Welcome to the Getting Started Guides.

Published on Tuesday, October 4, 2022

Welcome to the Getting Started Guides! The purpose of this page is to describe each article and how to share your feedback.

Extension development concepts

Extensions 101
Briefly covers some fundamental concepts of Chrome Extension development like web technologies and commonly used extension components. In addition, it includes what to be aware of when designing and distributing an extension in the Chrome Web Store.
Development Basics
Introduces the extension development workflow by creating a «Hello, Extensions» example. It walks through loading the extension during development, locating logs and errors, choosing a project structure, and using Typescript.

Extension tutorials

Reading time
It’s nice to know how much time we need to finish reading an article. Reading time teaches you how to insert an element containing the estimated reading time on every extension documentation page.
Focus mode
Removing extra clutter from a page allows our minds to concentrate on the most relevant information. Focus mode demonstrates how to change the style of extension documentation pages and hides a few distracting elements.
Tabs manager
While researching extension development, you can end up with many documentation tabs across multiple windows. Tabs Manager organizes your Chrome extension and Chrome Web store documentation tabs.

These tutorials not only teach you how to build real-world extensions but also strive to share development tips and best practices. In addition, using these extensions will improve your experience while reading the extension documentation.

Each tutorial includes the following sections:

  • The task the extension will perform.
  • The lessons that will be covered.
  • What you need to know before starting.
  • Step-by-step instructions on how to build the extension.
  • How to load and test the extension.

If you are up for a challenge, we included a section with a few ideas on how to customize or add other features to your extension.

We are excited to hear from you! Here are two ways you can contribute:

Improve the existing tutorials
If you see something wrong or unclear, report a bug on our GitHub repository.
Request a beginner tutorial
If you have an idea for another beginner tutorial, create an issue on our GitHub repository, choose «Extension tutorial request», and fill out the form.

We hope the new Getting Started guides help you feel confident and supported as you embark on your extension development journey.

Updated on Tuesday, October 4, 2022 Improve article

Спорим, вы прямо сейчас используете какие-то браузерные расширения. Некоторые из них очень полезны и популярны, например, блокировщики рекламы, менеджеры паролей или средства для просмотра PDF. Функциональность таких расширений (аддонов) не ограничена — вы можете сделать с их помощью что угодно, но сегодня мы будем разбираться, как делать непосредственно их. Другими словами, мы собираемся создать собственное расширение.

Что будем делать?

На сайте Reddit есть аккаунты, которые на добровольной основе занимаются транскрибацией (переводом в текст) изображений, аудио- и видео-контента. Это здорово улучшает доступность для людей с ограниченными возможностями.

Однако эти комментарии находятся в общей ленте и сразу не видны. Мы собираемся решить эту проблему.

Мы создадим расширение Transcribers of Reddit (TOR), которое будет искать и перемещать комментарии с расшифровкой на верх общего списка, а также добавлять aria-атрибуты для скрин-ридеров. Мы даже пойдем чуть дальше и добавим возможность изменить цвет фона и установить рамку для этих комментариев, чтобы улучшить визуальный контраст.

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

Мы начнем с расширения для Chromium-браузеров (Google Chrome, Microsoft Edge, Brave и т. д.) В следующих статьях, возможно, мы его портируем на Firefox и Safari, который с недавних пор тоже поддерживает веб-расширения и в MacOS и в iOS.

Репозиторий проекта

Рабочая директория

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

Создадим новую папку для проекта — можно назвать ее transcribers-of-reddit. А внутри нее папку src для исходного кода.

Манифест

Точка входа — это файл, который содержит всю важную информацию о расширении (название, описание и т. д.), а также определяет необходимые разрешения и выполняемые скрипты.

Нашей точкой входа будет файл manifest.json в папке src. Добавим в него для начала три свойства:

{
  "manifest_version": 3,
  "name": "Transcribers of Reddit",
  "version": "1.0"
}
  • Поле manifest_version — это версия используемой спецификации, она определяет, какие API доступны приложению. На данный момент самая свежая версия — mv3.
  • Поле name — название расширения, которое будет отображаться, например, в Chrome Web Store.
  • Поле version — это версия самого расширения. Она указывается в виде строки, которая должна содержать только цифры и точки. Можно указывать мажорные, минорные версии, а также патчи (например, 1.3.5).

Больше информации

В файл manifest.json можно добавить очень много различных параметров, описывающих наше приложение. Например, его описание (поле description), которое объясняет, что приложение умеет делать.

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

{
  "description": "Reddit made accessible for disabled users.",
  "icons": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  },
  "homepage_url": "https://lars.koelker.dev/extensions/tor/"
}
  • Поле description не должно быть длиннее 132 символов.
  • Иконки, указанные в поле icons, используются во множестве мест. Предпочтительно загружать файлы png-формата. Иконки для нашего проекта вы можете загрузить прямо из его репозитория.
  • В поле homepage_url можно указать любую страницу, которая содержит информацию о приложении. Она будет отображаться в графе Open extention website на странице управления расширением.

Скриншот страницы управления расширением

Разрешения

Большой плюс расширений — это возможность взаимодействовать напрямую с браузером. Но для этого нужно явно указать, какие API мы собираемся использовать, чтобы получить разрешения. Это нужно сделать в секции permissions файла manifest.json:


{
  "manifest_version": 3,
  "name": "Transcribers of Reddit",
  "version": "1.0",
  "description": "Reddit made accessible for disabled users.",
  "icons": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  },
  "homepage_url": "https://lars.koelker.dev/extensions/tor/",

  "permissions": [
    "storage",
    "webNavigation"
  ]
}

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

Кроме того, мы должны отслеживать пользовательскую навигацию по текущей странице — разрешение webNavigation. Дело в том, что Reddit — это одностраничное приложение (SPA), которое не вызывает событий перезагрузки страниц. Нам нужно поймать взаимодействие и загрузку комментариев на страницу.

В зависимости от указанных разрешений браузер может выводить сообщение пользователю для их подтверждения. Вот список таких разрешений.

Управление переводами

Браузерным расширениями доступен встроенный API интернационализации (i18n). Он позволяет управлять переводами для множества языков (полный список). Чтобы воспользоваться этой возможностью, нужно создать сами переводы, а также указать дополнительную информацию в manifest.json.

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

"default_locale": "en"

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

Сами переводы определяются в папке _locales. Для каждого создается отдельная папка, внутри которой находится файл messages.json:

src 
 └─ _locales
     └─ en
        └─ messages.json
     └─ fr
        └─ messages.json

Перевод каждой фразы оформляется по специальной схеме:

  1. Определяется ключ перевода (translation key), или идентификатор фразы, по которому мы будем к ней обращаться.
  2. Для каждого ключа указываются свойства: message, description и placeholders.
  • message — это сама переводимая фраза.
  • description — опциональное описание перевода, если необходимо. Предназначено, скорее, для самого переводчика, в расширении не используется.
  • placeholders — динамический контент внутри фразы, например, слова, которые могут изменяться.

Вот пример перевода одной фразы:

{
  "userGreeting": { // Translation key ("id")
    "message": "Good $daytime$, $user$!" // сама фраза
    "description": "User Greeting", // опциональное описание
    "placeholders": { // динамический контент внутри фразы
      "daytime": { // имя плейсхолдера, используется в самой фразе
        "content": "$1",
        "example": "morning" // необязательный пример значения
      },
      "user": { 
        "content": "$1",
        "example": "Lars"
      }
    }
  }
}

Схема использования плейсхолдеров может показаться довольно сложной.

Для начала мы должны определить, какие части сообщения (фразы) являются динамическими. Их может и не быть. Если такие фрагменты нашлись, нужно обернуть их символами $.

Каждый такой фрагмент нужно отдельно описать в поле placeholders. Это немного неинтуитивно, но Chrome хочет знать, какое значение следует вставить вместо плейсхолдера. Так как мы хотим использовать динамические (неопределенные) значения, нужно использовать специальную конструкцию $1, которая является ссылкой на вставленный контент.

Свойство example необязательно, оно может использоваться как подсказка для переводчиков, но в самом расширении не применяется.

Давайте добавим переводы в наш проект. Это оригинальный английский файл, а вы можете добавить столько языков, сколько захотите. Например, для русского нужно создать папку ru, а для немецкого de.

{
  "name": {
    "message": "Transcribers of Reddit"
  },
  "description": {
    "message": "Accessible image descriptions for subreddits."
  },
  "popupManageSettings": {
    "message": "Manage settings"
  },
  "optionsPageTitle": {
    "message": "Settings"
  },
  "sectionGeneral": {
    "message": "General settings"
  },
  "settingBorder": {
    "message": "Show comment border"
  },
  "settingBackground": {
    "message": "Show comment background"
  }
}

Возможно, вы хотите знать, почему мы не указали API интернационализации в списке разрешений.

Chrome довольно непоследователен в этом отношении — вам не нужно указывать все необходимые разрешения, например, chrome.i18n. Некоторые другие разрешения требуется указывать, но пользователь, который устанавливает ваше расширение о них ничего не знает. Некоторые разрешения вообще являются «гибридными», вы можете использовать некоторые их функции без объявления, но другие обязательно нужно указать в манифесте. Чтобы разобраться во всем этом, загляните лучше в документацию.

Интернационализация манифеста

Первое, что видит пользователь в Chrome Web Store, — это обзорная страница расширения. Мы должны убедиться, что на ней все переведено на актуальный язык, для этого придется внести несколько изменений в манифест — перевести название и описание:

{
  // Обновленные значения
  "name": "__MSG_name__",
  "description": "__MSG_description__"
}

Это специальный синтаксис, который позволяет ссылаться на фразы в messages.json по их идентификатору. Например, строка __MSG_name__ использует фразу с идентификатором name.

Интернационализация HTML-страниц

Чтобы перевести тексты на страницах самого расширения, потребуется добавить немного JavaScript:

chrome.i18n.getMessage('name');

Этот код также использует идентификатор фразы из файла messages.json.

Чтобы передать динамические плейсхолдеры, используйте второй параметр метода:

chrome.i18n.getMessage('userGreeting', {
  daytime: 'morning',
  user: 'Lars'
});

Конечно, перевести таким образом все тексты на странице не самая приятная задача, но мы можем схитрить. Вынесем тексты в data-атрибуты элементов и переведем их все сразу с помощью скрипта.

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

src 
 └─ js
     └─ util.js

Вот так выглядит сам код:

const i18n = document.querySelectorAll("[data-intl]");
i18n.forEach(msg => {
  msg.innerHTML = chrome.i18n.getMessage(msg.dataset.intl);
});

chrome.i18n.getAcceptLanguages(languages => {
  document.documentElement.lang = languages[0];
});

При выполнении этот скрипт найдет все элементы с атрибутом data-intl, найдет соответствующую указанному идентификатору переведенную фразу и вставит полученный текст в innerHTML элемента.

<!-- До выполнения JS -->
<html>
  <body>
    <button data-intl="popupManageSettings"></button>
  </body>
</html>
<!-- После выполнения JS -->
<html lang="en">
  <body>
    <button data-intl="popupManageSettings">Manage settings</button>
  </body>
</html>

Добавление выпадающего меню и страницы настроек

Прежде чем перейти уже наконец к настоящей разработке, мы должны создать две страницы:

  1. Страницу настроек, которая будет содержать настройки пользователя.
  2. Выпадающее меню, которое будет открываться при нажатии на иконку расширения в панели браузера. Его можно использовать для различных сценариев, например, отображать быстрые настройки или собранную статистику.

Скриншот страницы с настройками
Выпадающее меню содержит ссылку на страницу настроек

Вот файлы, которые нам понадобятся:

src 
 ├─ css
 |    └─ paintBucket.css
 ├─ popup
 |    ├─ popup.html
 |    ├─ popup.css
 |    └─ popup.js
 └─ options
      ├─ options.html
      ├─ options.css
      └─ options.js

Файлы .css содержат самый обычный CSS, не больше, не меньше. Вы все наверняка знаете, как это работает 🙂 При желании вы, разумеется, можете использовать различные препроцессоры, главное, не забудьте скомпилировать код.

Файл paintBucket.css:

:root {
  --primary: #ff4500;
  --primary-dark: #d83a00;
  --secondary: #f2f2f2;
  --secondary-dark: #cdcdcd;
  --text: #000;
  --body: #fff;
  --btn-background: rgba(214, 214, 214,.25);
  --btn-background-hover: #c3c3c3;
  --focus: rgba(24, 116, 195, .3);
  --warning: #9b050c;
}

@media (prefers-color-scheme: dark) {
  :root {
    --secondary: #7e818c;
    --secondary-dark: #91939d;
    --text: #fff;
    --body: #202124;
    --btn-background: #292a2d;
    --btn-background-hover: #47494e;
    --focus: rgba(255, 255, 255, .3);
  }
}
*, *::before, *::after {
  box-sizing: border-box;
}

*:focus {
  box-shadow: 0 0 0 3px rgba(0, 125, 250, 0.45);
  outline: none;
}

html {
  width: 100%;
}

html, body {
  margin: 0;
  padding: 0;
  font-size: 16px;
  color: var(--text);
  background-color: var(--body);
}

body {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

Обратите внимание, выпадающее меню — это НЕ отдельная вкладка браузера. Его размер зависит от контента. Если вы хотите задать определенные фиксированные размеры, просто установите свойства width и height для элемента html.

Выпадающее меню

Напишем HTML-код выпадающего меню, а также подключим туда CSS- и JS-файлы.

Файл popup.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title data-intl="name"></title>

    <link rel="stylesheet" href="../css/paintBucket.css">
    <link rel="stylesheet" href="popup.css">

    <script src="../js/util.js" defer></script>
    <script src="popup.js" defer></script>
  </head>
  <body>
    <h1 id="title"></h1>
    <button data-intl="popupManageSettings"></button>
  </body>
</html>

Заголовок первого уровня будет содержать название расширения и его текущую версию, а кнопка будет открывать страницу настроек. Обратите внимание, для кнопки установлен атрибут data-intl, то есть ее текст будет вставлен автоматически нашим вспомогательным скриптом.

Текст заголовка мы установим отдельно в файле popup.js. Там же будет обработчик клика по кнопке:

const title = document.getElementById('title');
const settingsBtn = document.querySelector('button');
const manifest = chrome.runtime.getManifest();

title.textContent = `${manifest.name} (${manifest.version})`;

settingsBtn.addEventListener('click', () => {
  chrome.runtime.openOptionsPage();
});

Чтобы получить доступ к данным манифеста, это скрипт использует runtime API браузера Chrome. Его метод getManifest возвращает JSON-объект (этот метод не требует специальных разрешений).

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

Стили меню в файле popup.css:

html {
  width: 360px;
  height: 160px;
  padding: 1.5rem;
}

body {
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

h1 {
  font-size: 1rem;
  font-weight: normal;
  text-align: center;
}

button {
  -moz-appearance: none;
  -webkit-appearance: none;
  appearance: none;
  border: none;
  color: #fff;
  background-color: var(--primary);
  transition: background-color 200ms;
  padding: 0.75rem 1.5rem;
  cursor: pointer;
}
button:hover {
  background-color: var(--primary-dark);
}

Мы полностью закончили меню, но наше расширение пока что ничего о нем не знает. Нужно зарегистировать его в файле manifest.json:

"action": {
  "default_popup": "popup/popup.html",
  "default_icon": {
    "16": "images/logo/16.png",
    "48": "images/logo/48.png",
    "128": "images/logo/128.png"
  }
},

Страница настроек

Страница с пользовательскими настройками создается точно так же.

Начинаем с разметки — файл options.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title data-intl="name"></title>

  <link rel="stylesheet" href="../css/paintBucket.css">
  <link rel="stylesheet" href="options.css">

  <script src="../js/util.js" defer></script>
  <script src="options.js" defer></script>
</head>
<body>
  <header>
    <h1>
      <!-- Icon provided by feathericons.com -->
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" role="presentation">
        <circle cx="12" cy="12" r="3"></circle>
        <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
      </svg>
      <span data-intl="optionsPageTitle"></span>
    </h1>
  </header>

  <main>
    <section id="generalOptions">
      <h2 data-intl="sectionGeneral"></h2>

      <div id="generalOptionsWrapper"></div>
    </section>
  </main>

  <footer>
    <p>Transcribers of Reddit extension by <a href="https://lars.koelker.dev" target="_blank">lars.koelker.dev</a>.</p>
    <p>Reddit is a registered trademark of Reddit, Inc. This extension is not endorsed or affiliated with Reddit, Inc. in any way.</p>
  </footer>
</body>
</html>

Сейчас здесь нет никаких обещанных настроек, только подготовленный для них блок. Вставлять их мы будем с помощью JavaScript (в файле options.js).

Сначала определим дефолтные значения и получим ссылку на DOM-элемент контейнера:

const defaultSettings = Object.freeze({
  border: false,
  background: false,
});
const generalSection = document.getElementById('generalOptionsWrapper');

Теперь нужно загрузить сохраненные ранее настройки. Для этого нам понадобится storage API (который мы уже зарегистрировали ранее в манифесте).

Можно сохранять данные локально (chrome.storage.local) или синхронизировать их между разными устройствами пользователя (chrome.storage.sync). Для простоты будем использовать локальное хранилище.

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

У нас ключ — это строка 'settings', но можно также передавать массив строк, если требуется загрузить несколько различных настроек.

Полученные из хранилища данные будут переданы функции-коллбэку:

chrome.storage.local.get('settings', ({ settings }) => {
  const options = settings ?? defaultSettings; // если не было сохраненных настроек, используем дефолтные
  if (!settings) {
    chrome.storage.local.set({
     settings: defaultSettings,
    });
 }

  // создаем HTML-опции для каждой настройки
  const generalOptions = Object.keys(options).filter(x => !x.startsWith('advanced'));
  generalOptions.forEach(option => createOption(option, options, generalSection));
});

Рендер опций вынесен в отдельную функцию createOption():

function createOption(setting, settingsObject, wrapper) {
  const settingWrapper = document.createElement("div");
  settingWrapper.classList.add("setting-item");
  settingWrapper.innerHTML = `
  <div class="label-wrapper">
    <label for="${setting}" id="${setting}Desc">
      ${chrome.i18n.getMessage(`setting${setting}`)}
    </label>
  </div>

  <input type="checkbox" ${settingsObject[setting] ? 'checked' : ''} id="${setting}" />
  <label for="${setting}"
    tabindex="0"
    role="switch"
    aria-checked="${settingsObject[setting]}"
    aria-describedby="${setting}-desc"
    class="is-switch"
  ></label>
  `;

  const toggleSwitch = settingWrapper.querySelector("label.is-switch");
  const input = settingWrapper.querySelector("input");

  input.onchange = () => {
    toggleSwitch.setAttribute('aria-checked', input.checked);
    updateSetting(setting, input.checked);
  };

  toggleSwitch.onkeydown = e => {
    if(e.key === " " || e.key === "Enter") {
      e.preventDefault();
      toggleSwitch.click();
    }
  }

  wrapper.appendChild(settingWrapper);
}

Здесь мы создаем отдельный блок для каждой настройки и помещаем внутрь него чекбокс, который может быть включен или выключен. Добавляем слушатель события change для этого чекбокса и при изменении вызываем метод updateSetting, который должен обновить сохраненное значение в хранилище. Для этого предназначен метод хранилища set, который принимает два аргумента: обновленное значение всего хранилища и коллбэк (опциональный параметр, который мы не будем использовать).

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

function updateSetting(key, value) {
  chrome.storage.local.get('settings', ({ settings }) => {
    chrome.storage.local.set({
      settings: {
        ...settings,
        [key]: value
      }
    })
  });
}

Стили страницы в файле options.css:

header {
  background-color: var(--primary);
  color: #fff;
  padding: 1rem 1.75rem;
}
header > h1 {
  margin: 0 auto;
  max-width: 40rem;
  font-weight: normal;
  display: flex;
  align-items: center;
}
header > h1 > svg {
  width: 2rem;
  height: 2rem;
  margin-right: 0.5rem;
}

main {
  width: 100%;
  padding: 0 1.75rem;
  margin: 1.5rem auto;
  display: flex;
  flex-direction: column;
  flex: 1;
}
main > section {
  width: 100%;
  max-width: 40rem;
  margin: 1rem auto;
}
main > section > h2 {
  max-width: 50rem;
  font-weight: normal;
  margin: 0.25rem 0;
  color: var(--black);
}
main > section > h2.dropdown {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  width: 100%;
  font-size: 1.5rem;
  border: none;
  cursor: pointer;
  padding: 0.25rem 0;
  background-color: var(--white);
  transition: background-color 200ms;
  display: flex;
  align-items: center;
  justify-content: space-between;
  user-select: none;
}
main > section > h2.dropdown > svg {
  width: 1.6rem;
  height: 1.6rem;
  transition: transform 200ms;
}
main > section > h2.dropdown:hover {
  background-color: var(--gray);
}
main > section > h2.dropdown + div {
  display: none !important;
}
main > section > h2.dropdown[aria-expanded=true] > svg {
  transform: rotate(-90deg);
}
main > section > h2.dropdown[aria-expanded=true] + div {
  display: flex !important;
}
main > section > #generalOptionsWrapper, main > section #advancedOptionsWrapper {
  display: flex;
  flex-direction: column;
  margin: 1rem 0;
}
main > section > #generalOptionsWrapper > .setting-item, main > section #advancedOptionsWrapper > .setting-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.5rem 0;
}
main > section > #generalOptionsWrapper > .setting-item:hover > .label-wrapper > button, main > section > #generalOptionsWrapper > .setting-item:focus-within > .label-wrapper > button, main > section #advancedOptionsWrapper > .setting-item:hover > .label-wrapper > button, main > section #advancedOptionsWrapper > .setting-item:focus-within > .label-wrapper > button {
  opacity: 1;
}
main > section > #generalOptionsWrapper > .setting-item > .label-wrapper, main > section #advancedOptionsWrapper > .setting-item > .label-wrapper {
  display: flex;
  align-items: center;
}
main > section > #generalOptionsWrapper > .setting-item > .label-wrapper > button, main > section #advancedOptionsWrapper > .setting-item > .label-wrapper > button {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background: none;
  border: none;
  cursor: pointer;
  margin-left: 0.25rem;
  opacity: 0;
  transition: opacity 200ms;
}
main > section > #generalOptionsWrapper > .setting-item > .label-wrapper > button > svg, main > section #advancedOptionsWrapper > .setting-item > .label-wrapper > button > svg {
  width: 1.5rem;
  height: 1.5rem;
  stroke: var(--placeholder);
}
main > section > #generalOptionsWrapper > .setting-item > input, main > section #advancedOptionsWrapper > .setting-item > input {
  display: none;
}
main > section > #generalOptionsWrapper > .setting-item > input:checked + .is-switch, main > section #advancedOptionsWrapper > .setting-item > input:checked + .is-switch {
  background-color: var(--primary);
  border-color: var(--primary);
}
main > section > #generalOptionsWrapper > .setting-item > input:checked + .is-switch:hover, main > section #advancedOptionsWrapper > .setting-item > input:checked + .is-switch:hover {
  background-color: var(--primary-dark);
  border-color: var(--primary-dark);
}
main > section > #generalOptionsWrapper > .setting-item > input:checked + .is-switch::after, main > section #advancedOptionsWrapper > .setting-item > input:checked + .is-switch::after {
  transform: translateY(-50%) translateX(28px);
}
main > section > #generalOptionsWrapper > .setting-item > .is-switch, main > section #advancedOptionsWrapper > .setting-item > .is-switch {
  width: 4rem;
  height: 34px;
  background: var(--secondary);
  cursor: pointer;
  border-radius: 30px;
  overflow: hidden;
  position: relative;
  transition: background-color 200ms, border-color 200ms;
  border: 1px solid var(--secondary);
}
main > section > #generalOptionsWrapper > .setting-item > .is-switch:hover, main > section > #generalOptionsWrapper > .setting-item > .is-switch:focus, main > section #advancedOptionsWrapper > .setting-item > .is-switch:hover, main > section #advancedOptionsWrapper > .setting-item > .is-switch:focus {
  background-color: var(--secondary-dark);
  border-color: var(--secondary-dark);
}
main > section > #generalOptionsWrapper > .setting-item > .is-switch::after, main > section #advancedOptionsWrapper > .setting-item > .is-switch::after {
  background: #fff;
  border-radius: 50%;
  box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.2);
  content: "";
  height: 30px;
  width: 30px;
  position: absolute;
  left: 2px;
  top: 50%;
  transform: translateY(-50%);
  transition: transform 200ms;
  will-change: transform;
}

footer {
  display: flex;
  flex-direction: column;
  padding: 1rem 1.75rem;
  text-align: center;
}
footer > p {
  margin: 0.25rem 0;
  font-size: 75%;
}
footer > p > a {
  text-decoration: none;
  color: var(--text);
}

dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  max-width: 40rem;
  border: none;
  margin: 0;
  box-shadow: 5px 5px 8px 0 rgba(0, 0, 0, 0.15);
  border-radius: 0.5rem;
  padding: 2rem;
}
dialog::backdrop {
  background: rgba(0, 0, 0, 0.25);
  backdrop-filter: blur(10px);
}
dialog > #dialogClose {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  padding: 0.25rem;
  background: none;
  border: none;
  cursor: pointer;
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  color: lightgray;
  transition: color 200ms;
}
dialog > #dialogClose:hover {
  color: var(--placeholder);
}
dialog > #dialogClose > svg {
  width: 1.75rem;
  height: 1.75rem;
}
dialog > #optionDescription {
  margin-bottom: 2rem;
  line-height: 1.5;
}
dialog > #dialogActions {
  display: flex;
  justify-content: flex-end;
  margin: 0 -0.5rem;
}
dialog > #dialogActions > #dialogOk {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background: var(--blue);
  padding: 0.75rem 1.5rem;
  min-width: 4rem;
  max-width: fit-content;
  border: none;
  color: var(--white);
  cursor: pointer;
}

И конечно же, мы должны зарегистрировать новую страницу в манифесте:

"options_ui": {
  "open_in_tab": true,
  "page": "options/options.html"
},

При необходимости вы можете оформить страницу опций в виде модального диалога. Для этого нужно установить свойство open_in_tab: false.

Установка расширения для разработки

Мы сделали всю подготовительную работу, и теперь неплохо было бы проверить, работает ли наше расширение.

Перейдите на страницу chrome://extensions и включите режим разработчика (в правом верхнем углу).

Появятся три кнопки, нам нужна первая — Загрузить распакованное расширение. Кликните на нее и в появившемся окне выберите папку src, в которой хранятся все файлы нашего проекта.

После этого наше расширение появится в списке в статусе Установлено. Также его иконка появится в верхней панели браузера рядом с адресной строкой (его можно найти, кликнув на иконку паззла ?) . Если вы нажмете на нее, то появится выпадающее меню с кнопкой:

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

Страница настроек:

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

Основной скрипт

Итак, мы сделали выпадающее меню и настроили взаимодействие со страницей настроек, но само расширение все еще не делает ничего полезного. Давайте исправим это и добавим основной скрипт.

Создайте новый файл comment.js внутри директории js.

Теперь его нужно добавить в манифест в секцию content_scripts:

"content_scripts": [
  {
    "matches": [ "*://www.reddit.com/*" ],
    "js": [ "js/comment.js" ]
  }
],

Это массив, каждый элемент которого состоит из двух частей:

  • matches — содержит массив урлов, на которых браузер должен запускать наше расширение. Мы указываем всего один урл домен верхнего уровня — www.reddit.com, а изменяющиеся части обозначаются звездочками.
  • js — содержит массив скриптов, которые нужно запустить на подходящих урлах.

Помните, что скрипты расширения не могут взаимодействовать на скриптами сайта, не могут использовать их переменные или функции.

// script_on_website.js
const username = 'Lars';

// content_script.js
console.log(username); // Error: username is not defined

Давайте приступим уже к программированию.

Прежде всего добавим несколько констант в файл comment.js, в которых сохраним селекторы DOM-элементов и регулярные выражения для дальнейшей работы.

Объект CommentUtils содержит набор тестовых функций для определения, содержит ли пост «ToR-комментарии» и существует ли вообще блок комментариев на странице.

const messageTypes = Object.freeze({
  COMMENT_PAGE: 'comment_page',
  SUBREDDIT_PAGE: 'subreddit_page',
  MAIN_PAGE: 'main_page',
  OTHER_PAGE: 'other_page',
});

const Selectors = Object.freeze({
  commentWrapper: 'div[style*="--commentswrapper-gradient-color"] > div, div[style*="max-height: unset"] > div',
  torComment: 'div[data-tor-comment]',
  postContent: 'div[data-test-id="post-content"]'
});

const UrlRegex = Object.freeze({
  commentPage: //r/.*/comments/.*/,
  subredditPage: //r/.*//
});

const CommentUtils = Object.freeze({
  isTorComment: (comment) => comment.querySelector('[data-test-id="comment"]') ? comment.querySelector('[data-test-id="comment"]').textContent.includes('m a human volunteer content transcriber for Reddit') : false,
  torCommentsExist: () => !!document.querySelector(Selectors.torComment),
  commentWrapperExists: () => !!document.querySelector('[data-reddit-comment-wrapper="true"]')
});

Затем мы проверяем, находится ли пользователь на странице «поста» с комментариями. Для этого проверяем текущий урл и обновляем значение переменной directPage. Эта проверка сработает в том случае, если пользователь перешел на страницу с внешних ресурсов или собственноручно вбил адрес в адресную строку браузера.

let directPage = false;
if (UrlRegex.commentPage.test(window.location.href)) {
  directPage = true;
  moveComments();
}

Но в большинстве случаев переходы осуществляются внутри SPA. Чтобы отловить их, мы можем добавить слушатель сообщений, используя runtime API.

chrome.runtime.onMessage.addListener(msg => {
  if (msg.type === messageTypes.COMMENT_PAGE) {
    waitForComment(moveComments);
  }
});

Теперь нам нужно написать реализацию функций moveComment и waitForComment.

moveComment — находит в общем списке «ToR-комментарии» с транскрипцией медиа-ресурсов и перемещает их в начало списка. Также при наличии соответствующих настроек она изменяет цвет фона и рамки этих комментариев (это проверяется с помощью уже знакомого нам метода get).

function moveComments() {
  if (CommentUtils.commentWrapperExists()) {
    return;
  }

  const wrapper = document.querySelector(Selectors.commentWrapper);
  let comments = wrapper.querySelectorAll(`${Selectors.commentWrapper} > div`);
  const postContent = document.querySelector(Selectors.postContent);

  wrapper.dataset.redditCommentWrapper = 'true';
  wrapper.style.flexDirection = 'column';
  wrapper.style.display = 'flex';

  if (directPage) {
    comments = document.querySelectorAll("[data-reddit-comment-wrapper='true'] > div");
  }

  chrome.storage.local.get('settings', ({ settings }) => { 
    comments.forEach(comment => {
      if (CommentUtils.isTorComment(comment)) {
        comment.dataset.torComment = 'true';
        if (settings.background) {
          comment.style.backgroundColor = 'var(--newCommunityTheme-buttonAlpha05)';
        }
        if (settings.border) {
          comment.style.outline = '2px solid red';
        }
        comment.style.order = "-1";
        applyWaiAria(postContent, comment);
      }
    });
  })
}

Функция applyWaiAria добавляет комментариям aria-атрибуты для улучшения доступности для скринридеров. Мы также используем вспомогательную функцию uuidv4 для генерации уникальных идентификаторов.

function applyWaiAria(postContent, comment) {
  const postMedia = postContent.querySelector('img[class*="ImageBox-image"], video');
  const commentId = uuidv4();

  if (!postMedia) {
    return;
  }

  comment.setAttribute('id', commentId);
  postMedia.setAttribute('aria-describedby', commentId);
}

function uuidv4() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

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

function waitForComment(callback) {
  const config = { childList: true, subtree: true };
  const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      if (document.querySelector(Selectors.commentWrapper)) {
        callback();
        observer.disconnect();
        clearTimeout(timeout);
        break;
      }
    }
  });

  observer.observe(document.documentElement, config);
  const timeout = startObservingTimeout(observer, 10);
}

function startObservingTimeout(observer, seconds) {
  return setTimeout(() => {
    observer.disconnect();
  }, 1000 * seconds);
}

Добавляем сервис-воркер

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

Создайте новый файл sw.js внутри директории src (важно, чтобы он находился в корне проекта).

Сразу же зарегистрируем его в манифесте:

"background": {
  "service_worker": "sw.js"
}

Начнем, как обычно, с констант для сообщений и типов страниц:

const messageTypes = Object.freeze({
  COMMENT_PAGE: 'comment_page',
  SUBREDDIT_PAGE: 'subreddit_page',
  MAIN_PAGE: 'main_page',
  OTHER_PAGE: 'other_page',
});

const UrlRegex = Object.freeze({
  commentPage: //r/.*/comments/.*/,
  subredditPage: //r/.*//
});

const Utils = Object.freeze({
  getPageType: (url) => {
    if (new URL(url).pathname === '/') {
      return messageTypes.MAIN_PAGE;
    } else if (UrlRegex.commentPage.test(url)) {
      return messageTypes.COMMENT_PAGE;
    } else if (UrlRegex.subredditPage.test(url)) {
      return messageTypes.SUBREDDIT_PAGE;
    }

    return messageTypes.OTHER_PAGE;
  }
});

Мы будем следить за историей навигации браузера (onHistoryStateUpdated). Нужное нам событие будет вызываться каждый раз, когда SPA обращается к History API при обновлении контента страницы без перезагрузки.

При наступлении события мы запрашиваем активную вкладку браузера и получаем ее tabId, а затем отправляем сообщение основному скрипту расширения.

chrome.webNavigation.onHistoryStateUpdated.addListener(async ({ url }) => {
  const [{ id: tabId }] = await chrome.tabs.query({ active: true, currentWindow: true });

  chrome.tabs.sendMessage(tabId, {
    type: Utils.getPageType(url),
    url
  });
});

Готово!

Вот и все!

Перейдите в менеджер расширений (chrome://extensions) и перезапустите наше расширение, чтобы все изменения применились.

Теперь откройте на Reddit какой-нибудь пост, у которого есть комментарии с транскрибацией (например, вот этот). И вы сразу же увидите нужный комментарий, потому что он отображается в самом верху списка, да еще и с рамкой и красным фоном (если вы установили соответствующие настройки).

Расширение Transcribers of Reddit находит комментарии с транскрибацией изображения, поднимает их на верх списка и выделяет красной рамкой и фоном

Заключение

Создавать браузерные расширения совсем несложно, как вы только что убедились. В них нет ничего особенного — те же самые HTML, CSS и JavaScript, которые вы пишете каждый день, плюс совсем немного магии в manifest.json.

Если у вас возникнут какие-то проблемы, скорее всего вы сможете найти ответ в отличной документации Chrome API.

Репозиторий с кодом статьи

Понравилась статья? Поделить с друзьями:
  • Как написать плагин вордпресс
  • Как написать пищевым фломастером на мастике
  • Как написать пиши грамотно
  • Как написать пицца на английском языке
  • Как написать питчинг