Skip to main content

Разрабатываем расширение для браузера

· 7 min read

Hello, world!

В этом небольшом туториале мы с вами разработаем простое, но полезное расширение для браузера с помощью Plasmo.

Наше расширение будет представлять собой вызываемый сочетанием клавиш попап с инпутом для поиска информации на MDN с выводом 5 лучших результатов в виде списка. Кроме основного функционала, мы добавим страницу настроек для кастомизации цветов и отображения хлебных крошек. Мы будем разрабатывать расширения для Chrome, которое также будет работать в Firefox.

Вот как это будет выглядеть:


Для тех, кого интересует только код, вот ссылка на соответствующий репозиторий.

Основной функционал - попап с поиском

Для работы с зависимостями будет использоваться Yarn.

Создаем шаблон приложения:

# mdn-finder - название приложения/расширения
yarn create plasmo mdn-finder

Переходим в созданную директорию и устанавливаем зависимости:

cd mdn-finder

yarn

Устанавливаем дополнительные зависимости, необходимые для работы поиска:

yarn add @plasmohq/storage downshift flexsearch fzf swr
  • @plasmohq/storage - абстракция над Storage API, который может использоваться расширениями браузера для локального хранения данных;
  • downshift - библиотека, предоставляющая примитивы для разработки простых, гибких, отвечающих всем критериям WAI-ARIA React-компонентов autocomplete/combobox или select/dropdown;
  • flexsearch - библиотека для реализации полнотекстового поиска;
  • fzf - библиотека для реализации неточного (fuzzy) поиска;
  • swr - хуки React для получения, кэширования и мутации данных.

Структура проекта будет следующей:

- assets
- icon.png
- search-index.json
- search.png
- src
- components
- Search.tsx
- search
- fuzzy-search.ts
- search-utils.ts
- search.tsx
- background.ts
- options.tsx
- popup.tsx
- storage.ts
- style.css
- ...

После переноса файлов в директорию src, необходимо немного отредактировать файл tsconfig.json:

{
// ...
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~*": [
"./src/*"
]
}
}
}

О самом поиске я рассказывал в этой статье, поэтому в данном туториале мы сосредоточимся на Plasmo. Скопируйте файлы из директорий components, search и assets, а также файл style.css из репозитория проекта. Поисковый индекс (search-index.json), также можно копировать с MDN. Запросы к MDN из другого источника блокируются CORS, поэтому поисковый индекс хранится локально.

Для того, чтобы иметь возможность работать с поисковым индексом, необходимо немного отредактировать файл package.json:

{
// ...
"manifest": {
"web_accessible_resources": [
{
"resources": [
"assets/search-index.json"
],
"matches": [
"https://*/*"
]
}
],
"host_permissions": [
"https://*/*"
]
}
}

Точкой входа приложения Plasmo является файл popup.tsx. Как следует из названия, этот компонент отвечает за рендеринг попапа, в котором будет находиться инпут для поиска. Редактируем этот файл следующим образом:

import Search from './components/Search'
import './style.css'

function IndexPopup() {
return <Search preload={true} />
}

export default IndexPopup

Запускаем сервер для разработки с помощью команды yarn dev. Выполнение этой команды приводит к генерации директории build/chrome-mv3-dev с файлами расширения.

Переходим по адресу chrome://extensions/ и загружаем расширение в браузер (кнопка "Загрузить распакованное расширение"/"Load unpacked extension"):


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

Сочетание клавиш для запуска расширения можно установить на странице chrome://extensions/shortcuts:


Для создания производственной сборки необходимо выполнить команду yarn build. По умолчанию создается сборка для Chrome. В настоящее время Plasmo также поддерживает создание сборок для Firefox. Команда для создания такой сборки: yarn build --target=firefox-mv2. Подробнее почитать об этом можно здесь.

Для тестирования расширения в Firefox необходимо сделать 2 вещи:

  • создать в директории src файл background.ts следующего содержания:
export {}

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

  • создать производственную сборку в виде архива с помощью команды yarn build --target=firefox-mv2 --zip.

Дополнительный функционал - страница настроек

Для инициализации страницы настроек достаточно создать файл options.tsx в директории src.

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

Создаем в директории src файл storage.ts следующего содержания:

import { Storage } from '@plasmohq/storage'

// ключ объекта настроек
export const OPTIONS_KEY = 'mdn_finder_options'

// дефолтные настройки
export const defaultOptions = {
// цвет фона
backgroundColor: '#282c34',
// цвет текста
textColor: '#f7f7f7',
// фон выделения
selectionBackground: '#5cb85c',
// цвет выделения
selectionColor: '#282c34',
// индикатор отображения хлебных крошек в списке результатов поиска
showUrl: true
}

// создаем экземпляр хранилища
const storage = new Storage()

// и экспортируем его
export default storage

Редактируем файл options.tsx следующим образом:

import { useRef } from 'react'
import storage, { defaultOptions, OPTIONS_KEY } from '~storage'
import './style.css'

export default function IndexOptions() {
// ссылка на кнопку отправки формы
const btnRef = useRef<HTMLButtonElement | null>(null)

// обработчик отправки формы
const onSubmit: React.FormEventHandler = async (e) => {
e.preventDefault()

// получаем данные формы в виде объекта
const formData = Object.fromEntries(
new FormData(e.target as HTMLFormElement).entries(),
)

try {
// записываем настройки в хранилище
await storage.set(OPTIONS_KEY, formData)

// меняем текст кнопки
if (btnRef.current) {
btnRef.current.textContent = 'Saved'

const id = setTimeout(() => {
btnRef.current.textContent = 'Save'
clearTimeout(id)
}, 1000)
}
} catch (e) {
console.log(e)
}
}

return (
<form className='options' onSubmit={onSubmit}>
<label>
Background color:{' '}
<input
type='color'
name='backgroundColor'
defaultValue={defaultOptions.backgroundColor}
/>
</label>
<label>
Result item color:{' '}
<input
type='color'
name='textColor'
defaultValue={defaultOptions.textColor}
/>
</label>
<label>
Selection background:{' '}
<input
type='color'
name='selectionBackground'
defaultValue={defaultOptions.selectionBackground}
/>
</label>
<label>
Selection color:{' '}
<input
type='color'
name='selectionColor'
defaultValue={defaultOptions.selectionColor}
/>
</label>
<label>
Show URL:{' '}
<input
type='checkbox'
name='showUrl'
defaultChecked={defaultOptions.showUrl}
/>
</label>
<button ref={btnRef}>Save</button>
</form>
)
}

Для того, чтобы попасть на страницу настроек, необходимо кликнуть по иконке расширения и выбрать пункт "Параметры"/"Options":



Возвращаемся к попапу. Редактируем файл search/search.tsx. Импортируем хранилище и извлекаем из него настройки:

import storage, { defaultOptions, OPTIONS_KEY } from '~storage'

// ...

function InnerSearchNavigateWidget(props: InnerSearchNavigateWidgetProps) {
// ...
const [options, setOptions] = useState(defaultOptions)

// ...
useEffect(() => {
storage.get<typeof options>(OPTIONS_KEY).then((opts) => {
if (opts) {
setOptions(opts)
}
})
}, [])

// далее работаем с этим компонентом
}

Индикатор отображения хлебных крошек (options.showUrl) используется при формировании списка результатов поиска:

resultItems.map((item, i) => (
<div
{...getItemProps({
key: item.url,
className:
'result-item ' + (i === highlightedIndex ? 'highlight' : ''),
item,
index: i,
})}
>
<HighlightMatch title={item.title} q={inputValue} />
{/* ! */}
{Boolean(options.showUrl) ? (
<>
<br />
<BreadcrumbURI uri={item.url} positions={item.positions} />
</>
) : null}
</div>
))

Цвет фона (options.backgroundColor) передается элементу формы:

<form
// ...
style={
{
'--background-color': options.backgroundColor,
} as React.CSSProperties
}
>
{/* ... */}
</form>

В файле style.css у нас имеются такие строки:

.search-form {
--background-color: var(--dark);

/* ... */
background-color: var(--background-color);
}

Остальные цвета передаются контейнеру с результатами поиска:

<div
className='search-results'
style={
{
'--text-color': options.textColor,
'--selection-background': options.selectionBackground,
'--selection-color': options.selectionColor,
} as React.CSSProperties
}
>
{searchResults}
</div>

В style.css у нас имеются такие строки:

.search-results {
--text-color: var(--light);
--selection-background: var(--success);
--selection-color: var(--dark);
}

.result-item span,
.result-item small {
color: var(--text-color);
}

.result-item mark {
background-color: var(--selection-background);
color: var(--selection-color);
}

Спасибо переменным CSS за их динамичность :)

Меняем настройки:


Запускаем расширение:


Видим, что настройки благополучно применяются к попапу.

Следует отметить, что проект, созданный с помощью Plasmo CLI, включает в себя GitHub Action Browser Platform Publisher для автоматической публикации расширения во всех поддерживаемых сторах. Подробнее почитать об этом можно здесь. Соответствующий файл можно найти в директории .github/workflows.

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

Надеюсь, вы узнали что-то новое и не зря потратили время.

Happy coding!