uokadaの見逃し三振は嫌いです

ここで述べられていることは私の個人的な意見に基づくものであり、私が所属する組織には一切の関係はありません。

navigator.sendBeaconを動かしてみた!!

What is navigator.sendBeacon?

navigator.sendBeaconってなんのこっちゃって感じなのでこれを読むと良い。

Beacon
日本語版: Beacon (日本語訳)

何がうれしいの!?って感じですが、リンクをクリックした時にXMLHttpRequestでサーバーに情報を送っているようなケースで
サーバーのレスポンスが悪かったりするとサーバーにデータを送る前に遷移が完了してしまって取りこぼしてしまって正確なデータの収集が困難なケースがあります。 また、サーバーからのレスポンスを待って遷移させるようなコードを書いていてサーバー側のレスポンスが悪いとユーザーの遷移の妨げになりユーザーエクスペリエンスの低下につながります。

これをBeacon APIを使うことでユーザーの遷移は妨げずブラウザのバックグラウンドで処理を実行して確実にサーバーにデータを送れるようになります。
送ったデータは広告なりアクセス解析なりなんなりと利用すればいいと思います。

サーバーを書いてみる

まずはサーバーを書いてみます。

package main

import (
    "encoding/base64"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    apachelog "github.com/gorilla/handlers"
    "os"
)

const (
    body string = `
<html>
<script>
  // emit non-blocking beacon to record state update
  function reportEvent(eventData) {
    navigator.sendBeacon('/collector', eventData);
  }

  // emit non-blocking beacon to record click on a link
  function reportClick(url) {
    var clickData = JSON.stringify({
      clickTo: url,
      clickAt: performance.now()
    });
    navigator.sendBeacon('/collector', clickData);
  }

  // emit non-blocking beacon when page transitions to background state
  document.addEventListener('visibilitychange', function() {
    if (document.visiblityState === 'hidden') {
      var analyticsData = buildSessionReport();
      navigator.sendBeacon('/collector', analyticsData);
    }
  });
</script>

<body>
 <a href='http://www.w3.org/' onclick='reportClick(this)'>
 <button onclick="reportEvent('button event!')">Click me</button>
</body>
</html>
`
    base64GifPixel = "R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
)

type BaseHandler struct{}

func (bh *BaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, body)
}

type BeaconHandler struct {
    BeaconImage []byte
    RequireTLS  bool
}

func (bh *BeaconHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    log.Println("send beacon")
    //if bh.RequireTLS  r.TLS == nil {
    // log.Printf("WARNING: Please Requst with https.\n")
    //}
    if s, err := ioutil.ReadAll(r.Body); err == nil {
        log.Printf("DEBUG: %#v\n", string(s))
    } else {
        log.Printf("DEBUG: Error %#v\n", err)
    }
    output := string(bh.BeaconImage)
    w.Header().Set("Content-Type", "image/gif")
    //w.Header().Set("Content-Length", string(len(output)))
    io.WriteString(w, output)
    return
}

func main() {
    port := 8000
    mux := http.NewServeMux()

    // set demo page handler
    bh := &BaseHandler{}
    mux.Handle("/hey", bh)

    // Convert image && set handdler
    output, _ := base64.StdEncoding.DecodeString(base64GifPixel)
    beacon := &BeaconHandler{
        BeaconImage: output,
    }
    mux.Handle("/collector",
        apachelog.CombinedLoggingHandler(os.Stderr, beacon))

    // Extra
    mux.Handle("/", http.RedirectHandler("/hey", http.StatusFound))


    fmt.Printf("Listen: %d\n", port)
    addr := fmt.Sprintf(":%d", port)
    srv := http.Server{
        Addr:     addr,
        Handler:  mux,
        ErrorLog: nil,
        //ReadTimeout:  10 * time.Second,
        //WriteTimeout: 10 * time.Second,
    }
    err := srv.ListenAndServe()
    if err != nil {
        log.Fatal(err)
    }
}

検証用のサーバースクリプトを用意したのでとりあえず、サーバーを起動してみよう。

% go run server.go
Listen: 8000

ブラウザからアクセスしてみる

navigator.sendBeaconが実装されているブラウザはあまり多くない。
caniuseで確認してみるとFirefoxChromeには実装されているがそれ以外のサポート状況はあまりよろしくない。
バイル界隈も最新のAndroidのブラウザはサポートされているがiOS safariがサポートされていない状況となっている。
個人的にiOS safariがサポートされていないのは痛いなという印象。
日本のWEB界隈だとiOS:Androidが7:3ぐらいなので7に実装されてないとサポートする側のモチベーションに影響するんじゃないかと。
また、Can I useのページにも日本におけるサポートブラウザの比率が掲載されているがまだ 38.87% ということで半分にも満たない。
普及にはiOS Safariで実装がされることが必須のように思う。

Can I use... Support tables for HTML5, CSS3, etc

自分はFirefox 42からこのURLにアクセス。
アクセスしたらボタンを押して遷移してW3Cのページに移動しよう。
移動したら、コンソールに戻ってみたください。 下のようなログが落ちてるはずです。

% go run server.go
Listen: 8000

2015/12/13 02:34:45 send beacon
2015/12/13 02:34:45 DEBUG: "event!"
2015/12/13 02:34:45 send beacon
2015/12/13 02:34:45 DEBUG: "{\"clickTo\":{},\"clickAt\":2601.465}"

ここには詳細は書いてませんが仕様にはPOSTでリクエストすることとなってますので比較的大きなデータでも受け渡すことが可能です。

Beacon-Age ヘッダフィールドというものが定義されています。 これはユーザーアクションからサーバーに送るまで非同期で処理されるためアクションを起こした正確な時間をサーバー側で取得するためのヘッダーとなっています。 ただ、tcpdump使ってhttpの通信みた限りChrome,Firefoxともに実装されていないので頭の片隅に入ってればいいんじゃないかなと。

Link