Расширение Chrome с Typescript

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

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

  • Папка src содержит логику расширения.
  • Папка scss содержит файлы стилей.
  • общедоступная папка содержит статические файлы.
  • .env — это файл среды.
  • webpack.config.js содержит логику для компиляции машинописного текста в javascript.

Следуя документу расширения chrome, вы можете видеть, что расширение создается из некоторых основных частей: файла manifest.json, файлов html, файлов js, изображений. Поэтому, кроме файлов js, остальные части следует называть статическими файлами и помещать их в папку public.

Во-первых, вы должны установить некоторые пакеты для запуска проекта. Следуйте файлу package.json:

// package.json
{
  "name": "typescript-chrome-extension",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode production --watch"
  },
  "author": "Kien Duong",
  "license": "ISC",
  "devDependencies": {
    "@types/chrome": "^0.0.193",
    "@types/jquery": "^3.5.14",
    "browser-sync": "^2.27.10",
    "browser-sync-webpack-plugin": "^2.3.0",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.7.1",
    "mini-css-extract-plugin": "^2.6.1",
    "node-sass": "^7.0.1",
    "sass-loader": "^13.0.2",
    "ts-loader": "^9.3.1",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3"
  },
  "dependencies": {
    "@types/lodash": "^4.14.183",
    "axios": "^0.27.2",
    "dotenv": "^16.0.1",
    "dotenv-webpack": "^8.0.1",
    "jquery": "^3.6.0",
    "lodash": "^4.17.21"
  }
}
// webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const Dotenv = require('dotenv-webpack');

const path = require('path');
const outputPath = 'dist';
const entryPoints = {
    main: [
        path.resolve(__dirname, 'src', 'main.ts'),
        path.resolve(__dirname, 'scss', 'main.scss')
    ],
    background: path.resolve(__dirname, 'src', 'background.ts')
};

module.exports = {
    entry: entryPoints,
    output: {
        path: path.join(__dirname, outputPath),
        filename: '[name].js',
    },
    resolve: {
        extensions: ['.ts', '.js'],
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: /node_modules/,
            },
            {
                test: /\.(sa|sc)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(jpg|jpeg|png|gif|woff|woff2|eot|ttf|svg)$/i,
                use: 'url-loader?limit=1024'
            }
        ],
    },
    plugins: [
        new CopyPlugin({
            patterns: [{ from: '.', to: '.', context: 'public' }]
        }),
        new MiniCssExtractPlugin({
            filename: '[name].css',
        }),
        new Dotenv(),
    ]
};

На следующем шаге мы объясним конфигурацию в файле webpack.config.js. Как видите, мы используем 3 библиотеки:

  • copy-webpack-plugin используется для копирования всех статических файлов из папки public в папку dist.
  • mini-css-extract-plugin используется для компиляции файлов scss и sass в один файл css.
  • dotenv-webpack используется для настройки файла среды (.env)
  • outputPath определяет имя папки build.
  • entryPoints определяет имена скомпилированных файлов. Это означает, что webpack скомпилирует 3 файла src/main.ts, scss/main.scss, src/background.ts в dist/main.js, dist/main.css, dist /background.js

В этой статье мы напишем логику для сбора всех адресов электронной почты на конкретном сайте. Другими словами, логика обнаружит строку адреса электронной почты внутри HTML DOM. Требование должно заключаться в том, что когда пользователь нажимает на расширение, будет отображаться всплывающее окно с кнопкой «Найти электронные письма». Пользователь нажимает на эту кнопку, расширение покажет список обнаруженных электронных писем или сообщение об ошибке, если электронные письма не могут быть найдены. Мы определяем шаблон всплывающего окна следующим образом:

// index.html
<!DOCTYPE html>
<html>

<head>
    <link rel="stylesheet" href="main.css">
    <script type="text/javascript" src="main.js"></script>
</head>

<body>
    <div class="olive-extension">
        <button type="button" class="olive-extension__btn" id="olive-extension__btn">
            Find emails
        </button>

        <div class="olive-extension__email-table" id="olive-extension__email-table"></div>

        <h3 class="olive-extension__error-msg" id="olive-extension__error-msg"></h3>
    </div>
</body>

</html>

Вы можете видеть, что скомпилированные файлы (main.css и main.js) связаны с html-файлом. Нам не нужно добавлять путь, потому что все скомпилированные файлы и статические файлы будут находиться в одной и той же папке dist.

// manifest.json
{
    "name": "Typescript Chrome Extension",
    "description": "Detect the emails on a website",
    "version": "1.0.0",
    "manifest_version": 3,
    "icons": {
        "16": "/images/logo-16x16.png",
        "48": "/images/logo-48x48.png",
        "128": "/images/logo-128x128.png"
    },
    "permissions": [
        "activeTab",
        "scripting",
        "storage"
    ],
    "content_scripts": [
        {
            "matches": [
                "*://*/*"
            ],
            "js": [
                "main.js"
            ]
        }
    ],
    "action": {
        "default_popup": "index.html"
    },
    "background": {
        "service_worker": "background.js"
    }
}

Чтобы запустить расширение, мы должны определить файл manifest.json, содержащий информацию о расширении.

  • версия_манифеста
  • icons содержит информацию о логотипе расширения.
  • разрешения определяет разрешения, к которым расширение хочет получить доступ.
  • content_scripts ›match определяет шаблоны URL, над которыми должно работать расширение.
  • content_scripts › js определяет файл логики для расширения.
  • default_popup определяет файл шаблона HTML.
  • service_worker определяет работающий фоновый файл.
// src/background.ts
chrome.runtime.onMessage.addListener((request, sender) => { });
// src/main.ts
import $ from 'jquery';

class Main {
    constructor() {
        this.init();
    }

    init() {
        $(document).ready(async () => {
            this.resetEmailTable();
            this.hideErrorMessage();
            this.handleLoadEmails();
            this.handleData();
        });
    }

    hideErrorMessage() {
        if ($('#olive-extension__error-msg')[0]) {
            $('#olive-extension__error-msg').removeClass('olive-extension-showing').addClass('olive-extension-hidding');
            $('#olive-extension__error-msg').html();
        }
    }

    showErrorMessage(text: string) {
        if ($('#olive-extension__error-msg')[0]) {
            $('#olive-extension__error-msg').removeClass('olive-extension-hidding').addClass('olive-extension-showing');
            $('#olive-extension__error-msg').html(text);
        }
    }

    resetEmailTable() {
        if ($('#olive-extension__email-table')[0]) {
            $('#olive-extension__email-table').empty();
        }
    }

    validateEmail(email: string) {
        if (email && email !== '') {
            return email.match(
                /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
            );
        }

        return false;
    };

    handleLoadEmails() {
        const t = this;

        $(document).ready(() => {
            $('#olive-extension__btn').on('click', async () => {
                t.hideErrorMessage();

                const tabData = await chrome.tabs.query({ active: true, currentWindow: true });
                const tabId = tabData[0].id;

                const handleCurrentTab = () => {
                    const documentHtml = document.body.innerHTML;
                    const context = documentHtml.toString();
                    const emailsData = context.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi);
                    const emails: string[] = [];

                    if (emailsData && emailsData.length) {
                        for (const item of emailsData) {
                            if (
                                !item.endsWith('.png') &&
                                !item.endsWith('.jpg') &&
                                !item.endsWith('.jpeg') &&
                                !item.endsWith('.gif') &&
                                !item.endsWith('.webp')
                            ) {
                                emails.push(item);
                            }
                        }
                    }

                    if (emails && emails.length) {
                        const temp: string[] = [];

                        let html = `
                            <table>
                                <thead>
                                    <tr>
                                        <th>Email</th>
                                    </tr>
                                </thead>

                                <tbody>
                        `;

                        for (const email of emails) {
                            if (!temp.includes(email)) {
                                temp.push(email);

                                html += `
                                    <tr>
                                        <td>${email}</td>
                                    </tr>
                                `;
                            }
                        }

                        html += `
                                </tbody>
                            </table>
                        `;

                        chrome.runtime.sendMessage(chrome.runtime.id, { type: 'EMAIL_TABLE_CONTENT', data: html });
                    } else {
                        chrome.runtime.sendMessage(chrome.runtime.id, { type: 'NO_EMAIL' });
                    }
                }

                if (tabId) {
                    chrome.scripting.executeScript({
                        target: { tabId },
                        func: handleCurrentTab,
                    })
                }
            });
        });
    }

    handleData() {
        chrome.runtime.onMessage.addListener((request, sender) => {
            if (request && request.type) {
                switch (request.type) {
                    case 'EMAIL_TABLE_CONTENT': {
                        $('#olive-extension__email-table').html(request.data);
                        break;
                    }
                    case 'NO_EMAIL': {
                        this.showErrorMessage('No email');
                        break;
                    }
                }
            }
        });
    }
}

new Main();

Когда пользователь нажимает кнопку «Найти электронные письма», расширение должно получить доступ к текущей активной вкладке с помощью chrome.tabs.query({ active: true, currentWindow: true }). Эта функция возвращает информацию об активной вкладке, и мы можем использовать логику js с этой активной вкладкой, запустив chrome.scripting.executeScript({ target: { tabId }, func: handleCurrentTab }). Функция handleCurrentTab должна содержать логику для обнаружения строки адреса электронной почты в html-доме, потому что в это время мы сможем получить активный дом-вкладку.

Чтобы передать результат из логики активной вкладки в логику расширения, мы можем использовать функцию chrome.runtime.sendMessage. chrome.runtime.onMessage.addListener будет прослушивать все отправленные сообщения и отображать результат или сообщение об ошибке в шаблоне всплывающего окна расширения для каждого конкретного случая.

// scss/main.scss
body {
    background: #fff;
    width: 300px;
    height: auto;
    font-family: "Roboto";
    margin: 0;
    padding: 1rem;
    .olive-extension {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        &__email-table {
            table {
                border: 1px solid #e3e3e3;
                thead {
                    tr {
                        th {
                            font-size: 0.8rem;
                            text-align: center;
                            border-bottom: none;
                            padding: 0.3rem;
                            background: #dcdcdc;
                        }
                    }
                }
                tbody {
                    tr {
                        td {
                            font-size: 0.8rem;
                            text-align: center;
                            padding: 0.6rem;
                        }
                    }
                }
            }
        }
        h3 {
            &.olive-extension__error-msg {
                margin: 0.2rem 0;
                padding: 0;
                font-size: 0.8rem;
                font-weight: 200;
                text-align: center;
                color: red;
            }
        }
        button {
            font-size: 0.8rem;
            margin-bottom: 1rem;
            width: 50%;
            border: none;
            color: #fff;
            height: 2rem;
            border-radius: 5px;
            cursor: pointer;
            &.olive-extension__btn {
                background: #007bff;
            }
        }
    }
}

.olive-extension-hidding {
    display: none !important;
}

.olive-extension-showing {
    display: block !important;
}

На данный момент мы уже используем это расширение. Запустите эту команду yarn build или npm run build, чтобы создать папку dist. Когда у нас уже есть папка dist, откройте браузер Chrome и запустите эту ссылку chrome://extensions/. По этой ссылке вы сможете увидеть все установленные расширения.

Вы должны изменить режим на режим разработчика, который позволит вам загружать расширение с вашего локального компьютера. Нажмите кнопку «Загрузить распакованное», чтобы загрузить логику из папки dist. Так что теперь расширение уже можно использовать.

Во время разработки вам не нужно много раз пересобирать проект, просто нужно запустить эту команду yarn dev или npm run dev. Эта команда будет отслеживать любые изменения в исходном коде и автоматически перестраивать проект. В chrome://extensions/screen вам просто нужно щелкнуть значок перезагрузки, чтобы загрузить обновленное расширение.

Исходный код: https://github.com/duongduckien/typescript-chrome-extension.git