Модификация веб страницы
========================

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

Существует два способа сделать это используя WebExtension API:

* **Декларативно**: объявить шаблон, которому соответствует набор URL-адресов, и загрузить набор скриптов на страницы, которые попадают в под этот шаблон.
* **Программно**: используя JavaScript API, загрузить скрипт на страницу, из определённой вкладки.

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

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

В этой статье мы рассмотрим оба способа загрузки скрипта.

Модификация страниц, подпадающих под URL-шаблон
-----------------------------------------------

Прежде всего создадим новую директорию, назовём её "modify-page". В этой директории, создадим файл "manifest.json", со следующим содержимым:


```json
{
  "manifest\_version": 2,
  "name": "modify-page",
  "version": "1.0",

  "content\_scripts": [
    {
      "matches": ["https://developer.mozilla.org/\*"],
      "js": ["page-eater.js"]
    }
  ]
}
```


Ключ `content_scripts` (en-US) - это как мы загружаем скрипты на страницы, соответствующие URL-шаблону. В нашем случае, `content_scripts` говорит браузеру загрузить скрипт "page-eater.js" на все страницы, начинающиеся с https://developer.mozilla.org/.

**Примечание:** Поскольку свойство `"js"` ключа `content_scripts` это массив, вы можете использовать его, для внедрения более одного скрипта. Если вы сделаете это, страницы получат набор, как если бы эти скрипты были загружены самой страницей, они будут загружены в той же очерёдности, в которой они расположены в массиве.

**Примечание:** Ключ `content_scripts` также имеет свойство `"css"`, которое вы можете использовать для вставки CSS-таблиц.

Далее, создадим файл "page-eater.js", внутри директории "modify-page":


```js
document.body.textContent = "";

var header = document.createElement("h1");
header.textContent = "Эта страница была съедена";
document.body.appendChild(header);
```


Теперь установим расширение, и перейдём на страницу https://developer.mozilla.org/:

**Примечание:** Обратите внимание, несмотря на то, что в указанном видео, на странице addons.mozilla.org всё работает нормально, на текущий момент, для этого сайта, контентные скрипты заблокированы.

Программная модификация страницы
--------------------------------

Что, если вы всё ещё хотите "съедать" страницы, но лишь в тех случаях, когда пользователь попросил об этом? Давайте обновим этот пример таким образом, чтобы мы внедряли контентный скрипт, когда пользователь выбирает соответствующий пункт контентного меню.

Для начала обновим "manifest.json":


```json
{
  "manifest\_version": 2,
  "name": "modify-page",
  "version": "1.0",

  "permissions": ["activeTab", "contextMenus"],

  "background": {
    "scripts": ["background.js"]
  }
}
```


Мы удалили ключ `content_scripts` и добавили два новых:

* `permissions (разрешения)`: для внедрения скрипта, нам нужны разрешения для страниц, которые мы модифицируем. Разрешение `activeTab` это способ получить доступ к текущей вкладки. Нам также нужно разрешение `contextMenus`, чтобы добавлять в контекстное меню новые элементы.
* `background (фоновый)`: мы используем этот ключ, для загрузки постоянного "фонового скрипта", с именем "background.js", в котором мы настроим контекстное меню и внедрим контентный скрипт.

Давайте создадим этот файл. Создадим новый файл "background.js" в директории "modify-page" и поместим в него следующий код:


```js
browser.contextMenus.create({
  id: "eat-page",
  title: "Съесть эту страницу",
});

browser.contextMenus.onClicked.addListener(function (info, tab) {
  if (info.menuItemId == "eat-page") {
    browser.tabs.executeScript({
      file: "page-eater.js",
    });
  }
});
```


В этом скрипте мы создаём элемент контекстного меню, передавая ему определённый идентификатор и заголовок (текст будет отображаться в элементе контекстного меню). Затем мы настраиваем обработчик событий таким образом, чтобы когда пользователь выбирает пункт контекстного меню, осуществлялась проверка, наш ли это элемент `eat-page`. Если это так - внедряем скрипт "page-eater.js" в текущую вкладку, используя `tabs.executeScript()` (en-US) API. Это API опционально принимает идентификатор вкладки, в качестве аргумента. Мы опустили его, это означает, что скрипт будет внедряться в текущую активную вкладку.

На данном этапе расширение должно иметь следующий вид:
```

modify-page/
    background.js
    manifest.json
    page-eater.js
```


Теперь перезагрузим расширение, откроем страницу (на этот раз любую) активируем контекстное меню и выберем "Съесть эту страницу":

**Примечание:** Обратите внимание, несмотря на то, что в указанном видео, на странице addons.mozilla.org всё работает нормально, на текущий момент, для этого сайта, контентные скрипты заблокированы.

Обмен сообщениями
-----------------

Контентные и фоновые скрипты не могут на прямую взаимодействовать друг с другом. Не смотря на это они могут взаимодействовать с помощью обмена сообщениями. Для этого один конец создаёт обработчик сообщений, а другой - может посылать сообщения. В следующей таблице представлены API-интерфейсы, задействованные с каждой стороны:

|  | В контентном скрипте | В фоновом скрипте |
| --- | --- | --- |
| Отправка сообщения | `browser.runtime.sendMessage()` | `browser.tabs.sendMessage()` |
| Получение сообщения | `browser.runtime.onMessage` | `browser.runtime.onMessage` |

Давайте обновим наш пример, чтобы посмотреть, как послать сообщение из фонового скрипта.

Изменим "background.js" :


```js
browser.contextMenus.create({
  id: "eat-page",
  title: "Съесть эту страницу",
});

function messageTab(tabs) {
  browser.tabs.sendMessage(tabs[0].id, {
    replacement: "Message from the extension!",
  });
}

browser.contextMenus.onClicked.addListener(function (info, tab) {
  if (info.menuItemId == "eat-page") {
    browser.tabs.executeScript({
      file: "page-eater.js",
    });

    var querying = browser.tabs.query({
      active: true,
      currentWindow: true,
    });
    querying.then(messageTab);
  }
});
```


Теперь, после внедрения "page-eater.js", мы используем `tabs.query()` (en-US), чтобы получить текущую открытую вкладку и используем `tabs.sendMessage()` (en-US), для отправки сообщения контентному скрипту, загруженному на этой вкладке. Сообщение несёт полезную нагрузку `{replacement: "Message from the extension!"}`.

Далее, обновим "page-eater.js":


```js
function eatPage(request, sender, sendResponse) {
  document.body.textContent = "";

  var header = document.createElement("h1");
  header.textContent = request.replacement;
  document.body.appendChild(header);
}

browser.runtime.onMessage.addListener(eatPage);
```


Теперь, вместо простого "поедания страницы", контентный скрипт ждёт сообщение, используя `runtime.onMessage` (en-US). Когда сообщение получено, контентный скрипт выполняет в точности такой же код, как и а примере ранее, за исключением того, что заменяющий текст берётся из `request.replacement`.

Если мы хотим отправить сообщение наоборот, из контентного скрипта в фоновый, настройка будет обратной данному примеру, за исключением того, что мы будем использовать `runtime.sendMessage()` (en-US) в контентном скрипте.

**Примечание:** Все эти примеры внедряют JavaScript; вы можете программно внедрять стилевые таблицы CSS используя функцию `tabs.insertCSS()` (en-US).

Узнать больше
-------------

* Руководство по Content scripts
* Ключ манифеста`content_scripts` (en-US)
* Ключ манифеста`permissions`
* `tabs.executeScript()` (en-US)
* `tabs.insertCSS()` (en-US)
* `tabs.sendMessage()` (en-US)
* `runtime.sendMessage()` (en-US)
* `runtime.onMessage` (en-US)
* Примеры использования `content_scripts`:
    + borderify
    + notify-link-clicks-i18n
    + page-to-extension-messaging
* Примеры использования `tabs.executeScript()`:
    + beastify
    + context-menu-demo