ブラウザでNode.js(KoaJS)サーバを走らせる

:calendar_spiral: gloops Advent Calendar 24日目
---

:santa_tone2: Merry Christmas!! [gloops Advent Calendar](http://qiita.com/advent-calendar/2016/gloops) 24日目です
23日目は @t_shibaki さんの「[webのサーバーエンジニアがunreal engineをさわってみた](http://qiita.com/t_shibaki/items/8546b0a78008cf8e5ed5)」でした
昔と違ってプログラムを書かずにゲームが作れちゃうんですね。。すごい

---

早速ですが、タイトルだけ見ると「何を言ってるんだ？？」という感じだと思います
何がしたいかというと、ブラウザ(Service Worker)内で [KoaJS](http://koajs.com/) [^1] を実行しリクエストを処理させれば
オフラインアプリに近いことができるのでは？と試してみたところ実現できたので、共有します
(全然クリスマスっぽいネタじゃなくてすみません！)

# 概念図

![概念図.png](https://qiita-image-store.s3.amazonaws.com/0/54850/dec93e16-fd10-2a44-8f1f-d2a962ff4f91.png)

初回のみ、ServiceWorker、ServiceWorker 登録用の HTML と JavaScript をサーバにリクエストします
2回目移行は ServiceWorker 内で動いている KoaJS サーバがルーティング処理などを行います

# ビルド環境構成

- Node.js(v6.9.1)
- KoaJS(v1.2.4)
- Webpack(v2.1.0-beta.27)

ServiceWorker の JavaScript を作成するために必要なものです
参考にさせて頂いたリポジトリは express + browserify という構成でしたが、今回は慣れている構成で試しました

# KoaJS サーバの実装

ブラウザ内で実行させる前に、まずは普通にサーバを実装し、オンラインでどんな感じになるか確認しておきます

```app.js
const app = require('koa')();
const route = require('koa-route');

function buildHtml(header, link1, linkText1, link2, linkText2) {
    return `
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
    <title>Koa Service</title>
    <link rel="stylesheet" href="./bootstrap.min.css">
</head>
<body>
    <nav class="navbar navbar-default" role="navigation">
        <div class="container-fluid">
            <div class="navbar-header">
                <h1 class="navbar-brand">${header}</h1>
            </div>
        </div>
    </nav>
    <div class="container-fluid">
        <a class="btn btn-info" role="button" href="${link1}">${linkText1}</a>
        <a class="btn btn-info" role="button" href="${link2}">${linkText2}</a>
    </div>
</body>
</html>
`;
}

app.use(route.get('./', function* () {
    this.body = buildHtml('Index Page', './about', 'To About', './help', 'To Help');
}));

app.use(route.get('./about', function* () {
    this.body = buildHtml('About Page', './', 'To Index', './help', 'To Help');
}));

app.use(route.get('./help', function* () {
    this.body = buildHtml('Help Page', './', 'To Index', './about', 'To About');
}));

app.listen(3000); // バンドルする時には必要ないので消します
```

今回は説明を簡単にするために、bootstrap で装飾された3つのページがあり
それぞれがリンクで繋がっている HTML を返すだけにしました

`node app.js` を実行し、http://localhost:3000 にアクセスしてみます
index、about、help というページがリンクで繋がっているのが確認できたと思います

これでサーバが用意できました
ここからはブラウザ内で実行させるための手順を説明していきます

# いろいろな問題

ブラウザ内で実行させるために、いろいろな問題を解決する必要がありました

## ブラウザでは Node.js を実行できない(コアモジュールを require できない)

Node.js のモジュールは様々なコアモジュールを require していますが
ブラウザではそれが不可能なため、Webpack でコアモジュールも含めて1つのファイルにバンドルします

```webpack.config.js
module.exports = {
    entry: './src/server.js',
    output: {
        filename: './docs/sw.js'
    },
    target: 'web'
};
```

ポイントは `target` の設定です
`'web'` を設定することでコアモジュールもまとめて1つにバンドルしてくれます

この状態でビルドしてみると・・

```
Module not found: Error: Can't resolve 'fs' in 'koa-service\node_modules\destroy'
Module not found: Error: Can't resolve 'net' in 'koa-service\node_modules\koa\lib'
```

エラーでコケました・・orz
おそらく `target: 'web'` にしたため、ブラウザ向けの代替モジュールがないためエラーになったようです

今回は下記のように回避しました

```webpack.config.js
module.exports = {
    entry: './src/server.js',
    output: {
        filename: './docs/webpack.sw.js'
    },
    target: 'web',
    node: {
        fs: 'empty',
        net: 'empty'
    }
};
```

`empty` を設定すると空のモジュールを用意してくれるようです
実際に書き出された JavaScript を見ると、こんな感じになっていました

```js
/***/ },
/* 63 */
/***/ function(module, exports) {

/***/ },
```

この状態でビルドしてみると・・

```
ERROR in ./~/statuses/codes.json
Module parse failed: \koa-service\node_modules\statuses\codes.json Unexpected token (2:7)
```

今度は json の読み込みに失敗してるようなので loader の設定を追加してみます
(json-loader のインストールも必要です)

```webpack.config.js
module.exports = {
    entry: './src/server.js',
    output: {
        filename: './docs/sw.js'
    },
    target: 'web',
    node: {
        fs: 'empty',
        net: 'empty'
    },
    module: {
        rules: [
            { test: /\.json$/, loader: 'json-loader' },
        ]
    }
};
```

これでビルドが通るようになりました！
(ウェブパッ筋があまり高くないので、他の方法でもイケるよ！というご指摘お待ちしておりますm(_ _)m)

## 実際に読み込ませてみると・・

書き出された ServiceWorker をブラウザに読み込ませてみると

```
Uncaught ReferenceError: window is not defined
Uncaught TypeError: global.XMLHttpRequest is not a constructor
```

このようなエラーが出てしまいます
ServiceWorker 内でのグローバル変数は window ではなく global に定義されてるため
存在しない window オブジェクトにアクセスしているのと、global.XMLHttpRequest が存在しないためエラーになったようです

```patch.js
if (typeof window === 'undefined') {
    window = global;
}

if (typeof global.XMLHttpRequest === 'undefined') {
    global.XMLHttpRequest = function () {
        this.open = function () {};
    }
}
```

上記のようなパッチを用意して、しのいでみました
このパッチを含めてビルドすると、エラーが出ない状態になります

# ServiceWorker 内での KoaJS の処理

ここまでで、ServiceWorker 内でサーバーを走らせる準備ができました
あとは、KoaJS に対してリクエストを投げることができればOKです

まずコードを見た方がイメージしやすいと思いますので、載せてみます
install イベント、activate イベント時は特殊なことはしてないので省略します

```service-worker.js
self.addEventListener('fetch', (event) => {
    // 省略

    event.respondWith(new Promise((resolve) => {
        const parsedUrl = url.parse(event.request.url);
        const splitedPath = parsedUrl.pathname.split('/');
        
        const req = {
            url: `./${splitedPath[splitedPath.length - 1]}`, // Make relative url
            method: event.request.method,
            socket: {}
        };

        const res = {
            _headers: {},
            end(body) {
                const options = {
                    status: this.statusCode || 200,
                    headers: this._headers
                };
                resolve(new Response(body, options));
            },
            setHeader(name, value) {
                this._headers[name] = value;
            }
        };

        const handleRequest = app.callback();
        handleRequest(req, res);
    }));
});
```

重要なポイントはここです

```js
const handleRequest = app.callback();
handleRequest(req, res);
```

下記のように、KoaJS では `callback()` で得られた requestListener を `http.createServer` に渡すことでリクエスト処理をしていることがわかります

```application.js
app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};
```

同じように `callback()` で得られた requestListener を fetch 時に呼び出すことで
リクエスト処理させるということです

requestListner には http.IncomingMessage である req と http.ServerResponse である res を渡す必要がありますが
これらのインスタンスは http モジュール内でしか作成できないっぽい？ので自前で用意します

```js
const req = {
    url: `./${splitedPath[splitedPath.length - 1]}`, // Make relative url
    method: event.request.method,
    socket: {}
};
```

req には url と HTTP Method を指定するだけ十分です
(socket は request モジュール内で参照されるため、とりあえず空のオブジェクトを用意しました)

```js
const res = {
    _headers: {},
    end(body) {
        const options = {
            status: this.statusCode || 200,
            headers: this._headers
        };
        resolve(new Response(body, options));
    },
    setHeader(name, value) {
        this._headers[name] = value;
    }
};
```

res には `end` と `setHeader` を用意します
`end` はリクエスト処理が終わった時に KoaJS 内部で呼ばれます
引数で渡された body と option で作成したレスポンスで resolve することでリクエスト処理が終了します

これで、ServiceWorker 内で KoaJS がリクエストをさばけるようになりました

最終的なソースコードは[こちら](https://github.com/kanatapple/koa-service)にあるので興味がある方はご覧ください

# デモ
https://kanatapple.github.io/koa-service/
(実際に触れます)

動作確認済み環境：PC Chrome 55.0.2883.87 m (64-bit)

![ServiceWorkerDemo.gif](https://qiita-image-store.s3.amazonaws.com/0/54850/92756c53-ca44-26dc-3b35-db258b43a02a.gif)

ご覧頂くとわかると思いますが、全て ServiceWorker からリソースを取得しています
また、DeveloperTools の Offline にチェックを入れてもページ遷移が正常に行われています

# まとめ

- 簡単なものであれば、完全なオフラインアプリが作成できそう
- 強引にコンパイルを通している部分があるので考慮が必要
- Node.js 上でしか動かないモジュールは代替手段を用意する必要がある(ファイル保存の代わりに IndexedDB に入れるとか)

# 参考

https://github.com/bahmutov/express-service

---
最終日25日目は @j_kitayama_hoge000 さんです
クリスマスっぽいものを紹介してくれるそうなので期待しましょう！

[^1]: Node.js の Web Framework。express の方がよく知られているかも