はじめに
前回、Webシステムからスマートフォンへ通知する方法について調べました。
その中で、
- Notifications API
- Push API
- Service Worker
- WebSocket
- ポーリング
など、通知やリアルタイム処理に関係する仕組みを知りました。
今回はもう一歩進めて、
Webシステム自体を変更せず、ブラウザ側から新着を検知してスマートフォンへ通知できないか?
という方法を試してみました。
そこで作ったのが、Chrome拡張機能による新着監視です。
一定間隔で対象ページを取得し、前回取得した未読IDと比較します。
新しいIDが追加されていた場合だけ、通知サービスへHTTPリクエストを送り、スマートフォンへ通知します。
※この記事では、実際に使用したURL・サービス固有の名称・環境情報などをサンプル用の値に置き換えています。
1. 作ったもの
全体像は次のようになっています。
┌─────────────────┐
│ Chrome拡張機能 │
└────────┬────────┘
│
│ chrome.alarms
│ 一定間隔で実行
▼
┌─────────────────┐
│ 対象Webシステム │
└────────┬────────┘
│
│ HTMLを取得
▼
┌─────────────────┐
│ 未読IDを抽出 │
└────────┬────────┘
│
│ 前回のIDと比較
▼
新着あり?
│
▼
┌─────────────────┐
│ 通知サービス │
└────────┬────────┘
│
▼
スマートフォン
サーバー側から新着情報を送ってもらうのではなく、
Chrome拡張側から一定間隔で状態を確認する
方式です。
これはポーリングの考え方です。
2. ファイル構成
今回のChrome拡張は、主に次のような構成にしました。
web-notifier/
├── manifest.json
├── background.js
├── options.html
└── options.js
それぞれの役割は次の通りです。
| ファイル | 役割 |
|---|---|
manifest.json |
Chrome拡張全体の設定 |
background.js |
定期的な新着確認・差分検出・通知 |
options.html |
通知先などを入力する設定画面 |
options.js |
設定値の保存・読み込み |
今回の中心となるのは background.js です。
3. manifest.json
まずChromeに、
このフォルダはChrome拡張機能です
と認識してもらうために必要なのが manifest.json です。
公開用に一般化すると、例えば次のような構成です。
{
"manifest_version": 3,
"name": "Web Message Notifier",
"version": "1.0.0",
"description": "Webシステムの新着情報を検知して通知します",
"permissions": [
"storage",
"alarms"
],
"host_permissions": [
"https://example.com/*",
"https://ntfy.sh/*"
],
"background": {
"service_worker": "background.js"
},
"options_page": "options.html"
}
順番に見ていきます。
manifest_version
"manifest_version": 3
今回は Manifest V3 を使用しています。
Chrome拡張機能の現在の仕組みでは、バックグラウンド処理にService Workerを利用できます。
permissions
"permissions": [
"storage",
"alarms"
]
今回必要なのは主に2つです。
storage
chrome.storage.local
を使用するための権限です。
今回は、
- 前回取得した未読ID
- 通知先の設定値
などを保存するために使用しています。
alarms
chrome.alarms
を使用するための権限です。
一定間隔で新着確認処理を起動するために使っています。
4. なぜsetIntervalではなくchrome.alarmsなのか
setInterval
指定した時間間隔で、同じ処理を繰り返し実行するJavaScriptの関数。
最初は、
setInterval(checkUnread, 60000);
のようにすればいいのでは?
とも考えられます。
しかし、Manifest V3ではバックグラウンド処理としてService Workerを使用します。
"background": {
"service_worker": "background.js"
}
Service Workerは、ずっと起動し続けることを前提とした仕組みではありません。
イベントが発生したときに起動して処理し、不要になれば停止することがあります。
そのため、
Service Workerを常駐させる
+
setInterval()
という考え方ではなく、
chrome.alarms
↓
時間になったらイベント発生
↓
Service Workerが処理
という形にしました。
ここはChrome拡張を作る中で重要だったポイントです。
5. host_permissions
次に、
"host_permissions": [
"https://example.com/*",
"https://ntfy.sh/*"
]
を設定します。
今回の拡張機能では、
対象Webシステム
と、
通知サービス
の2か所へHTTPリクエストを送ります。
そのため、拡張機能からアクセスする必要があるホストを指定しています。
6. background.jsをService Workerとして登録する
"background": {
"service_worker": "background.js"
}
これによって background.js がChrome拡張のバックグラウンド処理を担当します。
今回の background.js では、
定期実行
↓
HTML取得
↓
未読ID抽出
↓
前回との差分比較
↓
通知
↓
状態保存
という処理を行います。
7. chrome.alarmsで定期実行する
まずアラーム名を決めます。
const ALARM_NAME = "check-messages";
拡張機能をインストールしたときに、
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create(ALARM_NAME, {
periodInMinutes: 1
});
checkUnread();
});
としてアラームを作成します。
これで一定間隔でイベントを発生させられます。
さらに、Chrome起動時にもアラームを作成します。
chrome.runtime.onStartup.addListener(() => {
chrome.alarms.create(ALARM_NAME, {
periodInMinutes: 1
});
checkUnread();
});
そしてアラームが発生したら、
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === ALARM_NAME) {
checkUnread();
}
});
として新着確認を実行します。
全体として、
Chrome
↓
alarmイベント
↓
background.js
↓
checkUnread()
という流れになります。
8. Webページを取得する
新着確認の中心となるのが checkUnread() です。
まず対象ページを取得します。
const UNREAD_URL =
"https://example.com/messages";
const response = await fetch(UNREAD_URL, {
method: "GET",
credentials: "include",
cache: "no-store"
});
ここでは、
credentials: "include"
を指定しています。
対象ページがログインを必要とする場合、認証状態を利用したリクエストが必要になるためです。
また、
cache: "no-store"
も指定しています。
新着確認なのに古いキャッシュを取得すると困るため、毎回現在の状態を確認する意図があります。
9. HTTPステータスを確認する
ページを取得できなかった場合は、その回の処理を終了します。
if (!response.ok) {
console.log(
"ページ取得失敗:",
response.status
);
return;
}
例えば、
200 → 成功
404 → ページが存在しない
500 → サーバー側エラー
など、HTTPステータスによって結果が変わります。
fetch() が完了したからといって、必ず正常なレスポンスとは限らないため、
response.ok
を確認しています。
10. HTMLを取得する
レスポンスからHTMLを文字列として取得します。
const html = await response.text();
今回はAPIからJSONを取得しているわけではありません。
取得しているのは通常のWebページです。
つまり、
GET
↓
HTML
↓
必要な情報を探す
という処理になります。
11. HTMLから未読IDを探す
今回のWebページでは、各項目へのリンクに一意なIDが含まれていました。
公開用に単純化すると、
messageId=123
messageId=456
messageId=789
のような形式です。
そこで正規表現を使ってIDを抽出します。
const matches = [
...html.matchAll(/messageId=(\d+)/g)
];
正規表現の、
\d+
は、
1文字以上の数字
を表します。
例えば、
messageId=123
なら、
123
を取り出せます。
12. Setで重複を取り除く
HTML内に同じIDが複数回登場する可能性もあります。
そこで、
const currentIds = [
...new Set(matches.map(match => match[1]))
];
としています。
例えば、
["101", "101", "102", "103", "103"]
という結果でも、
Set を通すと、
["101", "102", "103"]
になります。
これを現在の未読ID一覧として扱います。
13. 前回の状態をchrome.storage.localから取得する
新着かどうかを判断するには、
前回はどのIDが存在していたか
を知る必要があります。
そこで、
const data = await chrome.storage.local.get([
"lastUnreadIds",
"notificationTopic"
]);
としてChrome側に保存したデータを読み込みます。
そして、
const previousIds =
Array.isArray(data.lastUnreadIds)
? data.lastUnreadIds
: null;
として前回のID一覧を取得します。
14. 初回は通知しない
初回起動時には、まだ前回データがありません。
例えば起動した時点で、
["101", "102", "103"]
という未読があったとします。
これを全部新着として通知すると、
拡張機能を入れる前から存在したもの
まで通知されてしまいます。
そこで、
if (previousIds === null) {
await chrome.storage.local.set({
lastUnreadIds: currentIds
});
return;
}
として、初回だけは通知しません。
初回起動
現在
[101, 102, 103]
↓
通知しない
↓
基準として保存
という処理です。
15. 件数ではなくIDの差分を見る
今回特に重要なのがここです。
単純に未読件数を比較すると、
前回:3件
今回:3件
なので、
新着なし
と判断してしまいます。
しかし実際には、
前回
101
102
103
から、
今回
101
103
104
になっている可能性があります。
102 が消えて 104 が追加されています。
つまり件数は同じでも、
新しい項目は存在しています。
そこで、
const newIds = currentIds.filter(
id => !previousIds.includes(id)
);
とします。
これは、
現在存在するIDのうち、前回存在しなかったもの
を探しています。
結果は、
["104"]
となります。
これによって、
未読件数の変化
ではなく、
未読集合の変化
を見ることができます。
16. 新着があれば通知する
新しいIDが存在した場合だけ通知します。
if (
newIds.length > 0 &&
data.notificationTopic
) {
// 通知
}
通知メッセージも件数によって変えます。
const message =
newIds.length === 1
? "新しいメッセージがあります"
: `新しいメッセージが${newIds.length}件あります`;
これによって、
新しいメッセージがあります
または、
新しいメッセージが3件あります
のような通知になります。
17. 通知サービスへPOSTする
スマートフォンへの通知には、HTTPリクエストを受け取ってPush通知へ変換してくれるサービスを利用しました。
例えば、
const notifyResponse = await fetch(
`https://ntfy.sh/${encodeURIComponent(
data.notificationTopic
)}`,
{
method: "POST",
body: message
}
);
という形です。
流れとしては、
Chrome拡張
↓
HTTP POST
↓
通知サービス
↓
スマートフォン
となります。
つまりChrome拡張側でスマートフォン用のPush通知システムを一から構築しているわけではありません。
新着検出まではChrome拡張、Push通知部分は通知サービス
と役割を分けています。
18. encodeURIComponent()を使う理由
通知先をURLの一部として使用するため、
encodeURIComponent(
data.notificationTopic
)
としています。
URLにそのまま埋め込むと問題になる可能性のある文字を、安全にURLへ含められる形式に変換するためです。
19. 通知後に現在の状態を保存する
最後に、
await chrome.storage.local.set({
lastUnreadIds: currentIds
});
として、今回取得したIDを保存します。
すると、
今回のcurrentIds
↓
保存
↓
次回のpreviousIds
になります。
これを繰り返すことで、新しく追加されたIDだけを検出できます。
20. 設定画面も作る
通知先の設定値を background.js に直接書くのは避けたかったので、設定画面を用意しました。
manifest.json では、
"options_page": "options.html"
と指定します。
これによってChrome拡張の設定画面として options.html を開けるようになります。
簡単なHTMLは次のような形です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Notifier 設定</title>
</head>
<body>
<h2>Notifier 設定</h2>
<label for="topic">
通知トピック
</label>
<input
id="topic"
type="password"
placeholder="通知先のトピック名"
>
<button id="save">
保存
</button>
<p id="status"></p>
<script src="options.js"></script>
</body>
</html>
入力欄を、
type="password"
にすることで、設定値が画面上にそのまま表示されないようにしています。
設定画面で入力した値は options.js から chrome.storage.local に保存し、background.js が読み込む構成にできます。
options.html
↓
ユーザーが設定
↓
options.js
↓
chrome.storage.local
↑
background.js
これによって、
設定画面とバックグラウンド処理を分離
できます。
21. 全体を整理する
今回の処理を最初から並べると、次のようになります。
① Chrome拡張を起動
↓
② chrome.alarmsを登録
↓
③ 一定間隔でイベント発生
↓
④ Service WorkerがcheckUnread()を実行
↓
⑤ fetch()で対象ページを取得
↓
⑥ HTMLから未読IDを抽出
↓
⑦ chrome.storage.localから
前回のIDを取得
↓
⑧ currentIdsとpreviousIdsを比較
↓
⑨ 新しいIDを発見
↓
⑩ 通知サービスへHTTP POST
↓
⑪ スマートフォンに通知
↓
⑫ currentIdsを保存
↓
⑬ 次回の確認へ
22. Chrome拡張の構成も理解できた
今回作る前は、Chrome拡張というと、
Webページの見た目を変えるJavaScript
くらいのイメージがありました。
しかし実際には、
manifest.json
↓
拡張機能全体の設定
Service Worker
↓
バックグラウンド処理
chrome.alarms
↓
定期実行
chrome.storage
↓
状態・設定の保存
options.html
↓
設定UI
のように役割を分けて作ることができます。
通常のWebアプリケーションとは少し違った構成になっているのが面白いところでした。
23. 作って理解できたこと
今回特に勉強になったのは、
「通知を実現したい」という要件から、必ずしもPush機能そのものを対象システムへ追加する必要はない
ということです。
今回のように、
定期取得
+
HTML解析
+
状態保存
+
差分検出
+
外部通知
を組み合わせる方法もあります。
また、新着判定についても、
未読が何件あるか
ではなく、
前回と比べて
新しいIDが存在するか
を見ることで、より目的に合った判定ができました。
まとめ
今回作った仕組みは、
Chrome Extension
↓
chrome.alarms
↓
Service Worker
↓
fetch()
↓
HTML解析
↓
未読ID抽出
↓
chrome.storage.local
↓
差分検出
↓
HTTP POST
↓
Push通知
という構成になりました。
今回触れた技術は、
- Chrome Extensions
- Manifest V3
- Service Worker
chrome.alarmschrome.storage.local- Fetch API
- HTTP GET / POST
- HTML解析
- 正規表現
Setfilter()- ポーリング
- 状態管理
- 差分検出
です。
一つひとつはそれほど複雑な処理ではありません。
しかし、
「取得 → 解析 → 保存 → 比較 → 通知」
と役割を分けて組み合わせることで、既存のWebシステムを変更せずに新着通知の仕組みを作ることができました。
単に通知を作っただけでなく、
ブラウザ拡張がどのようにバックグラウンドで処理を実行し、状態を保持し、外部サービスとHTTP通信するのか
を理解する良い題材になりました。