ServiceWorkerを使ってLZ4+MessagePackで通信してみる

# 概要

![sw.png](https://qiita-image-store.s3.amazonaws.com/0/380/88a5a6e1-7dbf-31eb-32fc-5a8b02a4742a.png)

あるサイトではクライアント側(script.js)でXMLHttpRequestをつかってjson形式でデータの送受信を行っています。ところで、サーバ側(app.js)では圧縮形式とデータの形式を指定してやるとよしなにハンドリングしてくれるようなかんじになっていました。なにも考えずに通信を行えばjsonでやりとりされますが、そこにプロキシとしてServiceWorker(sw.js)をかませば、クライアント側の変更は無しで、ぼくがかんがえたさいきょうのデータ形式で通信することができます。

で、今回はLZ4+MessagePackでやってみたという話です。

# ローカルで動かす

ServiceWorkerはlocalhost以外はhttps必須なので以下のデモをローカルで実行して下さい。

```
git clone https://gist.github.com/a6a4ae1b5197c18328d2.git sw-test
cd sw-test
npm install
node app.js
```

node v5.1.0、Chrome 47で動作確認しました。正しく動作すれば、`contentType`とかにそれらしいものが入っているはず。

<img width="402" alt="スクリーンショット 2015-12-15 14.45.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/380/2ee8af3a-b2d7-e74d-a265-a72d8ac7a8db.png">

# 各ファイルについての解説

## クライアント(script.js)

なんの変哲もないコードです。ServiceWorkerをインストールして、sendButtonをクリックしたらtextareaに書かれたjsonを送りつけるだけです。インストールする部分は[HTML5 Rocks](http://www.html5rocks.com/ja/tutorials/service-worker/introduction/)から拝借しました。

```js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function(registration) {
    // 登録成功
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
  }).catch(function(err) {
    // 登録失敗 :(
    console.log('ServiceWorker registration failed: ', err);
  });
}

window.onload = function() {
  var textarea = document.querySelector('textarea');
  var sendButton = document.querySelector('#sendButton');
  var formatButton = document.querySelector('#formatButton');
  var pre = document.querySelector('pre');

  formatButton.addEventListener('click', function() {
    textarea.value = JSON.stringify(JSON.parse(textarea.value), null, 2);
  });

  sendButton.addEventListener('click', function() {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/api');
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.responseType = 'json';
    xhr.send(textarea.value);
    xhr.onload = function() {
      pre.textContent = JSON.stringify(xhr.response, null, 2);
    };
  });
};
```

## サーバ(app.js)

`/api`に対してpostされたリクエストのheaderをみて処理を振り分けます。`X-Content-Encoding`が`lz4`ならLZ4、`Content-Type`が`application/x-msgpack`ならMessagePack、レスポンスに関しては`X-Accept-Encoding`が`lz4`ならLZ4にして返すというかんじです。ちなみに`X-hoge`となっているところはexpressに怒られたので付けただけです。

```js
var express = require('express');
var bodyParser = require('body-parser');
var lz4 = require('lz4-asm');
var msgpack = require('msgpack-lite');

var app = express();
app.use(express.static('.'));
app.use(bodyParser.raw({type: '*/*'}))

app.post('/api', function(req, res) {
  var buff = new Uint8Array(req.body);
  var obj, ret;
  console.log('byte length: ' + buff.length);

  if (req.headers['x-content-encoding'] === 'lz4') {
    buff = lz4.decompress(buff);
  }
  if (req.headers['content-type'] === 'application/x-msgpack') {
    obj = msgpack.decode(new Buffer(buff));
  } else {
    obj = JSON.parse(new Buffer(buff).toString('utf8'));
  }
  obj.contentType = req.headers['content-type'];
  obj.contentEncoding = req.headers['x-content-encoding'];
  console.log(obj);

  if (req.headers['x-accept-encoding'] === 'lz4') {
    ret = new Buffer(lz4.compress(new Uint8Array(msgpack.encode(obj))));
  } else {
    ret = new Buffer(JSON.stringify(obj));
  }
  res.send(ret);
});

app.listen(3333);
```

## ServiceWorker(sw.js)

`/api`に対してリクエストするときだけjsonとLZ4+MessagePackの変換をするだけ(変換部分がやや雑な気がしますが)。時間がなかったのですが、見た感じ[fetch API](https://fetch.spec.whatwg.org/)では[Stream](https://streams.spec.whatwg.org/)が使えるっぽいのでそのうち試してみたいですね。

```js
importScripts(
  'node_modules/lz4-asm/lz4.js',
  'node_modules/msgpack-lite/dist/msgpack.min.js'
);

self.addEventListener('fetch', event => {
  // apiだけlz4+messagepackで通信するようにする(手抜き)
  if (/api/.test(event.request.url)) {
    var headers = new Headers();
    headers.append('X-Content-Type', 'application/x-msgpack');
    headers.append('X-Content-Encoding', 'lz4');
    headers.append('X-Accept-Encoding', 'lz4');

    event.respondWith(
      event.request.json().then(o => {
        return fetch(event.request.url, {
          method: event.request.method,
          body: new Blob([lz4.compress(msgpack.encode(o))], {type: 'application/x-msgpack'}),
          headers: headers
        });
      })
      .then(response => response.arrayBuffer())
      .then(buffer => new Response(new Blob([JSON.stringify(msgpack.decode(lz4.decompress(new Uint8Array(buffer))))])))
    );
  } else {
    event.respondWith(fetch(event.request));
  }

});
```

# まとめ

ServiceWorkerは結構面白いですね。特に良いなと思ったのは以下の3つ。

* クライアントの既存コードを書きかえる必要がない
* 結構重い処理を書いてもWorkerなので画面が止まらない
* なんでもできる感

ただ、開発について、Chromeのインスペクタは便利なんですが、ファイルの更新をしたら古いWorkerが消えてインスペクタをもう一回立ち上げ直すのが面倒臭すぎます。

# 参考

* [Service Worker の紹介: Service Worker の使い方 - HTML5 Rocks](http://www.html5rocks.com/ja/tutorials/service-worker/introduction/)
* [kawanet/msgpack-lite](https://github.com/kawanet/msgpack-lite)
* [ukyo/lz4.js](https://github.com/ukyo/lz4.js)
* [ServiceWorker sample fetch with LZ4+MessagePack(デモ)](https://gist.github.com/ukyo/a6a4ae1b5197c18328d2)