LoginSignup
8
8

More than 5 years have passed since last update.

Content ScriptからBackground Script内の関数を直接呼びだす(ように見せる)

Last updated at Posted at 2018-05-25

何を実現させたいか

ブラウザ拡張を作っているとAPIによってContent ScriptではできないからBackgroundに処理を任せて結果を返してもらうといったケースに度々みまわれます。
chrome1.runtime.sendMessage と、background.jsで受けるchrome.runtime.onMessage.addListener毎回書いてますよね?
あーめんどくさい。

やらせたいことが複数あると引数にケース分けする識別子入れて、それ以外もパラメータがあるからJSONに固めて渡して…とやってますよね?
あーめんどくさい。

処理がどこでやってるのか視覚的にも分かりづらい。可読性もよろしくないです。
これを次のようにできるようにするよーってのがこの記事です。

Content Script側

var result = await bg.myMethod(param1, param2);

Background側

bg.myMethod = function(param1, param2) {
    var result = ...;
    ...
    return result;
}

bgって略語オブジェクトの命名はともかく、どうでしょ。読みやすくないですか?2 メソッド名でソース検索できるし。

先の例のBackground側、バックグラウンド内で非同期の処理でないと結果が得られない場合も対応できます。Promiseを使いましょう。
例えば、needCallbackLogic(successCallback, errorCallback) という引数に渡されたcallback関数に結果を返す処理があり、その関数がPromiseに対応していない場合は以下のように。

Background側

bg.myMethod = function() {
    return new Promise((resolve, reject)=>{
        needCallbackLogic(function (result){
            resolve(result);
        }, function(error){
            reject(error);
        });
    });
}

このように、バックグラウンドの実装メソッドは、functionでもasync functionでも対応できます。便利ですよっ!
Promiseとasync/awaitについてはここでは割愛しますのであしからず。

bgオブジェクトのソースコード

では実現方法です。bgにトリックがあるというのは推測できますよね。実現させるには、フロント側とバックグラウンド側にProxyPromise(async/awaitも)を使って作成した、それぞれ異なるbgオブジェクトを定義したソースを拡張内に置くだけです。以下にそれぞれのソースを示します。
解説は後述しますね。

Content Script側

backgroundCaller.js
/**
 * バックグラウンドにRPCする呼び出し側の実装(manifestのcontent_scriptsに記述して配備すること)
 */
'use strict';
(function(){

    function createAsyncRpcFunction(methodName) {
        return async function() {
            const req = {
                method: methodName,
                args: [].slice.apply(arguments)
            };
            return new Promise((resolve, reject)=>{
                chrome.runtime.sendMessage(req, function(res){
                    if (res && ["OK","ERROR"].includes(res.status)) {
                        if (res.status == "OK") {
                            if (!res.data) {
                                res.data = undefined;
                            }
                            try {
                                resolve(res.data);
                            } catch(e) {
                                console.error(`then-callback call failure!! ${e}`);
                                reject(e);
                            }
                            return;
                        } else {
                            console.error(`RPC ${methodName} result ERROR!! ${JSON.stringify(res)}`);
                            reject(res);
                        }
                    } else {
                        console.error(`RPC return value of ${methodName} is no reply or unknown status!! ${res}`);
                        reject(res);
                    }
                });
            });
        }
    }

    /**
     * RPCの仕掛け。
     */
    window.bg = new Proxy({}, {
            get: function(target, methodName){
                if (methodName in target) {
                    return target[methodName];
                }
                return target[methodName] = createAsyncRpcFunction(methodName);
            }
        });

})();
Background側
backgroundListener.js
/**
 * RPCされる側の実装(manifestのbackground/scriptsに記述して配備すること)
 */
'use strict';
window.bg = window.bg || {};
(function(){
    chrome.runtime.onMessage.addListener(function(req, sender, sendResponse) {
        if (req.method in bg && bg[req.method] instanceof Function) {
            let func = bg[req.method];
            try {
                var result = func.apply(func, req.args || []);
                if (!(result instanceof Promise)) {
                    let trueResult = result;
                    result = new Promise((resolve,reject)=>{
                        try {
                            resolve(trueResult);
                        } catch(e) {
                            reject(e);
                        }
                    });
                }
                result.then(result=>sendResponse({
                        status: "OK",
                        data: result
                    })
                ).catch(e=>sendResponse({
                        status: "ERROR",
                        error: e.toString()
                    })
                );
            } catch(e) {
                sendResponse({
                    status: "ERROR",
                    error: e.toString()
                });
            }
        } else {
            sendResponse({
                status: "ERROR",
                error: `method not found - ${req.method}`
            });
        }
        return true;
    });
})();

あとは冒頭に書いたようなコードを書けば、あら不思議。ちゃんと結果が返るようになりますよ!3

解説

まずメソッド名解決から。bgに格納されたバックグラウンドで書いたメソッドをフロント側が知るはずもないのに、どういうギミックで解決するのか。
この解決をするのがProxyの役目です。

Proxyはその名前のとおり、本来のオブジェクトに成り代わって、実際のプロパティアクセスに割り込んで処理を変異させることができます。
Proxyのコンストラクタの第1引数は本来のオブジェクト(コードでは只の空オブジェクト)。第二引数はgetやsetのメソッドを実装したオブジェクトを用意しておくことで呼び出されるようになります。

  1. window.bg.なにか を参照する。
  2. 第二引数のオブジェクトのgetが、get(本来のオブジェクト, "なにか"); で呼び出される。

…といった動きに成り代わります。
window.bgオブジェクトがなぜProxyで出来ているかと言えば、未解決のメソッド名をメッセージングの識別子に利用するためなのです。

このコードでgetはcreateAsyncRpcFunctionが作成したfunctionを返していますので、"なにか"はfunction型のプロパティとなります。そのためwindow.bg.なにか(渡すよA, 渡すよB);のように返されたfunctionをそのまま呼び出すことができるようになっています。

次に、この作成されたfunctionは何をやっているかというと、
{ "method": "なにか", "args": [渡すよA, 渡すよB] } をsendMessageしています。これをバックグラウンドにあるメッセージリスナが受け取って解釈し、バックグラウンド側のbgオブジェクトが"なにか"という名前のfunction型のプロパティを持っている場合は、それを呼び出して実行結果を返しています。
返す内容のパターンは2種類です。
{ "status": "OK", "data": メソッドのリターン値 }
{ "status": "ERROR", "error": 例外のエラーメッセージ }
呼び出し側はこれらのresponseに応じて、結果を返します。

ユーザーが作成した応答側のメソッドはasync functionで作っているのであればPromiseが返りますが、そうでない普通のfunctionの場合はPromiseにしてthenとcatchでresponseを返します。
Listenerが必ずtrueを返しているので非同期でsendResponseするまで呼び出し側は待機します。

呼び出し側は常にPromiseオブジェクトなので、thenとcatchで受け止めましょう(もしくはawait)。

終わりに

いかがでしたでしょうか。これを参考にしてProxyPromiseを使い倒していきましょう。
例えばProxyは、chrome.i18n.getMessageの第一引数を識別子として使うなんてのも良いですね。

const constants = new Proxy({},{get:function(target,name){return chrome.i18n.getMessage(name);}});

なんてしておけば、constants.extensionNameなどのようにして、messages.json内の値を参照できたりします。

ではでは。


  1. web extensionならbrowserに読み替えてください。chromeのままでもFirefoxのWeb Extensionで動きますけどね。 

  2. Content Script側は常に非同期で結果が返るので、await使うならasync function内で呼び出す記述とすることをお忘れなく。もちろんawaitを使わずに、.then(...).catch(...) のようにPromiseのコールバックを書いても構いません。 

  3. manifest.json内では作成する、呼び出す/呼び出されるメソッドの記述のあるソースコードより前に記述してください。 

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8