Почему вам стоит обратить внимание на Manifest V3 и TypeScript
Если вы подумывали о создании расширения для Chrome, но вас пугала перспектива устаревания Manifest V2, приготовьтесь — это ваш шанс. Manifest V3 пришёл, чтобы остаться, а его сочетание с TypeScript превращает разработку расширений из «отладки загадочных условий гонки в 2 часа ночи» в нечто по-настоящему профессиональное.
Признаюсь честно: раньше создание браузерных расширений было похоже на борьбу с осьминогом вслепую. Но сегодня? Сегодня это больше похоже на экскурсию, где TypeScript держит вас за руку и мягко указывает, когда вы собираетесь выстрелить себе в ногу.
Архитектурный ландшафт
Прежде чем мы погрузимся в код, давайте разберёмся, как все части соединяются вместе. Современное расширение для Chrome с Manifest V3 — это не просто что-то одно, это целая экосистема скриптов, каждый из которых имеет свои специфические обязанности и ограничения.
React/HTML"] ContentScript["Content Script
Выполняется в контексте страницы"] ServiceWorker["Service Worker
Логика в фоновом режиме"] StorageAPI["Chrome Storage API"] User -->|Нажатие на значок расширения| Popup User -->|Взаимодействие с страницей| ContentScript Popup -->|Отправка сообщения| ServiceWorker ContentScript -->|Отправка сообщения| ServiceWorker ServiceWorker -->|Сохранение данных| StorageAPI Popup -->|Чтение данных| StorageAPI ServiceWorker -->|Общение| ContentScript
Эта архитектура существует по соображениям безопасности — каждый компонент работает в разных контекстах с разными разрешениями. Это как крепость со специализированными стражами, каждый из которых защищает свой периметр.
Настройка среды разработки
Давайте перейдём к практике. Сначала создайте каталог вашего проекта и инициализируйте его:
mkdir my-awesome-extension
cd my-awesome-extension
npm init -y
Затем установите необходимые зависимости:
npm install --save-dev typescript webpack webpack-cli ts-loader @types/chrome @types/node
npm install react react-dom
npm install --save-dev @types/react @types/react-dom
Теперь инициализируйте TypeScript:
npx tsc --init
Это создаст файл tsconfig.json. Обновите его, чтобы он был строг к TypeScript, но разумен:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Создание основы Manifest V3
Файл manifest.json — это паспорт вашего расширения. Он объявляет, что делает ваше расширение и что ему для этого нужно. Вот хорошо структурированный пример, который работает с TypeScript:
{
"manifest_version": 3,
"name": "Моё потрясающее расширение",
"version": "1.0.0",
"description": "Расширение, которое делает потрясающие вещи, очевидно",
"permissions": ["storage", "scripting", "tabs"],
"action": {
"default_popup": "popup.html",
"default_title": "Моё потрясающее расширение"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"host_permissions": ["<all_urls>"]
}
Обратите внимание, что мы больше не используем фоновые страницы — только service_worker. Это способ Manifest V3 поддерживать эффективность и компактность. Сервис-воркер просыпается, когда это необходимо, и снова засыпает, экономя системные ресурсы. Представьте его как высококвалифицированного ассистента, который появляется только по вызову.
Создание пользовательского интерфейса всплывающего окна с помощью React и TypeScript
Создайте src/popup/index.tsx:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import './popup.css';
interface CounterData {
clickCount: number;
lastClicked?: string;
}
const Popup: React.FC = () => {
const [count, setCount] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
chrome.storage.local.get(['clickCount'], (result) => {
setCount(result.clickCount || 0);
setLoading(false);
});
}, []);
const handleClick = async (): Promise<void> => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.id) {
chrome.tabs.sendMessage(tab.id, { action: 'doSomethingAwesome' });
const newCount = count + 1;
setCount(newCount);
chrome.storage.local.set({
clickCount: newCount,
lastClicked: new Date().toISOString()
});
}
};
if (loading) {
return <div className="popup">Загрузка...</div>;
}
return (
<div className="popup">
<h1>Привет, расширение!</h1>
<p>Вы нажали кнопку <strong>{count}</strong> раз</p>
<button onClick={handleClick}>Сделать что-то потрясающее</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<Popup />);
И соответствующий popup.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="popup.js"></script>
</body>
</html>
Контент-скрипт: где происходит волшебство на странице
Создайте src/content/index.ts:
interface ExtensionMessage {
action: string;
payload?: unknown;
}
interface ContentScriptResponse {
success: boolean;
message: string;
}
chrome.runtime.onMessage.addListener(
(request: ExtensionMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: ContentScriptResponse) => void) => {
if (request.action === 'doSomethingAwesome') {
try {
// Ваша потрясающая логика здесь
const elements = document.querySelectorAll('body');
elements.forEach((element) => {
element.style.borderRadius = '8px';
element.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
});
sendResponse({
success: true,
message: 'Что-то потрясающее было сделано!'
});
} catch (error) {
sendResponse({
success: false,
message: `Ошибка: ${error instanceof Error ? error.message : 'Неизвестная ошибка'}`
});
}
}
}
);
Сервис-воркер: мозг вашего расширения
Создайте src/background/index.ts:
interface StorageData {
clickCount: number;
lastClicked?: string;
extensionEnabled: boolean;
}
// Инициализация хранилища при установке расширения
chrome.runtime.onInstalled.addListener((): void => {
chrome.storage.local.set({
clickCount: 0,
extensionEnabled: true
} as StorageData);
console.log('Расширение установлено и готово к работе!');
});
// Прослушивание сообщений от контент-скриптов и всплывающего окна
chrome.runtime.onMessage.addListener(
(request: { action: string }, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void)
