Top View


Author Naoya Miyagawa

Vite ベースの vitesse-webext で Chrome 拡張機能を作ってみた⚡️

2022/03/15

🌱 はじめに

こんにちは。
自社プロダクト「360(さんろくまる)」を手掛けている技術開発第一部門の みやがわ です。

先日、Zenn でこちらの記事を見つけました。

以前から antfu/vitesse-webext の存在はなんとなく知っていたのですが、
実際の作例を目にするのは初めてで、改めて「普段の技術スタックで Chrome 拡張機能が作れることへの驚き」と「自分も作ってみたい」という気持ちが芽生えてきました。

というわけで、作ってみました。


🌱 モチベーション

普段開発に利用している VSCode で使っているタブ移動と、
それに割り当てているショートカットキー 「Cmd + Ctrl + [ or ]」 を Chrome で利用したい!

ショートカット設定

  • タブ切り替え: Cmd + Shift + [ or ] (VSCode デフォルト)
  • タブ移動(左/右): Cmd + Ctrl + [ or ] (カスタムショートカット)

Image from Gyazo


🌱 作ったもの

ショートカット設定

  • タブ切り替え: Cmd + Shift + [ or ] (Chrome デフォルト)
  • タブ移動(左/右): Cmd + Ctrl + J or K (作成した拡張機能)
  • タブ移動(最左/最右): Cmd + Ctrl + H or L (作成した拡張機能)

後述しますが、諸事情によりショートカットを統一できませんでした😇


Image from Gyazo


🌱 拡張機能の作り方

スターターテンプレートを使おう

Vite ベースの高速開発ベースを Vue / Nuxt / Vite のコアチームメンバーの方が作成されています。
Readmeにある通り、TypeScript / Vue / WindiCSS / IconComponent が out-of-the-box で利用できます。とてもパワフルです。


開発効率◎

実際に使ってみて思ったのは、本当に開発効率が良いことです。


ポイント① サンプル

拡張機能の特徴である、下記の要素に対するサンプルが書いてある状態から開発を始められます。

  • browser_action
  • content_scripts
  • options_page
  • background

ポイント② TypeScript

拡張機能をブラウザに読み込ませる際の起点となるのは manifest.json ファイルです。
上記の各要素や拡張機能の動作のトリガーとなるショートカットキーを定義するものです。

このスターターキットでは、TypeScript で書かれた src/manifest.ts を編集するだけで、開発時およびビルド時に manifest.json が生成されます。


こういったコードや型によるサポートが充実しているので、初めて拡張機能を作った私でも簡単に開発することができました😋


ショートカットキーを登録しよう

今回作りたい拡張機能は、下記の要件です。
ですので、バックグラウンドで実行させる background のスクリプトを変更しました。

  • ページ情報へのアクセスは不要
  • ブラウザがアクティブな状態で常にショートカットキーを利用できる

ここでは指定のショートカットキー addListener で監視しているものです。シンプルですね。
ショートカットキーは環境別に指定ができますが、今回は mac のみ設定しています。
また、扱えるキーや書き方については、Googleのドキュメントが提供されています。とても見やすいです。


src/background/main.ts

import { moveTabToLeftEndKey, moveTabToLeftKey, moveTabToRightEndKey, moveTabToRightKey, } from '~/config/command'
import { useTabShifter } from '~/composables/useTabShifter'
 ︙
const { shiftTabToLeft, shiftTabToRight, shiftTabToLeftEnd, shiftTabToRightEnd } = useTabShifter()

browser.commands.onCommand.addListener(async (command) => {
  // move one
  if (command === moveTabToLeftKey) {
    shiftTabToLeft()
    return
  }
  if (command === moveTabToRightKey) {
    shiftTabToRight()
    return
  }

  // move to end
  if (command === moveTabToLeftEndKey) {
    shiftTabToLeftEnd()
    return
  }
  if (command === moveTabToRightEndKey) {
    shiftTabToRightEnd()
    return
  }
})

src/config/command.ts

/**
 * Single move
 */
export const moveTabToLeftKey: string = 'tab_to_left';
export const moveTabToLeft: Command = {
  [moveTabToLeftKey]: {
    suggested_key: { mac: 'Command+MacCtrl+J', }, },
};

export const moveTabToRightKey: string = 'tab_to_right';
export const moveTabToRight: Command = {
  [moveTabToRightKey]: {
    suggested_key: { mac: 'Command+MacCtrl+K', }, },
};

/**
 * Edge move
 */
export const moveTabToLeftEndKey: string = 'tab_to_left_end';
export const moveTabToLeftEnd: Command = {
  [moveTabToLeftEndKey]: {
    suggested_key: { mac: 'Command+MacCtrl+H', }, },
};

export const moveTabToRightEndKey: string = 'tab_to_right_end';
export const moveTabToRightEnd: Command = {
  [moveTabToRightEndKey]: {
    suggested_key: { mac: 'Command+MacCtrl+L', }, },
};

export const commandList = {
  //...executeBrowserAction,
  ...moveTabToLeft, ...moveTabToRight, ...moveTabToLeftEnd, ...moveTabToRightEnd
};

また、manifest.json への反映はこれだけで大丈夫です。とても便利ですね。

src/manifest.ts

import { commandList } from './config/command'

export async function getManifest() {
 ︙
  const manifest: Manifest.WebExtensionManifest = {

    commands: commandList,
  }
 ︙
}

タブ移動を実装しよう

行っている処理は非常にシンプルで、下記の流れで実装しています。

  1. タブ移動前に、現在のタブのインデックスを取得。(browser.tabs.query()
  2. 移動先候補のインデックスを算出。
  3. 指定インデックスにタブ移動。(browser.tabs.move()

import { Tabs } from 'webextension-polyfill'

export const useTabShifter = () => {
  const currentTab = ref<Tabs.Tab | null>(null)

  const tabIndexes = computed(() => ({
    left: Math.max(0, Number(currentTab.value?.index) - 1),
    right: Number(currentTab.value?.index) + 1,
    leftEnd: 0,
    rightEnd: -1,
  }))

  /** Shift active tab to right */
  const shiftTabToRight = async () => {
    await updateCurrentTab()
    shiftTab(tabIndexes.value.right)
  }

 ︙

  /** Shift active tab to left */
  const shiftTabToLeftEnd = async () => {
    await updateCurrentTab()
    shiftTab(tabIndexes.value.leftEnd)
  }

  /** Shift tab to specified index */
  const shiftTab = async (indexTo: number) => {
    const tabId = currentTab.value?.id
    if (tabId === undefined) {
      return
    }

    try {
      await browser.tabs.move(tabId, { index: indexTo })
    } catch (error) {
      if (error == 'Error: Tabs cannot be edited right now (user may be dragging a tab).') {
        setTimeout(() => shiftTab(indexTo), 50)
      }
    }
  }

  /** Update current tab info */
  const updateCurrentTab = async () => {
    ;[currentTab.value] = await browser.tabs.query({ active: true, currentWindow: true })
  }

  return { shiftTabToLeft, shiftTabToRight, shiftTabToRightEnd, shiftTabToLeftEnd }
}

Chrome に拡張機能を読み込ませる

開発時は pnpm dev 、本番プレビューは pnpm build を実行し、各種スクリプトや manifest.json をビルドします。
ブラウザ上で動作確認をする際は、
拡張機能ページのデベロッパーモードを ON にして Load unpacked から extension/ を読み込ませるだけです。

今回作成したストア未公開の「Tab Shifter」が読み込まれています。


Image from Gyazo


🌱 実現できなかったこと

ショートカットキーに使えないキーがあった

下記のドキュメントに使用できるキーが記載されています。 https://developer.chrome.com/docs/extensions/reference/commands/#supported-keys

| 後述しますが、諸事情によりショートカットを統一できませんでした😇

一方で当初やりたかった Cmd + Ctrl + [ or ] の内、 [ or ] が使用できず、
commands に登録しようとしても、Chrome が拡張機能に対してエラーを吐きました。
また、 Cmd + Shift + Ctrl + H or L のように3つ以上の修飾キーを組み合わせることができませんでした。
(もしかしたらこのパターンだけかもしれませんが。)


登録できるショートカットキー数が 4 つまでだった

拡張機能の browser_action のトリガーとして扱える _execute_browser_action を含め、
設定できるショートカットキーは 4 つまでのようです。

開発途中で、「コマンド + 数字 で対応箇所にタブ移動できると便利そう」というアイデアをもらったので実装しようとしましたが、
このショートカットキー数の制約に阻まれました😇
(「コマンド + 数字」のショートカットキーが扱える拡張機能はたまに見るので、何か方法があるのかもしれません。)


🌱 おわりに

とてつもなく強力なスターターテンプレートの恩恵で、
簡単な Chrome 拡張機能であれば、ほとんどコードを書かずに作ることができます。

実際にコミット履歴を見返すとファイル差分はこれだけです。

ぜひ、興味ある方は一度試してみてください✨


参考


Naoya Miyagawa

Naoya Miyagawa

Twitter X

Hi🙋🏻‍♂️ Web Developer in Fukuoka, Japan🇯🇵 ❄️ Fusic Co., Ltd.|360 (さんろくまる) ❄️ PHP: CakePHP ❄️ JS: Vue.js ❄️ AWS: SAP ❄️ FAV: nm7/🍣/SG🇸🇬