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-8UTF-16 コンバータとして機能するらしい。

そのライブラリは、XPCOM のアドオンだったのだけれど、文字列リテラルに必要な処理だったのかどうかは謎だし、WebExtension では必要ない(というか、エラーになる)。
対応としては、convertStringEncoding() を書き換えれば良いだけなんだけど、

  • ライブラリをいじらずにおきたい
  • ロードするときに呼ばれてる処理なので、後から置き換えるという手が通じない

ということがあって、ライブラリをロードする前に decodeURIComponent() を書き換えて、ロードが終わった後に戻す(ある意味、荒業
# 素直に、ライブラリをいじった方が良いんじゃないか(という気はしてる

で、こんな感じの処理の流れになる。

  1. ライブラリがロード済みかどうかを確認
  2. ライブラリがロードされてなかったら
    1. decodeURIComponent の実装を保存して、書き換える
    2. ライブラリをロードする
    3. decodeURIComponent の実装を戻す
    4. ライブラリの機能追加/変更のスクリプトをロードする
  3. ライブラリを使う

前置きが長くなりました。

で、実際のコード

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);
    }
}

diff を取ったものがこちら

思ったこと

こんなハイクをした後に書きました。
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



おしまい。