概要
以前の記事で、イベントループ方式の理解に苦しんだことについて記事にあげましたが、今回は、非同期処理についてつまづいたところを忘れないように記事にしてみようと思います。
つまづいた経緯としては、Node.jsでローカルサーバーを構築し、リクエストを受け付けられるようになったところで、「リクエストごとに処理を変えたいなぁ」、と思ったことが全ての始まりでした。
環境
自分が実際につまづいた時の環境は、以下となります。
- OS:Windows10
- ブラウザ:Chrome
やりたかったこと
- データを取得して、それをページに表示する。
最初はMVCモデルに基づいてプログラムを組もうと考えていましたが、少しめんどくさくなりそうだったので、簡単にデータを取得・反映させるページを作ろうと思いました。
つまづいたところ
最初に記述したコードになります。
// モジュールの取り込み
const http = require('http');
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const mime = require('mime-types');
const qs = require('querystring');
const setting = require('./setting');
const db = require('./db');
// 本ファイルの親フォルダを取得(htmlファイル等を取得するため)
const parentDir = path.dirname(__dirname);
// webサーバーの作成
const server = http.createServer();
// リクエストの受付を検知する
server.on('request', AppController);
server.listen(setting.port, setting.host);
console.log('server listeneing...');
// URLのマッピング処理を振り分ける
async function AppController(req, res) {
let filePath = req.url;
if(filePath === '/favicon.ico'){
res.end();
return;
}
let parseData;
if(req.method === "POST"){
req.data = "";
req.on("data", function(chunk){
req.data += chunk;
});
req.on("end", function(){
parseData = qs.parse(req.data);
try{
const resultData = selectAllItem();
//
// 結果を取得して色々処理...
//
buildPage(res, "/public/index.html", resultData);
}catch(e){
console.log(e.stack);
buildPage(res, "err.html", "");
}
});
}else if(req.method === "GET"){
try{
buildPage(res, "/public/index.html", "");
}catch(e){
console.log(e.stack);
buildPage(res, "err.html", "");
}
}
};
// ページ遷移処理
const buildPage = function (res, filePath, postData){
try{
res.writeHead(200, {"Content-Type": mime.lookup(path.basename(filePath)) });
const content = fs.readFileSync(parentDir + filePath, 'utf-8');
const data = ejs.render(content,{form:postData});
res.write(data);
res.end();
}catch(e){
console.log(e.stack);
throw e;
}
}
// DBに接続して、データを取得する
const selectAllItem = async function(){
let item1 = {};
let item2 = {};
let item3 = {};
try{
item1 = db.selectItem1("*");
item2 = db.selectItem2("*");
item3 = db.selectItem3("*");
}catch(e){
console.log(e.stack);
throw e;
}
return {"item1":item1, "item2":item2, "item3":item3};
}
この状態で実行すると、selectAllItem()メソッド実行後の、resultDataに何も入っていませんでした。
これは、処理が非同期処理であるため、selectの実行をする前に、ページ遷移が先に実行されてしまったようです。
これを解決するためには、
selectの実行 → データの編集処理 → ページ遷移
という順番を守ってもらわなければなりません。
解決策(Promise / async / await)
Node.jsでは、基本的にイベントループ方式のため、関数の終了を待ってはくれないみたいです。
そこで必要な知識となるのが、Promiseオブジェクトとasync/awaitの記述でした。
Promiseとasync/awaitは、書き方が違うだけで、実装できる内容はほぼほぼ同じです。
詳しい違いについては、以下の記事を参考にしてみてください。
https://qiita.com/h1guchi/items/0434f1295226cdd19a53
今回の解決策には、async / awaitを使用しました。
まずメイン処理から修正していきます。
メイン処理の修正
// URLのマッピング処理を振り分ける
async function AppController(req, res) {
...//省略
if(req.method === "POST"){
req.data = "";
req.on("data", function(chunk){
req.data += chunk;
});
req.on("end", async function(){
parseData = qs.parse(req.data);
try{
// 1・・・
const resultData = await selectAllItem();
// 2・・・
// 結果を取得して色々処理...
//
// 3・・・
buildPage(res, "/public/index.html", resultData);
}catch(e){
console.log(e.stack);
buildPage(res, "err.html", "");
}
});
}else if(req.method === "GET"){
try{
buildPage(res, "/public/index.html", "");
}catch(e){
console.log(e.stack);
buildPage(res, "err.html", "");
}
}
};
メイン処理で修正することは、下記の流れを守ってもらうことです。
1. データを取得する
2. データを編集する
3. ページ遷移処理を実行する
非同期処理の場合は、この1・2・3が同時に実行されてしまったために問題が起きてしまいましたが、これを解決するために、await
を使用します。awaitを実行したいasyncな処理の前に記述することで、その処理の終了を待つことができます。
ただし、このawait
を使用する場合は、その使用しているメソッドをasync
にする必要があるみたいです。(このために下層の関数はほとんどasyncをつけなくてはいけなくなりました。。。)
今回のメイン処理では、selectAllItem()
メソッドの前に、awaitを記載しました。
次は、selectAllItem
メソッド内の処理を修正していきます。
データ取得処理の修正
const selectAllItem = async function(){
let item1;
let item2;
let item3;
return await Promise.all([
db.selectItem1("*"),
db.selectItem2("*"),
db.selectItem3("*")
])
.then((values) => {
item1 = values[0];
item2 = values[1];
item3 = values[2];
return {"item1":item1, "item2":item2, "item3":item3};
})
.catch((err) => { console.log(err.stack);throw err; });
}
ここでの処理は、Promise.all()
という処理を使用しています。
最初修正した際は、1つ1つのselect処理にawaitをかけていたのですが、それぞれのselectの順番は気にしなくてもよいため、Promise.allとしています。
Promise.all()の中で定義されたasyncな関数は、全て非同期で実行されますが、その全ての関数の終了を待ってから次に進むため、一括でデータを取得したい時とかには向いているかもしれません。
まとめ
普段非同期処理などを意識することがあまりないため、かなり戸惑いましたが、とても勉強になりました。
普段はバックエンド側なので、javascriptを仕事で触ることは稀にしかないのですが、最近調べてみるとjavascriptでARの開発やサーバーの構築やフレームワーク使って簡単にリッチなWEBページを作ったり。。。すごいですねぇ
これから勉強して、またここにアウトプットしていこうと思います。
補足(参考リンク)
Promiseとasync/await
https://qiita.com/suin/items/97041d3e0691c12f4974
https://qiita.com/toshihirock/items/e49b66f8685a8510bd76#comments