JavaScriptの非同期処理(Promise)のWindowsストアアプリ向け実装版で苦戦したことまとめ

こんにちは、コンちゃんこと佐々木です。

ほとんど需要ありませんがWindowsストアアプリ(Windows8.1対象)を開発しています。
非同期処理でつまづいたので、まとめておきます。

– おしながき –
・非同期後にやりたい非同期じゃない処理の書き方
・非同期処理を含むループの書き方
・複数の非同期処理を複雑に組み合わせる

– keywords –
windowsストアアプリ、JavaScript、Promise、FlipView

_______________________________________________________

Windowsストアアプリは、C#のほかに、C++やJavaScript(以下JS)で開発することができます。
ネット情報はC#が多い気がします。

JSを選択する場合、画面構成やデザインをHTMLとCSSが、ロジック部分をJSが担当します。

さて、JSでのWindows開発において、FileIOやXMLHttpRequestなど、重たい処理にはPromises/Aという非同期処理モデルが採用されています。

Promise基礎は参考リンク(本ページ最下部)先をご覧ください。

さて、以下のコードは、「data4_*.txt(*は0~9)がローカルフォルダに存在するなら、FlipViewの最後(dataArrayの最後)にnewObjectを追加する」ことを期待して書いたがうまく動かなかったものです。

// 画像のリストを定義
var dataArray = [
    { type: "item", title: "絶壁", picture: "images/Cliff.jpg" },
    { type: "item", title: "葡萄", picture: "images/Grapes.jpg" },
    { type: "item", title: "恵比寿", picture: "images/test.jpg" },
    { type: "item", title: "レーニア山", picture: "images/Rainier.jpg" },
    { type: "item", title: "夕焼け", picture: "images/Sunset.jpg" },
    { type: "item", title: "渓谷", picture: "images/Valley.jpg" }
];

// その他のグローバル変数の宣言は省略
// プロジェクト作成時にすでに書かれていたデフォルト処理も省略
// initialize()呼び出し部分も省略

function initialize() {
        // http://blogs.msdn.com/b/osamum/archive/2014/09/09/deta-bind-using-winjs2-1.aspx
        // https://msdn.microsoft.com/ja-jp/library/windows/apps/hh700764.aspx
        // http://qiita.com/iwate/items/aeb077526ffdcf16a471
        var itemTemp = document.getElementById("ItemTemplate");
        var flipView = document.getElementById("FlipView").winControl;
        var folder = Windows.Storage.ApplicationData.current.localFolder;
        var textFileName = "";
        var msg = document.getElementById("msg").textContent;

        for (var i = 0; i < 10; i++) {
            console.log("i = "+i+" start");
            textFileName = "data4_" + i + ".txt";
            folder.getFileAsync(textFileName).then(function (file) {
                // success
                return Windows.Storage.FileIO.readTextAsync(file);
            }, function (err) {
                //error
                return null;
            }).done(function (data) {
                // success
                if (data != null) {
                    var newObject = { type: 'item', title: i, picture: data };
                    dataArray.push(newObject);
                    //itemTemp.winControl.render(newObject, FlipView);
                }else if (i == 9) {
                    //flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
                    //flipView.itemTemplate = itemTemp;
                }
                console.log("i = "+i);
            }, function (err) {
                // error
            });
        }
        // forの中は非同期なので、以下が先行実行?
        console.log("out of for  start");
        //flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
        //flipView.itemTemplate = itemTemp;
        console.log("out of for  end");
}

 このコードの問題点は2点。
(1)forループが終わってもループ内の非同期処理が終わっているとは言えない(手元のPC環境では終わらなかった)
(2)そのため、forループ後に書いた処理の方が、非同期処理がすべて終わるよりも先に実行される(51行目のように、ループ内処理に依存した処理は危険。非同期処理はいつ終わるか分からないので、実行のたびに51行目の結果が変わったりもするよ。

まずはforループを取り除いて(2)を考えてみましょう。

console.log("i = "+i+" start");
textFileName = "data4_" + i + ".txt";
folder.getFileAsync(textFileName).then(function (file) {
	// success
    return Windows.Storage.FileIO.readTextAsync(file);
}, function (err) {
    //error
    return null;
}).done(function (data) {
    // success
    if (data != null) {
        var newObject = { type: 'item', title: i, picture: data };
         dataArray.push(newObject);
        //itemTemp.winControl.render(newObject, FlipView);
    }else if (i == 9) {
        //flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
        //flipView.itemTemplate = itemTemp;
    }
    console.log("i = "+i);
}, function (err) {
    // error
});
// forの中は非同期なので、以下が先行実行?
console.log("out of for  start");
//flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
//flipView.itemTemplate = itemTemp;
console.log("out of for  end");

 さて、forループ後の処理を非同期処理の後に確実に実行するには、forループのPromiseチェーンの最後の処理を非同期化すれば良いです。

そして、さらにPromiseチェーンを続け、ループ後処理をそのチェーン内に書きます。

非同期化したコードが以下。
(注意:動作確認せずに感覚で書いたコードです、間違い指摘を歓迎いたします。)

console.log("i = "+i+" start");
textFileName = "data4_" + i + ".txt";
folder.getFileAsync(textFileName).then(function (file) {
	// success
    return Windows.Storage.FileIO.readTextAsync(file);
}, function (err) {
    //error
    return null;
}).then(function (data) {
    return new WinJS.Promise(
	    function (success, error, prog) {
	    	// success
		    if (data != null) {
		        var newObject = { type: 'item', title: i, picture: data };
		        dataArray.push(newObject);
		        //itemTemp.winControl.render(newObject, FlipView);
		        
		        success();
		        return;
		    }
		}
	);
}, function (err) {
    // error
}).done(function(){
	// success
	console.log("out of for  start");
	//flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
	//flipView.itemTemplate = itemTemp;
	console.log("out of for  end");
}, function (err){
	//error
});

 ポイントは、new WinJS.Promise(function(成功関数, 失敗関数, 進捗関数))で非同期処理を作っているところ。

…なんだか分かりにくいので一般化。

Method_A_Async().then(function(arg){
	// 処理
	return Method_B_Async();
}, function(err){
	// error処理
}).then(function(arg){
	// 処理
	return new WinJS.Promise(function(success, error, prog){
		// 処理
		success();
		return;
	});
}, function(err){
	// error処理
}).done(function(){
	// 最終処理
},function(err){
	// Promiseチェーンでエラーが発生すると、必ず最終チェーンのエラー関数まで伝播する.
	// つまりここが呼ばれる.
});

 これで、最終処理が非同期処理終了後に呼ばれることが保障されます。

 

続いて(1)非同期処理のforループについてです。

これについては、「ひだまりソケットは壊れない」さんの、以下の記事をご覧ください。

WinJS.Promise による非同期処理をループさせる関数 (Windows ストアアプリ)

記事内のloopAsync関数を用いると、こう書けます。

// loopAsync関数は記事内のものをそっくりそのままコピーしています.
// ですので、記載は省略します.

var folder = Windows.Storage.ApplicationData.current.localFolder;
var textFileName = "";
var msg = document.getElementById("msg").textContent;
var loopPromise = loopAsync(0, function (controller) {
    //dataArray = controller.prevStepValue;
    if (controller.count >= 9) {
        controller.stopLoop();
    }
    var tmp = controller.count;
    textFileName = "data4_" + tmp + ".txt";
    console.log("now for = " + textFileName);
    folder.getFileAsync(textFileName).then(function (file) {
        // success
        return Windows.Storage.FileIO.readTextAsync(file);
    }, function (err) {
        // error
        return null;
    }).then(function (data) {
        // success
        if (data != null) {
            var newObject = { type: 'item', title: tmp, picture: data };
            //dataArray.push(newObject);
            
            // このへんでcontroller.countを取ると、変数tmpと違う値になっている.
            //dataArray.splice(controller.count - 1, 1, newObject);
            dataArray.splice(tmp, 1, newObject);

            // 本番モードを使用可能にする
            document.getElementById("exeMode").disabled = false;

            //dataArray.push(newObject);
        }
        //return dataArray;
        return;
    }, function (err) {
        // error
        //return dataArray;
        return;
    });

});
loopPromise.done(function () {
    var itemTemp = document.getElementById("ItemTemplate");
    var flipView = document.getElementById("FlipView").winControl;
    flipView.itemDataSource = new WinJS.Binding.List(dataArray).dataSource;
    flipView.itemTemplate = itemTemp;
});

 loopAsync関数はPromiseを返すので、Promiseチェーンでつなぐことができます(45行目)。

これで、46~49行目はループが終わってから実行されることが保障されます。やった!

 

・複数の非同期処理を複雑に組み合わせる

forループの話から、複数の非同期処理の組み合わせの話に変わります。

以下のような非同期処理の連鎖を書きたい!

/*
 *                            +---> [file_reader.onload] ----(画像data)----+
 *                            |                                           |
 *                        (画像file)                                       |
 *                            |                                           |
 * [pickSingleFileAsync()] ---+---> [createFileAsync()] ----(テキストfile)--+--> [writeTextAsync()]
 *
 * 1. pickSingleFileAsync()はファイルピッカーで選択されたfileオブジェクトを返す.
 * 2. そのfileオブジェクトをDataURLに変換する(file_reader.readAsDataURL()).
 * 3. 同時に(ファイルピッカーで選択されたことをきっかけに)、ローカルにテキストファイルを作成する.
 * 4. 作成したテキストファイルに、画像データを書き込む.
 * 5. 以上.
 */

 さて、writeTextAsync()には2つの非同期処理の結果を渡す必要があります。

言い換えると、2つの処理がどちらも終わっていることを確認してからwriteTextAsync()しなければなりません。

ここで用いるのが、WinJS.Promise.join(promises)です。
joinの引数は、Promiseを返す関数の配列です。
コードにすると、こんな感じ。

var promises = [];
promises.push(OnLoad());
promises.push(CreateFile());
return WinJS.Promise.join(promises);
// Promiseに包まれた、結果の配列が返される

 結果はthen(またはdone)で受け取りましょう。

.then(function(args){
	// success
	// argsは結果が入っている配列である
	// 結果格納順番は、promisesにpushした順番と等しいようだ.
});

 上記を踏まえて書いたコードが以下になります。

ボタンクリックでRegister3()が呼ばれてスタートです。
ボタンに関数を紐付けるaddEventListener()コードは省略。

function Register3(e) {
    // ファイルピッカーを開く
    var view = Windows.UI.ViewManagement.ApplicationView.value;
    if (view === Windows.UI.ViewManagement.ApplicationViewState.snapped &&
        !Windows.UI.ViewManagement.ApplicationView.tryUnsnap()) {
        // TODO : tryUnsnap ha hisuishou
        return;
    }
    // FileOpenPickerオブジェクトを生成
    var picker = new Windows.Storage.Pickers.FileOpenPicker();
    // 表示モード
    picker.viewMode = Windows.Storage.Pickers.PickerViewMode.thumbnail;
    // 開くフォルダーの指定
    picker.suggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.picturesLibrary;
    // 表示ファイルをフィルター
    picker.fileTypeFilter.replaceAll([".png", ".jpg", ".jpeg"]);
    // 「開く」ボタンの表記を変更
    picker.commitButtonText = "決定";


    var flipView = document.getElementById("FlipView");
    var write_data = "";
    var fileName = "";

    var tt = picker.pickSingleFileAsync().then(function (file) {
        if (file) {
            currentPage = flipView.winControl.currentPage;

            // 追加のオブジェクトを生成
            newObject = { type: 'item', title: '-1', picture: 'no_picture' };
            // オブジェクトに追加写真のURIを設定
            newObject["picture"] = URL.createObjectURL(file, { oneTimeOnly: true });
            // TODO : 削除時ファイル名出したいので.
            //newObject["title"] = file.name;
            // for develop
            newObject["title"] = currentPage;

            // image change
            flipView.winControl.itemDataSource.list.setAt(currentPage, newObject);

            var promises = [];
            // return write_data
            promises.push(OnLoad(file));
            // return file(書き込むテキストファイル)
            promises.push(CreateFileAsync(currentPage));
            return WinJS.Promise.join(promises);
        } else {
            // file pick miss
            // throw new Error(); ??
            return new WinJS.Promise.wrapError();
        }
    }, function (err) {
        return new WinJS.Promise.wrapError(err);
    }).then(function (args) {
        return Windows.Storage.FileIO.writeTextAsync(args[1], args[0]);
    }, function (err) {
        // join miss
        return new WinJS.Promise.wrapError(err);
    }).then(
            function () {
                document.getElementById("msg").textContent = "file write success!";
            },
            function (e) {
                document.getElementById("msg").textContent = e;
            }
        );
}

// promiseで処理してwrite_dataを返す
function OnLoad(file) {
    // TODO : file read miss時の処理. onloaded?
    var file_reader = new FileReader();
    file_reader.readAsDataURL(file);
    return new WinJS.Promise(function (comp, err) {
        file_reader.onload = function (e) {
            document.getElementById("msg").textContent = file_reader.result;
            comp(file_reader.result);
        }
    });
}

function CreateFileAsync(currentPage) {
    // return new WinJS.Pro ... のnewがないとfileが返されないっぽい。new必須。
    return new WinJS.Promise(function (Comp, Err) {
        // 本番モードを使えるようにする
        document.getElementById("exeMode").disabled = false;

        // 本番モードから作成モードに復帰時用
        //nowDataArray = flipView.winControl.itemDataSource.list;
        //// nowDataArray.splice(currentPage, 1, newObject);

        // 画像データを書き込むテキストファイルを準備
        var folder = Windows.Storage.ApplicationData.current.localFolder;
        var mode = Windows.Storage.CreationCollisionOption.replaceExisting;
        var fileName = "data4_" + currentPage + ".txt";

        folder.createFileAsync(fileName, mode).then(
            function (file) {
                // create file success!!
                Comp(file);
            },
            function (err) {
                // create file miss
                Err(err);
            }
        );
    });
}

 promises.push()の引数にする関数は、promiseを返す必要が(たぶん)あります。

OnLoad()関数はPromiseを返すようにreturn new WinJS.Promise()しました。
CreateFileAsync()関数は、createFileAsync()が最終処理なので、これをreturnすればPromiseに包まれたfileが返されるだろーっと思っていたらうまくいかない謎。
なのでcreateFileAsync()をreturn new WinJS.Promise()で包みました。
これはうまくいきました。

一般化しました。

function Func(){
	Method_A_Async().then(
		function(arg){
			// success
			// 処理
			
			var promises = [];
			promises.push(Method_B_Async());
			promises.push(Method_C_Async());
			// promises.push(Method_D_Async());
			//         .
			//         .
			//         .
			
			return WinJS.Promise.join(promises);
			
		},function(err){
			// error
		}
	).then(
		function(args){
			// Promise.join() success
			// argsは結果が格納された配列である.
			
			// 処理
			
			// 以下のようにPromiseをreturnする関数を実行すると、
			// さらに非同期処理をつなげられる.
			return Method_Z_Async();
		},function(err){
			// error
		}
	).done(
		function(arg){
			// doneはpromiseチェーンの最終処理にのみ使用可能
			// thenでも良いし、正直違いは不明だが、
			// doneはチェーンの最後用にあるので、せっかくだから
			// 使ってあげよう.
		},function(err){
			// 最終チェーンのエラーは、チェーン中のいかなるエラーも捕捉する(はず).
			// エラー処理
		}
	);
}

 以上になります。

 

– 参考にした本・リンク –

・[本]HTMLとJavaScriptではじめるWindowsストアアプリ開発入門
店頭で買いました。
開発の大部分はこの本のおかげ。
(ライフサイクルとか非同期処理とかデータバインドとかフリップビューとかファイル選択ピッカーとかメディアファイルまわりとかローカルフォルダーとかMediaCaptureとか…)

・[本]JavaScriptで作るWindows ストアアプリ開発スタートガイド
持っていないがgoogle booksで一部分見られます。
「HTMLとJavaScriptではじめる~」より一段階レベルの高い本のようです。
非同期処理については両本とも触れられていますが、promise.joinについてはこの本のみ。
promise.joinに関する日本語情報は少なく、大変参考にした本です。

・ひだまりソケットは壊れないさんの以下2記事

Windows ストアアプリ (Metro スタイルアプリ) における JavaScript の非同期処理 (WinJS.Promise)
WinJS.Promise による非同期処理をループさせる関数 (Windows ストアアプリ)

日本語での解説記事!
いろいろ救われました。ありがとうございます。

・Windows 8 アプリ開発者ブログさん

promise の概要 (JavaScript による Windows ストア アプリ向け)

MS公式。
記事下部にいくほど難しい。
必要そうなところだけなんとか理解するなど。

MS公式リファレンス(MSDN)
リファレンス。だいたい英語、ときどき日本語。

ほか、見る必要に迫られなかったけど、参考になるかもしれない日本語記事を以下に。
(検索してさがすこともなかなかに大変だったりするので紹介しておきます)

・[本]JavaScriptによるWindows8.1アプリケーション構築

・ICHIJO3’S BLOGさん

Javascriptと”promise”-非同期通信を再勉強!with JQuery

・Internet Explorer ブログ (日本語版)さん

“promise” による JavaScript での非同期プログラミング

※IEではPromiseは使えないはずです。Windows 10の新ブラウザは使えるかも?

 

 

 

作ったアプリは今度ストア審査に出すよ!

それでは失礼。

コメントを残す

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)