今回は iOSDC の影響があるのかないのか、とりあえず iOS 寄りの勉強会としては非日常な日程なので、そういえばちょうど前回の話題にのぼって面白かった ヌメロニム
が世を席巻したらどうなるんだろう — みたいに不意に思った都合で、今回は余興的な回として、Web ページ内に出てくる単語をヌメロニムで表示する S4i 拡張
⋯ もとい Safari 拡張
を作ってみたら楽しいかもしれない?みたいに思ったりしたので、それを体当たり的に実施してみる回にしてみます。完成に至るかはわからないですけれど、よろしくお願いしますね。
————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #293
00:00 開始 00:14 ヌメロニムという存在 01:45 略語はみだりに使わないこと 02:48 表記全部をヌメロニムにしたらどうなるだろう 03:37 Safari Extension とは 05:22 Safari Extension を作ってみよう 07:33 Safari Extension はネイティブ実装も可能 09:08 標準的なテスト機構は使わなそう 10:02 Safari Web Extension の雛形 11:24 開発段階の Safari Extension 利用は明示的な許可が必要 13:03 Safari Web Extension のプロジェクト構成 14:48 ネイティブコードでエラーを扱う際の注意 15:53 Safari Web Extension のマニフェスト 16:34 2種類のスクリプト配置場所 18:53 バックグラウンドで動けるスクリプトは今は1つだけ 22:08 その他のファイルの用途 24:17 制作にあたり、どこに手を加えていこう 26:13 今回は Wikipedia ページで使える機能にしよう 28:08 実際に Wikipedia 上で動かしてみる 30:31 ボタンを押されたら何かをしてみる 33:07 バックグラウンドスクリプトの出力確認方法 35:48 フォアグラウンドにメッセージを投げる 37:52 フォアグラウンドでは DOM など普通に利用可能 37:54 Web ページからテキストを探していく 41:15 取得したテキストをヌメロニム化する 44:56 クロージングと次回の展望 —————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #293
では始めていきますね。今日はちょっと変わったことをしてみようと思います。前回の勉強会の中で、APIデザインガイドラインの話題が出たんですが、Swiftで使うわけではないので関係ないかもしれません。しかし、名前の付け方については現代では分かりやすい名前を付けるのが主流になっている中で、「i18n」(internationalization)のようなヌメロニムは逆行しているとも感じます。
1980年代の頃はメモリの都合などで名前を省略することが普通だったようです。そのようなセンスが良いと評価されていたこともあります。たとえば、「i18n」の「i」で始まり「n」で終わる間に18文字あるという具合ですね。実際に知らなければ理解できない略語です。APIデザインガイドラインでも、略語は可能な限り使わない方が良いとされています。意味を伝えるのに支障がある場合を除いては、略語や専門用語は避けることが推奨されます。
プログラムの中でこのような名前付けをすることは少ないと思いますが、既に広く認知されているものはそう呼ぶこともあるでしょう。とはいえ、ちょっと微妙だとも思います。そこで遊び心で、ウェブページ全体の英単語をすべてヌメロニムに置き換えてみたらどうなるか試してみたくなりました。
Safariエクステンションを使ってそれを実現しようと思います。Safariエクステンションとは、広告ブロックのエクステンションなどのように、いろいろな機能をSafariに追加するための拡張機能です。これを作るにはMacOSアプリの作成が必要です。
まず、Xcodeを起動して新規プロジェクトを作成します。ここで、iOSではなくMacOSを選択します。Safariエクステンションアプリのテンプレートも用意されています。プロダクト名を「ヌメロニムサ」にして先に進みます。
Safariエクステンションには「Safari Web エクステンション」と「Safari App エクステンション」の2種類があります。昔はSafari App エクステンションしかなく、これはApple独自のフレームワークを使っていました。最近ではGoogle Chromeとの互換性を考慮したSafari Web エクステンションも追加されました。Google Chrome拡張の仕様を使ってSafariエクステンションを作ることができるようになっているわけです。
さて、実際に作成を進めていきますが、プロダクト名の通りにプロジェクトを作成して、Safariエクステンションを追加していきます。この過程でホストアプリと拡張機能を作成し、ホストアプリをインストールすることで拡張機能が自動で登録される流れになります。今回はMacOS用に作成しますが、うまくいけばiOS用にも応用できるかもしれません。
では、Xcodeでプロジェクト作成を始めていきましょう。 Safari Appエクステンションはネイティブの動作がかなりしやすい仕組みになっています。しかし、Safari Webエクステンションではメッセージを投げて、ホストとエクステンションがやり取りする必要があります。その際、メッセージとして投げられるものが限られており、主に文字列やJSONです。したがって、若干の手間がかかります。
エクステンションを開発する際にはSafari Appエクステンションが便利かもしれませんが、多くの場所で使えるのはSafari Webエクステンションです。グローバルスタンダードで、他のブラウザ(例えばGoogle Chrome)でも動作するため、勉強しがいがあります。今回はSafari Webエクステンションを使用しました。
テストについてですが、基本的にホストアプリのテストは不要です。エクステンションが動作しないとホストアプリとは関係がないからです。エクステンション自体のテストは必要ですが、Xcodeのユニットテストは効かないので無理に使う必要は感じません。不要なら外しておいても問題ないかと思います。
プロジェクトを作成すると、ホストアプリとSafariエクステンションの2つのターゲットが自動で作られます。ホストアプリはビューコントローラーにビューが載っていて、基本的にはリソースで定義されたHTMLが表示されるようになっています。実際に動かしてみると、ホストアプリは非常にシンプルで、基本的には触る必要がないでしょう。
Safariの設定画面でエクステンションを登録する際には、許可を与える必要があります。未署名の機能拡張を許可する設定をオンにすることで、エクステンションが使用可能になります。このステップはプライバシーの観点から重要です。
エクステンションのテストについては、基本的にはホストアプリのレイヤーではなく、拡張機能そのものをテストします。エクステンションのコードを見ると、SafariServices
がインポートされ、SafariWebExtensionHandler
が実装されています。クライアントからメッセージが投げられてくると、このハンドラーがネイティブコードを実行して対応します。メッセージを受け取って処理をした後に、NSExtensionItem
のインスタンスを作成し、結果を揃えてコンテキストに対して処理完了を通知します。
最後に、エクステンションはクライアント側からメッセージが投げられたときに動作しますので、主導権はクライアント側にあります。これに基づいて、実際にコードを触りながら進めていく形になるかと思います。 まず、サーバーに関してですが、今回のセッションではネイティブは使用しない予定です。しかし、ネイティブを使用するときは、コンプリートがあるので失敗もあります。例えば、検証リクエストなどもありますが、エラーも渡せます。ただし、これらはローレベルなエラーとして扱われるため、JavaScriptの実行をクライアントが強制的に終了させてしまいます。エラーメッセージもバグなのか分かりませんが、きちんと渡したメッセージが表示されず、あまり使い物になりません。
実際にネイティブコードを実行するときには、エラーが発生してもコンプリートリクエストとして処理し、リクエスト自体を終了させるといった感じで作ると、自然でスムーズに動作します。もしネイティブコードを書くようになった際には、これを思い出してみてください。
エクステンションの方には、ツイストコード的にはハンドラーだけです。しかし、他にマニフェストというものがあり、エクステンションの情報、名前、自動で埋め込まれるプロパティリストなどが含まれます。Xcodeがそれらを自動で処理してくれます。さらに、ディスクリプションやアイコンも含まれます。設定画面に表示されるアイコンも登録可能です。
Safariエクステンションには、バックグラウンドで動作するJavaScriptと、フォアグラウンドで動作するJavaScriptの二種類があります。例えば、ボタンを押してアクションを起こすといった場合にはバックグラウンドが入り口になります。初めにドキュメントがロードされたときにはフォアグラウンドがエントリーポイントになります。バックグラウンドから始める方がわかりやすいかもしれません。
バックグラウンドからクライアントやネイティブにメッセージを投げることができるため、複雑なエクステンションを作る際には、バックグラウンドを起点にして行ったり来たりするのが良い感じです。コンテンツスクリプトでは、どのJavaScriptファイルをどのURLのときにロードするかを複数設定できます。これによって、特定のサイトでのみスクリプトをロードするなど、適切な場所でのみコードを実行できます。
マニフェストのバージョン3からは、バックグラウンドファイルを一つしか書けなくなりました。複数のバックグラウンドファイルが書いてあるコードを見つけたら、マニフェストバージョンが古いと思ってください。アクションアイコンも独自のものを設定でき、ボタンを押した際にHTMLのポップアップが表示されます。
パーミッションについては、エクステンションがどんな機能を持つのか、ユーザーに知らせる必要があります。例えば、「ネイティブメッセージング」が必要なら、そのパーミッションを入れる必要があります。「タブ」などのパーミッションもそうです。必要性を感じたら適宜調べて、適切に設定してください。 まずはテンプレートには入っていない状態ですが、必要に応じて追加していく形で進めます。パワー4などの詳細は不明ですが、とりあえず入れずに進めてみましょう。今回は、ボタンを押してチェックボタンからメッセージを投げる方法を試してみますが、説明がまだ不足しているところがあります。
まず、バックグラウンドについてです。ブラウザのランタイムでイベントリスナーを登録し、バックグラウンドからフォアグラウンドへメッセージを投げる際に使います。他にもいろいろな機能をバックグラウンドで処理することになります。
次に、マニフェスト内でクライアント側にロードするスクリプト、つまり普通のAJAXやDynamic HTMLのようなクライアントサイドJavaScriptです。これはWeb開発に慣れた人にはなじみ深いものでしょう。そういったスクリプトを使いながらメッセージを受け取る処理をします。
ポップアップは、ボタンを押すと出てくるHTMLです。マニフェストで表示するように設定されています。スタイルシートやポップアップ内で動くJavaScriptもあり、これを通じてメッセージを投げることができるはずです。ポップアップについてはあまり経験がないので、各自調べてもらえればと思います。
エクステンションはサンドボックス内で動かなければならず、これは派手なことができないという制約があります。ネイティブコードでも複雑な操作は難しいです。例えばカレンダーにウェブ情報を登録しようとするとカレンダーのパーミッションが素直に取れないので、Text PC Serviceなど別の方法を用いる必要があります。ネイティブでもできることに制約が多いため、注意が必要です。
特にインフォプリストの設定は問題ないようです。NSエクステンションあたりが注目すべきポイントですが、大きな問題はなさそうです。マニフェストを基本にしていくことで、コンテンツをロードするURLを設定します。今回は全てのWebページで実行するか、Wikipediaに限定するかを選ぶ必要があります。日本のWikipediaも外国のWikipediaも同じく設定が必要です。
最後に、URLがWikipediaであれば何でも良いという形で設定します。たとえば、URLの頭に何かがついている可能性を考慮して以下のように設定します。
{
"matches": ["*://*.wikipedia.org/*"]
}
この設定でOKですね。それでは、実際にエクステンションを作成してみましょう。 あとはこのマニフェストについてですが、JSONで記載します。言語によってはリストの最後の要素にカンマを書ける場合がありますが、JSONでは許されていないようです。昔はこの制約が好きだったのですが、今はカンマが許される方が好みになっていて、カンマがないと面倒だと感じることもあります。
さて、アクションについてですが、デフォルトではポップアップになっていますが、ボタンを押すだけで動作するようにしたいので、デフォルトのポップアップは不要です。また、ついでにタイトルも設定したいと考えています。タイトル名として「メロニウム」と書いてみましょう。これでビルドをかけてみます。
マニフェストをいじった場合、ビルドが通るかどうかを確認することが重要です。エラーメッセージが出たり、リストから消えたりすることがあるので、確認を怠らないようにしましょう。
今回のビルドでは、特に問題なく三角マークが付きました。サイト jaa.wikipedia.org
へのアクセスが行われようとしています。ここで許可すれば、色が変わってボタンが押せるようになり、タイトルも表示されます。
開発する上で便利なのは、ウェブJavaScriptコンソールを表示することです。これにより、スクリプトのミスに気付きやすくなります。色々なメッセージが出ていますが、大抵の場合は無視しても問題ありません。手元のメッセージをコンソールに捨ててリロードすると、再びメッセージが表示されることもあります。
次に、ボタンを押したときの処理について考えてみます。まず、バックグラウンドで処理を行う必要があります。ボタンが押されたとき、フォアグラウンドではなくバックグラウンドで処理が動くと想定しています。ブラウザの拡張機能の中で、browser.browserAction.onClicked.addListener
のように関数を登録して、その中で処理を行います。
ここで、async
関数を使うとコードが書きやすくなります。パラメータとしてタブが渡ってくるので、それを受け取り、例えばコンソールにメッセージを出力してみましょう。これでビルドを行うと、ビルド完了後にはエクステンションが入れ替わります。
ボタンを押しても動作しない場合、何か間違った可能性があります。コードを再確認し、console.info
または console.log
を試してみてください。バックグラウンドの設定が正しく行われているかも確認します。マニフェストにバックグラウンドの設定を追加すると、うまく動作するかもしれません。 バックグラウンドとアプリやメッセージの上にプログラムを入れてみましょう。バックグラウンドの場合は、別のコンソールで確認する必要があったかな。コンソールで確認する場合はMacOSのコンソールで行う必要があります。バックグラウンドケースを選べるのかどうか、私はちょっと忘れかけているのですが、確かにそうだったかもしれません。普段は見れないので、フォアグラウンドに投げていたかもしれません。では、少しそちらでやってみましょう。バックグラウンドのテスト検証は意外と難しいです。
では、メッセージを送るためにフォアグラウンドに投げましょう。今回の検証が教習に全然終わらない可能性がありますが、雰囲気だけでも理解してもらえればと思います。次に、Zoomで教えてもらった内容を参考にしましょう。ウェブ拡張の中にバックグラウンドコンテンツなんてあるんですね。それは全然知らなかったかも知れません。せっかくなので試してみましょう。ブラウザの開発者サービスワーカーエミュレーターを使いましょう。
読み込みの状態を確認し、スマートフォンでどのように動作しているか確認しましょう。ネットワークやログ、コンソールが表示されるとデバッグがしやすいです。このようにバックグラウンドボタンを押すと、いくつかのパスが表示されます。この時に、バックグラウンドからフォアグラウンドにメッセージを送りたいのです。ブラウザのタブに対してメッセージを送信し、受け取ったタブのIDと共に投げるメッセージを設定します。
例えば、メッセージをJSON形式で送ります。コード例としては以下のようになります:
let message = ["key": "value"]
browser.runtime.sendMessage(tabId, message)
ちなみに sendNativeMessage
というメソッドもあり、ネイティブ側にメッセージを送ることができます。ただし、ここでは sendMessage
を使います。この sendMessage
は非同期で動作するため、この関数をasyncにしておき、await
を使ってレスポンスを受け取ることができます。簡単な例として以下のようになります。
async function sendMessageToContent() {
let response = await browser.runtime.sendMessage(tabId, message);
console.log(response);
}
このように、メッセージ送信が行われ、トリガーが発火します。これでメッセージが受信されるので、後はドキュメントオブジェクトモデル(DOM)を使って様々な操作が可能です。
次に、DOMから特定の要素を抽出して操作する例を示します。例えば、ノードを取り出し、テキストコンテンツを取得する場合は以下のようにします。
function traverseDOM(node) {
if (node.nodeType === Node.TEXT_NODE) {
console.log(node.textContent);
} else {
for (let child of node.childNodes) {
traverseDOM(child);
}
}
}
この関数を使って、ドキュメント全体を走査することができます。例えば、以下のようにして使います。
traverseDOM(document.body);
これで、ドキュメント内の全てのテキストノードの内容をコンソールに出力することができます。
このように、プログラムをバックグラウンドやフォアグラウンドに投げて操作する手順を理解して実装していきます。どのように動作するかを確認しながら進めていけば、効率的に開発を進めることができるでしょう。 とりあえず進めてみましょう。もし「押す」と動くようなら、テキストの変換がうまくいっているか確かめてみます。このテキストをヌメロニウム化するわけです。具体的には、たとえば、以下のようにします。
const text = node.textContent;
これを置き換えればいいのかな。replace
メソッドを使うわけですね。JavaScriptのreplace
メソッドでは、文字列の中のパターンを置き換えることができます。
ヌメロニウムを置き換える場合、一つの単語の境界から始まり、それが複数回続く形を想定します。プロパティとして正規表現を使い、例えば A
から Z
の英字が一つ以上続く部分を対象にします。
const pattern = /\\b[A-Za-z]+\\b/g;
このようなパターンです。この正規表現によって、一つの単語ごとにマッチします。
一度マッチングだけを確認し、もしマッチした場合には以下のようにして取り出します。
const matches = text.match(pattern);
if (matches) {
const first = matches[0];
const middle = matches.slice(1, -1).join(' ');
const last = matches[matches.length - 1];
console.log(`${first} ... ${middle} ... ${last}`);
}
これで各部分を取り出してコンソールに出力できます。エラーが起きた場合、どこで問題が発生したのかを確認するために、途中で console.log
を入れてデバッグします。
実行した結果が期待通りであれば、次に進む準備ができたことになります。実際には、これを更に洗練して動作するようにします。
時間が経ってしまったので、今回はここまでにします。次回はこの続きからやってみましょう。おそらくすぐに完成すると思います。
これで終わりにしますね。お疲れ様でした。ありがとうございました。