☀️

【コピペ5分】いま見てるWebページ内の9月1日を8月32日にして夏休みを終わらせないChrome拡張機能を作ってみた

2021/09/01に公開

https://twitter.com/ukokq/status/1432849691146739712?s=20

8月末にやることがたまりすぎて漠然とした不安に陥っていたので、心を整えるために作ってみました。

できたもの(インストールして試してね)

9月1日の朝に思い立って作ったんですが、Chromeウェブストアで一般公開しようとしたら

無知無念。。。。。。。

公開されたら貼り付けますので来年試してください(涙)
ただし、読んでいただいてるZennユーザーの方であれば、このページの通りにやれば5分〜10分でご自身のPCのChrome限定でこのくっだらない拡張機能を適用させられますのでぜひ夏が終わる前に試していただきたいところです。


こうなる

追記(2022年8月31日)

1年間忘れ去っていましたが普通に公開されてました。恥ずかしい。

https://chrome.google.com/webstore/detail/8月32日/epiknkmlfamciepmeankagdalbojcgoe

つくりかた

つくりたいものとしては「現在閲覧中のWebページ内に『9月1日』のような日付表示が存在していれば、それを『8月32日』に書き換えることで8月を1日だけ延長し、月末処理に追われる心に安寧をもたらすChrome拡張」です。早速さくさく作っていきます。

1. フォルダを作る

とりあえず「8.32」というフォルダを作り、その中にソースコードを置いていきます。

2. manifest.jsonを書く

https://qiita.com/mdstoy/items/9866544e37987337dc79

拡張機能のメタ的な設定をするやつです。
上記を参考にしながら以下のように設定しました。

manifest.json
{
  "name": "8月32日",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "すべての9月1日を8月32日にして夏休みを延長します",
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "script.js"
      ]
    }
  ]
}

3. script.jsを書く

拡張機能の本体となるものです。
DOMが静的なものと動的なものとで別の処理が必要となります。
(JSコード全体はこのパートの一番下で折りたたみにしているのでコピペする際は読み飛ばしてください)

静的DOMの場合の置換

フロントエンド側でDOMを書き換えたりしないWebページに適用される部分です。
HTMLのロード完了後に中身のHTMLをそのまま検索し、該当文字列を置き換えているシンプルな設計です。

window.addEventListener('load', () => {
  // body以下のscript以外のノードの中身をざっくりreplaceしてみる
  const childNodes = document.querySelectorAll('html body :not(script)');
  for (let i = 0; i < childNodes.length; i++) {
    const replaced = childNodes[i].innerHTML.replace('9月1日', '8月32日');
    childNodes[i].innerHTML = replaced;
  }
});

動的DOMの場合の置換

VueとかReactとか使ってるモダンなページ向けです。
TwitterとかのSNSはだいたいこっちが必要です。
MutationObserverを利用してDOMが変化したらそのぶんのノードだけを書き換えます。

// まずはObserverでDOM変化があった場合のコールバックを定義
const observer = new MutationObserver(records => {
  records.forEach(record => {
    // ここでrecord.addedNodesを再帰的に処理して
    // テキストノードのtextContentの中身を検索・置換する
  });
});

// body全体から下のノード全てを監視対象にする
observer.observe(document.querySelector('html body'), {
  childList: true,
  subtree: true,
});

https://qiita.com/munieru_jp/items/a6f1433652124a2165e4

静的DOM置換時のように全体のinnerHTMLとかを書き換えてしまうと正常に動作しなくなることがあるので、HTMLノードを末端まで調べあげて、いわゆる「空白文字列でないテキストノード」まで到達したらそのtextContentを書き換えることにします。

// HTMLノードを再帰的に探索して文字列置換をする関数
const recursiveReplace = (nodes) => {
  nodes.forEach(node => {
    // 子ノードがあったらその配列を引数にした自身を実行
    if (node.hasChildNodes()) recursiveReplace(node.childNodes);
    // テキストノードで中身があればreplaceを試みる
    if (node.nodeType === 3 && node.textContent) {
      const replaced = node.textContent.replace('9月1日', '8月32日');
      node.textContent = replaced;
    }
  });
}

正規表現で9月1日を発見する

9月1日という文字列表現はいろいろあるので、できるだけ全てを漏らさぬようカバーするために正規表現で検索・置換するようにします。(面倒だったのできちんとチェックはしていません)

const last_day_of_summer = /(9|)(|\/|\-|\.)(0|){0,1}(1|)()*/g;

コード全体

クリックして展開
script.js
// けしからん9月1日を検出するための正規表現
const before = /(9|)(|\/|\-|\.)(0|){0,1}(1|)()*/g;
const after = '8月32日'; // beforeの部分文字列から構成してもいいかも

// HTMLノードを再帰的に探索して文字列置換をする関数
const recursiveReplace = (nodes) => {
  nodes.forEach(node => {
    // 子ノードがあったらその配列を引数にした自身を実行
    if (node.hasChildNodes()) recursiveReplace(node.childNodes);
    // テキストノードで中身があればreplaceを試みる
    if (node.nodeType === 3 && node.textContent) {
      const replaced = node.textContent.replace(before, after); // ここで8月延長
      node.textContent = replaced;
    }
  });
}

// ロード完了してから以下を実行
window.addEventListener('load', () => {
  // 静的HTML書き換え
  // body以下のscript以外のノードの中身をざっくりreplaceしてみる
  const childNodes = document.querySelectorAll('html body :not(script)');
  for (let i = 0; i < childNodes.length; i++) {
    const replaced = childNodes[i].innerHTML.replace(before, after); // ここで8月延長
    childNodes[i].innerHTML = replaced;
  }

  // 動的HTML書き換え
  // まずはObserverでDOM変化があった場合のコールバックを定義
  const observer = new MutationObserver(records => {
    // 追加を検知したノードの配列を再帰的文字列置換の関数に入れて実行する
    records.forEach(record => recursiveReplace(record.addedNodes));
  });
  // body全体から下のノード全てを監視対象にする
  observer.observe(document.querySelector('html body'), {
    childList: true,
    subtree: true,
  });
});

ここまでで、以下のようなシンプルな構成になっていればOKです。

(デスクトップとかの任意のディレクトリ)
  ┗━ /8.32
      ┣━ manifest.json
      ┗━ script.js

4. Chromeに読み込ませて動作を確認する

Chrome拡張のページにゆき、右上の「開発者モード」をONにすると「パッケージ化されていない拡張機能を読み込む」ボタンが出るので、それをクリックして今回の8.32フォルダを選択します。

問題がなければ上図のように読み込まれます。
適当なWebページで動作確認しつつ、consoleを見ていてエラーが出たらスクリプトを修正したあとにこの拡張機能のリロードボタンをクリックして変更を反映 → Webページをリロード の順で、試しながら開発を進めてゆきます。

自分の手元で試す場合にはここまでで完成です!

5. Chromeウェブストアで一般公開する場合

ここは余裕をもって行いましょう(当日なんてもってのほかでしたね)

作ったコードをアップロード

先ほどの「8.32」のフォルダは、あらかじめZIPで圧縮しておきます。

https://belltree.life/chrome-extension-release/

Chromeウェブストアにアクセスし、右上の歯車マークから「デベロッパー ダッシュボード」に移動します。

同意にチェックをいれて500円ほどのお布施をクレジットで支払います。
一回支払うと以後は追加課金したりサブスク的にとられる心配はありません。

支払い終了後は自動でデベロッパーコンソールへ移動するので、「+ 新しいアイテム」ボタンをクリックして先ほど圧縮したZIPを指定しアップロードします。

審査に必要な情報の入力

項目はわりと多めですが、必須なものだけ紹介します。

<ストアの掲載情報>
  • 説明文(アイテムの紹介・インストールした場合のメリットを明記)
  • カテゴリ・言語の選択
  • 拡張機能のアイコン画像
    • 画像全体は128x128ピクセル
    • 実際のアイコンを示している部分は96x96ピクセルで各辺外側16ピクセルは透明のパディングがあることが望ましい
    • PNG形式が必須
    • 背景色が明るいテーマでもダークテーマでもよく見えるようにすること
    • より詳しくはこちら: https://developer.chrome.com/docs/webstore/images/#icons
  • スクリーンショット画像(使っている様子の画面など)
    • 1280x800または640x400サイズ
    • JPEGまたは透過無しのPNG形式
    • 最低1枚・最大5枚
<プライバシーへの取り組み>
  • 単一用途(具体的にどのように動作するかの簡潔で明解な説明)
  • 権限が必要な理由
    主にmanifest.jsonで設定した「どのWebサイトを見ているときに、どのタブで動作させるか」のような、この拡張機能を動作させるにあたってアクセスが必須な条件を設定した理由を述べます。
アカウントのプロフィール

初めてChrome拡張を作る場合は、アプリ本体ではなく自身に関する情報をしっかり設定しておかないと必須項目が入力されてないとみなされて先に進めません。かなりわかりづらいのですが、左上のハンバーガーメニューから「アカウント」をクリックし、プロフィールの「投稿者の表示名」と「メールアドレス」を必ず埋めておきましょう。

「審査のため送信」

必ず「(?)送信できない理由」をクリックして必須項目への記入漏れがないことを確認したあと、「下書きとして保存する」ボタンを少なくとも1回クリックしてください。そのあとで 「審査のため送信」ボタンが押せるようになります。

なお、ここで紹介している拡張機能のコードそのままを送信しようとすると、上図のような警告っぽい何かが表示されます。これは「あらゆる全てのWebページで動作をさせられるように権限を設定しているため、本当にそんな広い範囲で問題ないか調べるのに時間かかりますよ」の意です。

よって、当日中は無理でも少しでも早く審査してもらえたらということで「Twitterを開いているときのみ日付が8月32日になる」よう、以下のように適用範囲を狭めて申請を送ってみました。

manifest.jsonの一部
"content_scripts": [
  {
    "matches": [
      "https://twitter.com/*"
    ],
    "js": [
      "script.js"
    ]
  }
]

あとで気づいたのですが、activeTabという権限を用いることで、Webサイト全般を指定するよりもユーザー操作でより限定された範囲を操作するということで審査がおりやすいようです。今度試してみようと思いました。

https://kajindowsxp.com/chrome-host-permissions/

6. 審査が通ったら(自動で)公開

ここは通ったら書きます。

まとめ

  • Chrome拡張は1フォルダにJSON1つとJacaScript1つから簡単にはじめられる!
  • 自分のChromeで使うだけならすぐに試せる
  • Chromeウェブストアで一般公開するには初回のみ5US$の課金が必要
  • 一般公開されるまで最低3日ぐらいはかかりそう
    当日中すぐは絶対無理なのでハッカソンなどでは気をつけよう
  • 今回作った拡張はお試しなので大変どうでもよい機能しかないですが、特定の文字列置き換えができると次のような「言葉を方言に置き換える拡張機能」のようなものも作れます。

https://matome.eternalcollegest.com/post-2141282798739932201

審査を担当するChromeウェブストアの中の人には「すでに間に合ってないやんwww」と思われていることでしょう。夏休みの宿題はやはり最初に全て片付けるが正義である。

全体的な流れの参考

https://qiita.com/RyBB/items/32b2a7b879f21b3edefc

https://original-game.com/how-to-make-chrome-extensions/

Discussion