DevToolsのPanelを使ったChromeExtensionを作った時につまづいたところ

これは[misskey.dev ユーザー Advent Calendar 2024](https://adventar.org/calendars/10189)の15日目の記事です! 変わらず素敵な場所に感謝を...

https://adventar.org/calendars/10189

# きっかけ
ある日、労務担当の人が毎日遅くまで働いていることに気づきました。実質ワンマンで大変らしく、勉強がてらお手伝いできないかなと色々話を聞いてみたところ「利用しているサービスのwebページとExcelのシートの情報を突合する作業が月に数回ある」とのこと。なんでそんな仕事が残ってるのかさておき、これは楽にできそうです。というわけで情シスに話も通してこの作業をするためのChromeExtensionを作ることにしました。

# できたもの
1. 対象のサイトを開く
1. パネルを開く
1. チェック元のxlsxファイルを選択
1. 読み込むボタンをクリック

の4ステップで面倒だった突合作業を一発できるようにしました。
![スクリーンショット 2024-12-14 18.27.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168044/199751fb-ec19-8153-5f7f-cc9e9dd33aa0.png)
実行すると突合結果がテーブルの行として追加されていきます。ビューの切り替えでExcelの内容と見比べられたりもできるのですが、いろいろ見せられないので今回は割愛です。

"いくつかのサイトで対応する必要がある"ということと"違っていたときにその場でExcelの内容と見比べたい"という要求があったので、TampermonkeyなどのuserscriptやPageScriptでの実装はせずDevtoolsのpanelを使いました。ユーザー側の適応が必要というのはありますが、表示しているサイトの構造に影響されずビューワーを見せられるので楽ができた気がします。また、panelを作るのはそこまで難しくありませんでした。manifest.jsonでhtmlを指定するだけでサクッと作れます。

https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/api-samples/devtools/panels
# つまづきポイント
## 表示中のDOMをとったりそのDOMに対してjsを実行するのが大変
当初はuserScriptでの実装をかんがえていたので、DOMの内容の取得なんかはjsでパやっとやるつもりでいました。その後panelを使う方針にしてから重大な問題に気づきます。"jsが実行される世界"が違うのです。たとえばpanelの中で`documment.getElementById("ID!")`を実行しても、検索対象は"panelの中のDOM"です。このままではいかん、なんとかしなくては...
個人的には2通りのやり方を見つけました。ほんとはメッセージのAPIをつかっていい感じにできたりするかもしれませんが自分には複雑すぎるので今回はやりませんでした。
### 特定のサイトへの通信を見つけたらそのレスポンスをもらう
最高にヤバそうな文字列ですがそのままです。chromeのリクエスト完了に対するイベントリスナーを追加してレスポンスの内容をごっそりもらいます。この処理をpanelの中で実行すると、それ以降の通信でパターンに一致するものは全て我らの手中となります。

https://developer.chrome.com/docs/extensions/reference/api/devtools/network#event-onRequestFinished

```javascript
const pattern = new RegExp("https://mecha-checkcheck.example.com/viewview/[a-z]*");
// onRequestFinished の コールバック引数からレスポンス内容を得る処理
const getContent = async response => {
    return new Promise(resolve => {
      response.getContent((body) => {
        resolve(body)
      })
    })
};
// リクエストが完了したときのイベントリスナー
chrome.devtools.network.onRequestFinished.addListener(async req =>  {
  const url = req.request.url;
  if(pattern.test(url)){
      const responseBody = await getContent(req);
      console.log(responseBody);
  }
});
```
静的なコンテンツが降ってくるサイトには有効ですが動的な要素があったりするといまいちなかんじです。サイトじゃなくてAPIからの応答をゲットしたい場合はこっちが便利そうです。セキュリティとか動作への影響がありそうなのがちょっと心配です。
### 指定したタブでjsを実行させて戻り値をもらう
自分としてはこっちが本命です。指定したタブで、panel側で用意した処理をPageScriptみたいに実行してもらいます。実行には"scripting"と"activeTab"のパーミッションが必要です。

https://developer.chrome.com/docs/extensions/reference/api/scripting#method-executeScript

```javascript
/**
 * 実行されたときにアクティブになっているタブのIDを取得する
 **/
async function getCurrentTabId() {
  let queryOptions = { active: true, lastFocusedWindow: true };
  let [tab] = await chrome.tabs.query(queryOptions);
  return tab.id;
}
/**
 *  Windowで実行してもらう処理
 *  参照の都合上NodeやElementでは返せないので、必ず実の値を取って返す
 **/
async function hoeghoge(hogeArg){
  const data = Array.from(document.querySelectorAll(".hoge")).map(node => console.log(node.innerText))
  return data;
}
const data = await chrome.scripting.executeScript({
    target: { tabId : await getCurrentTabId() },
    func : hogehoge,
    args : [hogeArg] 
}).then(result => result[0]["result"]);
// 戻り値はresultキーを持つobjectになる
```
表示されているものに対して実行できるので、ページの情報という目的ならこっちのほうが確実に欲しいものがもらえたように思います。実行するコード自体はUserScript同様にdevtoolsのconsoleでデバッグできるのもすてき。さらにはそのページで定義されている関数も呼べるので、PageScriptで仕込んだり開発者が隠匿しそびれた関数も呼べたりします。結構便利に使えるかもしれません。
ただし、このコードをそのままループなどで連続して実行する場合は注意が必要です。この書き方ではコードを実行してもらうその瞬間にアクティブ(だった)タブで処理を実行することになります。ループ処理の最中にchromeのタブを切り替えると、実行する対象のタブが変わってしっちゃかめっちゃかになります。
## Storeを通していないパッケージ済みExtensionを使ってもらうのが大変
当初は作成したものを"パッケージ化されていない拡張機能を読み込む"機能で導入してもらおうと考えていました。しかしへーしゃが導入している製品の都合で、業務に使用していたユーザーがアクセスできるディレクトリはサインアウト時に全部削除されます。使用するたびに拡張機能のフォルダをダウンロードして導入してもらうのはあまりにもめんどくさい...ここでその人が普段使っているEdgeでは導入している拡張機能が毎回しっかり引き継がれていることに気づきました。なので、パッケージ化してしまえばいいということに気づいたのです。
本来はパッケージ化済みかつStoreでリリースされていないExtensionはセキュリティ上の懸念があるってことでインストールしても有効にすることはできません。 ~~べつにいいだろ/いやよくない~~ 
Windowsではレジストリに特定のキーを登録することでその使用を許可することができます。
Edge: `SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallAllowlist`

https://learn.microsoft.com/ja-jp/DeployEdge/microsoft-edge-policies#extensioninstallallowlist

Chrome: `\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist`

https://chromeenterprise.google/policies/?policy=ExtensionInstallAllowlist

登録する内容は、決まったパスに`"数字"="{extensionID}"`です。レジストリの値をファイルに吐くとこんな感じになります。
```
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallAllowlist]
"100"="kmjppopckdlpencclnhhckmahaokhamc"

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist]
"100"="kmjppopckdlpencclnhhckmahaokhamc"
```
このとき指定するExtensionIDは必ずパッケージ化したものをインストールして確認しましょう。パッケージ化していないものを読み込んだときとはIDが変わります。

:::note alert
当たり前ですが職場で許可を得ずこんなことしないでください。
できてもやってはいけません。
:::

# 完走した感想
無事労務の人の負担は減り、早く帰れる日もできたみたいです。理解があってやらせてくれる会社にも感謝です。一方でChromeExtensionのやれることの幅に改めて恐怖を感じました。こんなんなんでもできます。みなさんも新しくインストールする際は求められる権限をしっかり確認しましょう。

# さいごに
Qiita運営の人見てる~? Qiitadonをきっかけにfediverseに入りましたが今も楽しくさせてもらっています。できればもっといろんなQiitanグッズがほしいです。制作販売よろしくお願いします🙇