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!




Комментарии (0)