Posted at
gloopsDay 24

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

More than 1 year has passed since last update.


:calendar_spiral: gloops Advent Calendar 24日目

:santa_tone2: Merry Christmas!! gloops Advent Calendar 24日目です

23日目は @t_shibaki さんの「webのサーバーエンジニアがunreal engineをさわってみた」でした

昔と違ってプログラムを書かずにゲームが作れちゃうんですね。。すごい


早速ですが、タイトルだけ見ると「何を言ってるんだ??」という感じだと思います

何がしたいかというと、ブラウザ(Service Worker)内で KoaJS 1 を実行しリクエストを処理させれば

オフラインアプリに近いことができるのでは?と試してみたところ実現できたので、共有します

(全然クリスマスっぽいネタじゃなくてすみません!)


概念図

概念図.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 を見ると、こんな感じになっていました

/***/ },

/* 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);
}));
});


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

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 モジュール内でしか作成できないっぽい?ので自前で用意します

const req = {

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

req には url と HTTP Method を指定するだけ十分です

(socket は request モジュール内で参照されるため、とりあえず空のオブジェクトを用意しました)

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 には endsetHeader を用意します

end はリクエスト処理が終わった時に KoaJS 内部で呼ばれます

引数で渡された body と option で作成したレスポンスで resolve することでリクエスト処理が終了します

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

最終的なソースコードはこちらにあるので興味がある方はご覧ください


デモ

https://kanatapple.github.io/koa-service/

(実際に触れます)

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

ServiceWorkerDemo.gif

ご覧頂くとわかると思いますが、全て ServiceWorker からリソースを取得しています

また、DeveloperTools の Offline にチェックを入れてもページ遷移が正常に行われています


まとめ


  • 簡単なものであれば、完全なオフラインアプリが作成できそう

  • 強引にコンパイルを通している部分があるので考慮が必要

  • Node.js 上でしか動かないモジュールは代替手段を用意する必要がある(ファイル保存の代わりに IndexedDB に入れるとか)


参考

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


最終日25日目は @j_kitayama_hoge000 さんです

クリスマスっぽいものを紹介してくれるそうなので期待しましょう!





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