Promise vs async/await
背景というか、状況というか
Firefox の addon で、コンテンツプロセスでスクリプトをロードしたり、コードを実行したりする browser.tabs.executeScript() を続けて実行したい、という場面。
- ライブラリを使いたいんだけど、一部がエラーになる
- ライブラリに機能を追加したい
- でも、(何となく)ライブラリはいじらずにおきたい
- コンテンツプロセスでは、前回の状態が残っているので、ライブラリのロードは一回にしたい
「一部がエラーになる」というのは、
convertStringEncoding: function(str) { // for subscript loader return decodeURIComponent(escape(str || '')); },
この関数がこんなふうに呼ばれてて、
SketchSwitch.Buttons.RedPen = function(sketch) { this.sketch = sketch }; SketchSwitch.Buttons.RedPen.prototype = SketchSwitch.Utils.extend({ shortcut: null, icon: 'data:image/png;base64,iVBOR...', name: SketchSwitch.Utils.convertStringEncoding('赤ペン'),
URIError: malformed URI sequence というエラーが出ちゃう。
どうやら、XPCOM で、設定ファイルで非ASCII 文字を扱うときの あるある らしく、件のコードは UTF-8 → UTF-16 コンバータとして機能するらしい。
- Firefox 拡張機能の設定項目の保存時に日本語が化ける - vivid memo
- 4章:XPCOM活用術 - Firefox拡張機能開発チュートリアル (XHTML)
- javascript - decodeURIComponent vs unescape, what is wrong with unescape? - Stack Overflow
そのライブラリは、XPCOM のアドオンだったのだけれど、文字列リテラルに必要な処理だったのかどうかは謎だし、WebExtension では必要ない(というか、エラーになる)。
対応としては、convertStringEncoding()
を書き換えれば良いだけなんだけど、
- ライブラリをいじらずにおきたい
- ロードするときに呼ばれてる処理なので、後から置き換えるという手が通じない
ということがあって、ライブラリをロードする前に decodeURIComponent() を書き換えて、ロードが終わった後に戻す(ある意味、荒業
# 素直に、ライブラリをいじった方が良いんじゃないか(という気はしてる
で、こんな感じの処理の流れになる。
- ライブラリがロード済みかどうかを確認
- ライブラリがロードされてなかったら
- decodeURIComponent の実装を保存して、書き換える
- ライブラリをロードする
- decodeURIComponent の実装を戻す
- ライブラリの機能追加/変更のスクリプトをロードする
- ライブラリを使う
前置きが長くなりました。
で、実際のコード
Promise を使って書いたコード。
function show_sketch_menu(tab) { // https://github.com/mdn/webextensions-examples/blob/master/context-menu-copy-link-with-types/background.js let loaded_first = false; browser.tabs.executeScript(tab.id, { code: "typeof SketchSwitch === 'function'", }).then(result => { if (!result || result[0] !== true) { // SketchSwitch is not defined loaded_first = true; // for malformed URI --- SketchSwitch.Utils.convertStringEncoding() return browser.tabs.executeScript(tab.id, { code: ` original__decodeURIComponent = decodeURIComponent; decodeURIComponent = s => unescape(s); true; // Script '<anonymous code>' result is non-structured-clonable data `, }); } }).then(result => { if (loaded_first) { return browser.tabs.executeScript(tab.id, { file: "/content/SketchSwitch.js", }); } }).then(_ => { if (loaded_first) { return browser.tabs.executeScript(tab.id, { code: ` decodeURIComponent = original__decodeURIComponent; true; `, }); } }).then(_ => { if (loaded_first) { return browser.tabs.executeScript(tab.id, { file: "/content/sketch-patch.js", }); } }).then(_ => { return browser.tabs.executeScript(tab.id, { code: ` (_ => { const canvas = document.getElementById("__sketch_switch_canvas__"); if (canvas) { // Sketch Menu is shown. return; } const sketch = new SketchSwitch(window, {}); sketch.show(); })(); `, }); }).catch(error => { console.error(error.message || error); console.dir(error); }); }
こちらが async/await を使って、書き直したコード。
async function show_sketch_menu(tab) { try { // https://github.com/mdn/webextensions-examples/blob/master/context-menu-copy-link-with-types/background.js const result = await browser.tabs.executeScript(tab.id, { code: "typeof SketchSwitch === 'function'", }); if (!result || result[0] !== true) { // SketchSwitch is not defined // for malformed URI --- SketchSwitch.Utils.convertStringEncoding() await browser.tabs.executeScript(tab.id, { code: ` original__decodeURIComponent = decodeURIComponent; decodeURIComponent = s => unescape(s); true; // Script '<anonymous code>' result is non-structured-clonable data `, }); await browser.tabs.executeScript(tab.id, { file: "/content/SketchSwitch.js", }); await browser.tabs.executeScript(tab.id, { code: ` decodeURIComponent = original__decodeURIComponent; true; `, }); await browser.tabs.executeScript(tab.id, { file: "/content/sketch-patch.js", }); } await browser.tabs.executeScript(tab.id, { code: ` (_ => { const canvas = document.getElementById("__sketch_switch_canvas__"); if (canvas) { // Sketch Menu is shown. return; } const sketch = new SketchSwitch(window, {}); sketch.show(); })(); `, }); } catch(error) { console.error(error.message || error); console.dir(error); } }
思ったこと
こんなハイクをした後に書きました。
Promise を返してくれる API だったら、async/await は、気持ちすっきりするかなあ、という感じ。
行数もインデントも減るし。
書きながら思ったけど、Promise の方は、最初の一発目だけを、もうひとつの Promise でくくってあげれば、余計なフラグを持つ必要はなくなるのかも(試してない
}).then(result => { return new Promise((resolve, reject) => { if (!result || result[0] !== true) { // SketchSwitch is not defined // for malformed URI --- SketchSwitch.Utils.convertStringEncoding() browser.tabs.executeScript(tab.id, { code: ` original__decodeURIComponent = decodeURIComponent; decodeURIComponent = s => unescape(s); true; // Script '<anonymous code>' result is non-structured-clonable data `, }) .then(_ => { return browser.tabs.executeScript(tab.id, { file: "/content/SketchSwitch.js", }); }).then(_ => { ... }).then(_ => { resolve(); }).catch(_ => { reject(); } } }; }).then(_ => { ...
余計な変数は必要なくなるけど、インデントは深くなる。
自分で Promise 書いちゃうと、すっきりしないなあという感じはする。
こんなハイクのやり取りも。
http://h.hatena.ne.jp/noromanba/316607276733271244
http://h.hatena.ne.jp/noromanba/315956334695317249
おしまい。