Bookmarkletを作ろう(非同期通信その２)

javascriptとjQueryでBookmarkletを作ろうの第五回

#はじめに
[前回](https://qiita.com/kanaxx/items/265aee04e479a5a40e51)は、BookmarkletでHTMLを解析し、取れた値を使ってgoogleのAPIを投げました。Bookmarklet起動したらすべてが動くような形になっていましたが、今回はBookmarkletとAPIの実行を分離してみます。

#使うもの
- javascript
- jQuery3
- google page speed API

#作るもの
googleの検索結果ページで起動すると、各検索結果ごとに2つのボタンを設置するBookmarkletです。設置されたボタンをクリックすると、google page speed APIを実行してページスピードを数値で表示します。

実行結果
![image.png](https://qiita-image-store.s3.amazonaws.com/0/168315/97a294e9-0d4f-14e9-f473-0af8fdb33841.png)


Bookmarkletを実行した直後はdesktopとmobileのボタンが表示されるだけで、ボタンを押した後に（しばらく待つと）スコアが表示されます。
PageSpeedScoreを手間なく集めるためのブックマークレットではありませんのでご注意ください

#作成したコード

```javascript
void((function(f){
    var script = document.createElement('script');
    script.src = '//code.jquery.com/jquery-3.2.1.min.js';
    script.onload = function(){
      var $ = jQuery.noConflict(true);
      f($);
    };
    document.body.appendChild(script);
})(
function($, undefined){

    $('div.srg>div.g').each(function(i,e){
        $(e).append('<div class="_form_">'+
        '<button id="desktop_'+i+'">desktop</button>'+
        '<button id="mobile_'+i+'">mobile</button>'+
        '</div>');
    });
    
    $('div._form_>button').on('click', function() {
        strategy = $(this).text();
        href = $(this).parents('div.srg>div.g').find('a[href]').first().attr('href');
        console.log(href + '--' + strategy);
        s = getscore(href,strategy,$(this), update_caption);
    });

    function update_caption(strategy,btn,score){
        caption = btn.text();
        console.log( 'callback function>>' + caption );
        if( caption ==strategy){
            btn.text( strategy+ ' >> page speed score=' + score);
        }
    }

    function getscore(href,strategy,button,callback){
        if( strategy!='desktop' && strategy!='mobile' ){
            return ;
        }
        $.ajax({
            type: "get",
            url: "https://www.googleapis.com/pagespeedonline/v4/runPagespeed",
            timeout: 20000,
            cache: false,
            async: true,
            data: {'url':href, 'strategy':strategy},
            dataType: 'json'
        })
        .done(function (response, textStatus, jqXHR) {
            console.log(response);
            if (response.status === "err") {
                console.error("err: " + response.msg);
            } else {
                score = response['ruleGroups']['SPEED']['score'];
                console.log(href+'=>' + score);
                callback(strategy,button,score);
                return score;
            }
        })
        .fail(function (jqXHR, textStatus, errorThrown) {
            
            console.log(jqXHR);
            alert("失敗: サーバー内でエラーがあったか、サーバーから応答がありませんでした。");
        })
        .always(function (data_or_jqXHR, textStatus, jqXHR_or_errorThrown) {
        // done,failを問わず、常に実行される処理
        });

    }
}
)

);
```

closure compiler実行済みのもの。ブラウザのアドレスバーに貼ると動きます。

```javascript
javascript:void function(a){var b=document.createElement("script");b.src="//code.jquery.com/jquery-3.2.1.min.js";b.onload=function(){var b=jQuery.noConflict(!0);a(b)};document.body.appendChild(b)}(function(a,b){function d(a,c,b){caption=c.text();console.log("callback function>>"+caption);caption==a&&c.text(a+" >> page speed score="+b)}function e(b,c,d,e){"desktop"!=c&&"mobile"!=c||a.ajax({type:"get",url:"https://www.googleapis.com/pagespeedonline/v4/runPagespeed",timeout:2E4,cache:!1,async:!0,data:{url:b,strategy:c},
dataType:"json"}).done(function(a,f,g){console.log(a);if("err"===a.status)console.error("err: "+a.msg);else return score=a.ruleGroups.SPEED.score,console.log(b+"=>"+score),e(c,d,score),score}).fail(function(a,b,c){console.log(a);alert("\u5931\u6557: \u30b5\u30fc\u30d0\u30fc\u5185\u3067\u30a8\u30e9\u30fc\u304c\u3042\u3063\u305f\u304b\u3001\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u5fdc\u7b54\u304c\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002")}).always(function(a,b,c){})}a("div.srg>div.g").each(function(b,
c){a(c).append('<div class="_form_"><button id="desktop_'+b+'">desktop</button><button id="mobile_'+b+'">mobile</button></div>')});a("div._form_>button").on("click",function(){strategy=a(this).text();href=a(this).parents("div.srg>div.g").find("a[href]").first().attr("href");console.log(href+"--"+strategy);s=e(href,strategy,a(this),d)})});
```


コードの説明をしていきます

##解説１
**ボタンの設置**

```javascript
$('div.srg>div.g').each(function(i,e){
    $(e).append('<div class="_form_">'+
    '<button id="desktop_'+i+'">desktop</button>'+
    '<button id="mobile_'+i+'">mobile</button>'+
    '</div>');
});
```

前回のコードと同じパターンで、検索結果の繰り返し部分を捕まえます。それに対して、```<div>```タグを差し込んでその中にボタンを2つ置きます。文字列でHTMLタグを組み立てていますが、分かりやすさ重視で。


##解説２
**ボタンにイベントを仕込む**
先のeachで置いたボタンにfunctionを仕込みます。自分で作ったdivの下のbuttonに限定しています。

```javascript
$('div._form_>button').on('click', function() {
    strategy = $(this).text();
    href = $(this).parents('div.srg>div.g').find('a[href]').first().attr('href');
    console.log(href + '--' + strategy);
    s = getscore(href,strategy,$(this), update_caption);
});
```

やっていることは、ボタンが押されたら、
1. 押されたボタンの表示名を取る
2. 押されたボタンが含まれているGoogleの検索結果の塊を探し、その中の最初のAタグを見つけてリンク先を取る
3. googleのAPIを呼び出すfunctionを呼び出す
というコードをボタンに仕掛けます。あくまでボタンが押されたら動くコードです。

変数sでgetscoreの返り値を受け取っていますが、何も入っていません。getscoreの中がajax非同期メソッドのため、値が返ってきません。APIのコールが終わったあとに動作を継続するために、コールバック関数を渡しています。
ボタンの参照をそのまま渡しているのはちょっとダサいですが、ボタン名を変えるシチュエーションは無いと思うので、見え方重視でこんな形です。


##解説３
**コールバックメソッド**

```javascript
function update_caption(strategy,btn,score){
    caption = btn.text();
    console.log( 'callback function>>' + caption );
    if( caption ==strategy){
        btn.text( strategy+ ' >> page speed score=' + score);
    }
}
```

ボタン名とstrategy(desktop or mobile)が同じときだけ、ボタン名にスコアを表示しています。2回目以降押した場合は、ボタン名が変わっているのでスルーします。

##解説４
以降のコードは、前回作ったgoogleのpage speed APIを呼ぶ部分をfunctionにしただけですので、省略します。

ブックマークレットを起動して、プチプチとボタンを押してみてください。スコアが表示されるより前に別のボタンを押しても大丈夫です。そのうち表示されるはずです。

#まとめ
表示されているHTMLにボタンを設定したり、ボタンの動作を勝手に埋め込んだりできることが分かったと思います。今回は、ボタンを押した先がgoogleのAPIになっていますが、この部分を自作のAPIに向ければリアルタイムで結果を受け取ることができます。
たとえば、検索結果を人間の目で見てOKとNGの2段階の評価したり、ECサイトで安いものを見つけたときに価格を報告したり、NGな画像のURLを報告したり、など。ライバルサイトのチェック作業が捗るのではないかと思います。