前書き
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 属性を使ったダウンロード処理 } ); } )();
'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(); } ); } )();
'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の書き方でもっとはまると思っていたが、これらはそれ程でもなかった代わりに、拡張機能の仕様やブラウザ間の細かい差異の方が難解。