風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

ブラウザ拡張機能用に background で ZIP 化するためのライブラリを試作(Chrome拡張機能/Firefox Quantum WebExtensions 用)


前書き

WebExtensions について調べていると、Promise を使用して云々……という記述が出てきて、今さらながらに Promise というものの存在を知りました(ヲイ。
慣れれば使い勝手が良さそうなので、練習を兼ねて、ブラウザ拡張機能の background で ZIP 化することが出来るようなライブラリを試作してみました。
github.com
習作なので、いつも以上に動作保証できません。ご利用は計画的に(汗)。

概要

content_scripts に対し、ZipRequest クラスを提供します。
ZipRequest#open()/file()/generate()/close() という一連の関数にて、background に対してメッセージを送ることで ZIP 化に関する指示を出し、background からの応答メッセージで結果を受け取り、content_scrips に返します。
background での ZIP 化には、JSZip を使用しています。

比較する意味もあって、content_scripts 用には Promise を使ったもの(Promise版・zip_request.js) と、使わないもの(コールバック版・zip_request_legacy.js)とがあります。
background 用のもの(zip_worker.js)は共通です。

サンプル

はてなブログ("*://*.hatenablog.com/*")を開くと、画像を適当な数選んで ZIP 化・ダウンロードする、という迷惑な(汗)サンプルコード(抜粋)です。
サンプルソースコード全文は、こちらをご覧ください

並列処理

複数のファイルを同時並行で取得しながらアーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open();

files.forEach( function ( file ) {
    var url = file.src || file.href,
        filename = get_filename( url );
    
    console.log( '[start]', url, filename );
    
    zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    }, function ( result ) {
        console.log( '[result]', url, filename, result );
    } );
} );

zip_request.generate( 'blob', function ( response ) {
    zip_request.close();
    
    // 以下、Aタグのdownload 属性を使ったダウンロード処理
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

await Promise.all(
    files.map( async ( file ) => {
        let result,
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        result = await zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        } )
        .catch( result => { return result } ); // Promise.all() を停止させないための対策
        
        console.log( '[result]', url, filename, result );
        
        return result;
    } )
);

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

コールバック版のライブラリ内で多少工夫をしていることもあり、並列処理に関しては、一見したところそれ程違いは無いかもしれません。
コールバック版では、例えば file() に対応する応答が background からまだ来ない状態で generate() が呼ばれたとしても、全てのファイルについて結果が返るのを待って background に要求を出すようにしているため、上記の書き方が可能。ただし、close() については、generate() のコールバック後に呼び出す必要あり。

直列処理

ファイルを一つずつ順番に取得しながら(逐次)アーカイブする処理です。
コールバック版(zip_request_legacy.js)の場合

'use strict';

( function () {

var zip_request = new ZipRequest(),
// (中略)

zip_request.open( function ( result ) {
    var file_index = 0;
    
    function zip_files() {
        if ( files.length <= file_index ) {
            zip_request.generate( 'blob', function ( response ) {
                zip_request.close();
                
                // 以下、Aタグのdownload 属性を使ったダウンロード処理                                
            } );
            return;
        }
        
        var file = files[ file_index ++ ],
            url = file.src || file.href,
            filename = get_filename( url );
        
        console.log( '[start]', url, filename );
        
        zip_request.file( {
            url : url,
            filename : filename,
            zip_options : {
                date : new Date( '2017-01-01' )
            }
        }, function ( result ) {
            console.log( '[result]', url, filename, result );
            
            zip_files();
        } );
    }
    
    zip_files();
} );

} )();

Promise版(zip_request.js)の場合

'use strict';

( async function () {

let zip_request = new ZipRequest(),
// (中略)

await zip_request.open();

for ( let file of files ) {
    // files.map( async ( file ) => { ... } ) は使えないことに注意
    // ※ map() では、コールバック関数の戻り値が Promise object になり、直列処理されない
    let url = file.src || file.href,
        filename = get_filename( url ),
        result;
    
    console.log( '[start]', url, filename );
    
    result = await zip_request.file( {
        url : url,
        filename : filename,
        zip_options : {
            date : new Date( '2017-01-01' )
        }
    } )
    .catch( result => { return result } ); // エラーで停止させないための対策
    
    console.log( '[result]', url, filename, result );
}

let response,
    download_link;

response = await zip_request.generate( 'blob' );

await zip_request.close();

// 以下、Aタグのdownload 属性を使ったダウンロード処理

} )();

こちらは、Promise 版のメリットが出ていると思います。
コールバック版は処理の流れが一見解りにくいのに対し、Promise 版では上から下への自然な流れで解りやすくなっています。

はまった点など

  • Promise.all() は、並列実行中の Promise オブジェクトが一つでもエラーになると異常終了してしまう(catchされてしまう)ため、中断したくない場合、それぞれの Promise で reject() ではなく resolve() を呼び、戻り値によって判別するようにする
  • Array#map() 等のコールバック処理を持つものは、直列処理では使用できない(コールバックの結果が Promise オブジェクトで返されるため)
  • Promise の resolve() や reject() は、呼んだ後も続きが実行される(実行されないようにするには、直後に return が必要)
  • background における、browser/chrome.runtime.onMessage.addListener() のコールバック関数内で、非同期の処理を呼んでから sendResponse()を返す場合、コールバック関数の戻り値に true を設定する必要がある(chrome.runtime - Google Chrome
  • content_scripts と background 間のやり取り(sendMessage()/sendResponse())では、JSON で基本的にはシリアライズ可能なオブジェクトしか渡せない……ところが、渡せるオブジェクトの種類に、ブラウザ間で差異がある(関数オブジェクトは Firefox で NG、Blob が渡せるのは Firefox のみ、等)
  • background で URL.createObjectURL() により得られた Blob URL を content_scripts に送ると、Chrome では download 属性付き A タグでダウンロード可能なのに対し、Firefox や MS-Edge では不可(Firefoxについては、なぜか Blob がそのまま content_scripts に送れるため、そちらで Blob URL に変換することで対応している)
  • MS-Edge では、作成した ZIP をダウンロードさせる術が見つからない

Promise/async/awaitやclassの書き方でもっとはまると思っていたが、これらはそれ程でもなかった代わりに、拡張機能の仕様やブラウザ間の細かい差異の方が難解。

Chrome 拡張機能を Microsoft Edge の拡張機能にも対応させようとして挫折した件


前書き

承前。
furyu.hatenablog.com
furyu.hatenablog.com
せっかくだから、Firefox Quantum に対応できた拡張機能を、MS-Edge でも動かしたいと欲張ったのだが……見事に挫折した(哀)。

修正方法等

manifest.json

「マニフェスト解析エラー: 'author' フィールドが見つかりません。」と出たので、author フィールドを追加。

browser.* ネームスペース対応

WebExtensions 用に、予め対応してあった。

PATH の問題

オプション画面にて、chrome.browserAction.setIcon( { path : icon_path } ) を使ってアイコンを変化させる処理を書いてあったところ、MS-Edge ではアイコンが正常に表示されなくなった。
調べてみたところ、どうも、アイコンの PATH の指定が、

  • Chrome 拡張機能 / Firefox WebExtensions → 呼び出し元の HTML(オプション画面)からの相対パス
  • MS-Edge 拡張機能 → manifest.json のあるフォルダからの相対パス

のように違いがあるようで、場合分けが必要だった。

未解決問題

以下は、Microsoft Edge 41.16299.15.0 / Microsoft EdgeHTML 16.16299 で発生

fetch() や XMLHttpRequest で ArrayBuffer を使用すると未定義のエラーが発生

ユーザーコンテキスト(content_scripts)で fetch() を使用すると、

  SCRIPT65535: 未定義のエラーです。

どうやら、同様の現象が他でも見られるみたい。
Fetch API in Extension SCRIPT65535 error - Microsoft Edge Development
https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/attachment/14192157/8236868/

同様に、XMLHttpRequest で、xhr.responseType を 'arraybuffer' に設定した場合に、xhr.response を参照しようとするとエラーとなってしまう。

  SCRIPT65535: 未定義のエラーです。

いったん、responseType を 'blob' にして受けてやると response は参照できるので、これを ArrayBuffer 化することは可能だった。
ただ、その場合でも、JSZip の アーカイブ処理(JSZip#generateAsync)で失敗してしまう。

よって、MS-Edge の拡張機能では、Twitter 原寸びゅーや Twitter メディアダウンローダで使用している JSZip による ZIP 化が出来ず、ZIP ダウンロード機能が無効化してしまう。
特に、メディアダウンローダの方はほぼすべての機能が使えなくなってしまう……(哀)。

バックグラウンドでのダウンロードができない

バックグラウンドコンテキスト(background)内では、現状、ファイルのダウンロードができない模様。

Overarching issues
The following known issues span across the extension platform and will be fixed in the near future:
: (中略)
・Triggering a download via a hidden anchor tag will fail from background scripts. This should be done from an extension page instead.

Extensions - Supported APIs - Microsoft Edge Development | Microsoft Docs
  • browser.downloads API は存在しない(2017/04/11現在のロードマップで、"Under consideration" になっている
  • a タグの download 属性を使ったダウンロードも不可(click()してもダウンロードされない)
  • navigator.msSaveOrOpenBlob() 等も使えない
    SCRIPT16386: インターフェイスがサポートされていません
  • tabs.create() で新たにタブを開いてそちらでやろうとしても、background から開いた場合には上記の不具合が継承されてしまう

ということで、八方ふさがり。
Twitter 原寸びゅーのコンテキストメニューからの「原寸画像を保存」が実現出来ない。
background から content_scripts 宛に sendMessage() して、そちらで処理ならできるかも?でも面倒くさそう……。

余談

というわけで、自作拡張機能を MS-Edge に対応させることは、少なくとも現状では諦めた。
まぁ、出来た拡張機能を公式に公開するすべは今のところ無さそうだし良いけど。

それにしても MS-Edge は、拡張機能を開発しやすいとはお世辞にも言えないな……。

  • 拡張機能関連のメニューが自動的に隠れてしまうため、アクセスしずらい
    "about:flags"みたいに、タブに独立して出す方法はある?
  • 開発者ツールでデバッグしていると固まりやすく、またかなりの確率でブラウザごと落ちてしまう
  • 上記の fetch() のように、拡張機能のコンテキストでのみ動作しない不具合が多い

それでもめげず(?)、便利な拡張機能を公開されている限られた方々には頭が下がる訳だが。

少なくとも自分は、当面 MS-Edge では開発したくない。
幸い、MS-Edge にも Tampermonkey があるので、当方の拡張機能を使いたい方は、Tampermonkey を入れて、ユーザースクリプト版をお使いいただきたい。
とか言っていたら、MS-Edge 版の Tampermonkey が TweetDeck では異常が発生し、ユーザースクリプトを動かせない不具合を見つけてしまったり……。

【覚書】Firefox アドオン (WebExtensions) を AMO で公開した際の手順


前書き

Chrome 拡張機能を Firefox Quantum (WebExtentions) にも対応させたので、せっかくだしと AMO にも登録してみることに。
その手順を覚え書きとして記しておく。

手順

アドオン開発者センターにユーザー登録

Firefoxアカウントを未所持の場合、アドオン開発者センターにて、ユーザー登録を行う。

f:id:furyu-tei:20171117232228p:plain
f:id:furyu-tei:20171117232239p:plain

自作アドオンのアップロード準備

当該アドオンの全ファイルを一つの ZIP アーカイブにまとめる(アドオンのパッケージ化)。
この際、manifest.json 及び同一フォルダ内のファイルが一番上の階層になるようにアーカイブすること。

あなたのWebExtensionは次のものを格納したディレクトリです。manifest.jsonとその他の必要なファイル-スクリプト、アイコン、HTMLドキュメント等。あなたはこれらを1つにまとめたzipファイルをAMOにアップロードする必要があります。

ひとつトリッキーなこととして、ZIPファイルはWebExtensionを構成するファイルを含み、ディレクトリに入ってはいません。

WebExtensionを公開する - Mozilla | MDN

At this point your extension will consist of a directory containing a manifest.json and any other files it needs - scripts, icons, HTML documents, and so on. You'll need to zip these into a single file for uploading to AMO.

One trick is that the ZIP file must be a ZIP of the extension's files themselves, not of the containing directory.

Publishing your extension - Mozilla | MDN

OKな例

manifest.json
html/options.html
js/background.js
 :

NGな例

src/manifest.json
src/html/options.html
src/js/background.js
 :

自分の場合、src/manifest.json のように src 以下に全て置いていたため、src フォルダを含む形で ZIP を作っていたところ(実際、Chrome 拡張機能の場合はこれでも問題なくアップロードできる)、AMOにアップロードしようとしたらエラーが出てしまった。

新しいアドオンの登録

アドオン開発者センター

f:id:furyu-tei:20171117232245p:plain
[新しいアドオンの登録]ボタンを押して、登録を開始する。

f:id:furyu-tei:20171117232252p:plain
配布手段を選択し、[続ける]を押す。
特に理由がなければ、「◉当サイト上で……」で良いと思う。

f:id:furyu-tei:20171117232258p:plain

f:id:furyu-tei:20171117232304p:plain
前の手順で準備した パッケージ(ZIP ファイル)をアップロードすると、アドオンの検証が行われ、結果が表示される。

f:id:furyu-tei:20171117232309p:plain
「検証レポートの詳細を見る」をクリックすると、詳細な検証レポートが開く。

f:id:furyu-tei:20171117232319p:plain
これをチェックし、問題があれば修正する。
eval等を使用しているといった警告はあったが、自作以外のライブラリなこともあり、対応はせずにそのまま。エラーでなければ継続できるらしい。

f:id:furyu-tei:20171117232325p:plain

f:id:furyu-tei:20171117232332p:plain
「アドオンの説明」画面に必要事項を記入・入力し(名前と概要は manifest.json に記載のものが転記されている)、[バージョンを登録]を押して登録する。

f:id:furyu-tei:20171117232338p:plain
登録完了後、[掲載ページを管理]を押すと、

f:id:furyu-tei:20171117232347p:plain
アドオンに関する各種情報を確認・変更できるので、必要に応じて追記や修正を行う。

余談

「『AMO』ってなんだろう、サイト名としては『Firefox Add-ons』か『Add-ons for Firefox』かだよね…」とか思っていたのだが、これ、ドメイン名である"addons.mozilla.org" の頭文字を取ったものだったのか……。

Chrome 拡張機能を Firefox Quantum の WebExtensions にも対応させた例


前書き

とりあえず、自作の Chrome 拡張機能のうち、二つを Firefox Quantum (WebExtentions) にも対応させてみた。
Twitter 原寸びゅー – Firefox 向けアドオン
Twitter メディアダウンローダ – Firefox 向けアドオン
この際に修正した箇所を中心に、覚え書きを記しておく。

修正方法等

manifest.json

最初、次のような警告が出ていた。

Reading manifest: Error processing background.persistent: Event pages are not currently supported. This will run as a persistent background page.
Reading manifest: Error processing options_page: An unexpected property was found in the WebExtension manifest.

上の方は、

    "background": {
        "scripts" : [ "js/background.js" ],
        "persistent" : true
    },

の "persistent" オプションに関するもので、現状は Firefox ではサポートされておらず、無視される。動作的には特に問題ないので、このまま。

下の方は、

   "options_page" : "html/options.html",

に関するもので、エラーとなっているが、このままでもオプションページは開くことが出来る。
現在は、

    "options_ui" : {
        "page" : "html/options.html"
    },

という書き方になっているため、こちらに合わせておく。

jQuery

jQuery 読み込み時に、

  Content Security Policy: ページの設定により次のリソースの読み込みをブロックしました: self (“script-src moz-extension://7959b7cd-9bbb-443f-8362-df4673e654f6 'unsafe-eval'”) Source: onfocusin attribute on DIV element.

のようなエラーが出ていた。
どうも、v1.x 系を使用していると発生する模様(使用していたのは、v1.11.3)。
参考:Web Extension: Cannot load jQuery into background page, due to CSP - Development - Mozilla Discourse
これは、v2.x 系にて修正されているらしい。v2.2.4 にしたところ、発生しなくなった。

chrome.app.* は使わない

オプション画面の、拡張機能バージョンを取得する処理でエラーが発生していた。
原因は、chrome.app.getDetails().version を使用していたこと。
代わりに chrome.runtime.getManifest().version を使うことで解決した。
参考: Where is chrome.app officially documented? - Stack Overflow

browser.* ネームスペース対応

これまで chrome.* ネームスペースを使用していたが、今後の流れとしては browser.* ネームスペースを使う方が良さそう。
参考: Chrome incompatibilities - Mozilla | MDN

といいつつ、既存のものを全部書き換えるのは煩わしいので、各スクリプトの先頭に

window.chrome = ( ( typeof browser != 'undefined' ) && browser.runtime ) ? browser : chrome;

のような一行を入れておくことでお茶を濁してある。

なお、Firefox Quantum の WebExtensions では、browser と chrome の両方が定義されているが、コンテキストによって微妙に見え方が違う。

バックグラウンドコンテキスト(background)

chrome window.chrome browser window.browser
Chrome 拡張機能 × ×
Firefox WebExtensions
MS-Edge 拡張機能 × ×

ユーザーコンテキスト(content_scripts)

chrome window.chrome browser window.browser
Chrome 拡張機能 × ×
Firefox WebExtensions × ×
MS-Edge 拡張機能 × ×

独り言

とりあえず、Twitter 原寸びゅーTwitter メディアダウンローダに関しては、以上のような修正を行うことで、無事、Firefox Quantum でも動作するようになり、かつ、Google Chrome 上でもこれまで通り動作しているので、共用化できたと言える。
もともと、ユーザースクリプトとしては両方の環境で動作するように作成してあったため、最低限の修正で済んだといえる。
まぁ、細かい不具合は出ているのだが……。

Chrome 拡張機能の移植準備( Microsoft Edge 拡張機能編)


前書き

承前。
furyu.hatenablog.com

比較の意味もあって、Microsoft Edge に関しても拡張機能開発(移植)準備をしてみた。
結果から言えば、互換性や動作安定性が悪く、時期尚早だったな、と少し後悔している。

なお、MS-Edge の拡張機能は、まだ限られた開発者しか公開できない模様。
Microsoft Edge拡張機能の公開について - マイクロソフト コミュニティ
developer.microsoft.com

I have an extension ready for Edge. How can I get it submitted to the store?

For the Windows 10 Anniversary Update, we are intentionally starting with a small set of extensions. The list of extensions is locked - you can see the list at our extensions page here. We want to be mindful about what extensions are available on the platform and watch for telemetry and feedback and make sure the reliability, performance and functionality of the browser isn’t impacted by these new features. Extension developers can submit a request to https://aka.ms/extension-request to be considered for a future update.

Frequently Asked Questions - Microsoft Edge Development

拡張機能開発の手順

開発者向け設定の有効化

フラグ設定画面(about:flags)を開き、

f:id:furyu-tei:20171117203010p:plain
「開発者向け設定」の「☑ 拡張機能の開発者向け機能を有効にする (これによりデバイスが危険にさらされる可能性があります)」にチェックを入れる。

開発(移植)するアドオンの読み込み

右上にある「…」から、

f:id:furyu-tei:20171117203016p:plain
「拡張機能」を選択すると、[拡張機能の読み込み]というボタンが出来ている。

f:id:furyu-tei:20171117203024p:plain
これを押して、

f:id:furyu-tei:20171117203034p:plain
出てきたダイアログから、開発(移植)しようとする拡張機能の manifest.json が含まれたフォルダを選択する。

f:id:furyu-tei:20171117203039p:plain
マニフェストにエラーがあればその旨表示されるので、適宜修正して再読み込みする。
正常に読み込めたら、

f:id:furyu-tei:20171117203045p:plain
当該拡張機能が表示される(この時点で既に起動されている)。

ここで歯車アイコンをクリックすると、

f:id:furyu-tei:20171117204946p:plain
拡張機能の基本情報と操作用のメニューが表示される。

バックグラウンドページのデバッグ方法

拡張機能操作用メニューから

f:id:furyu-tei:20171117203052p:plain
「検査:バックグラウンド ページ」リンクをクリックすると、

f:id:furyu-tei:20171117203059p:plain
開発者ツールが開き、バックグラウンドページのデバッグを行うことが出来る。

オプション画面のデバッグ方法

右上にある当該拡張機能のアイコンをクリックすると、オプション画面がポップアップする。

f:id:furyu-tei:20171117203104p:plain
適当なところで右クリック(コンテキストメニュー)から、「要素の検査」を選択すると、

f:id:furyu-tei:20171117203110p:plain
開発者ツールが開き、オプション画面のデバッグが行える。

ポップアップから操作するのがやりにくい場合、少し面倒だがタブで開くこともできる。
拡張機能操作用メニューに表示されているIDから、

f:id:furyu-tei:20171117203124p:plain

ms-browser-extension://<拡張機能のID>/<オプション画面(HTML)への、manifest.json があるフォルダからの相対パス>

のようにして、URL が導かれるので、これをブラウザのアドレス欄に入れることで、オプション画面をタブで開くことが出来る。

f:id:furyu-tei:20171117203144p:plain