Edited at

複数のファイルにまたがるPromiseの使い方


複数ファイルにまたがるPromiseの使い方

まず、複数ファイルにまたがるPromiseの使い方を予習します。それがこの記事のメインです。最後に、Ionicでの例に適用します。

一つのファイル内でのPromiseチェーンについての例はよく見かけるけど、複数ファイルをまたがるケースはあまり見ないような気がしたのでここで書いてみます。

async、awaitについては触れません。

ではいきます。


状況

こんな状況があるとします。

ファイルAの処理Aを実行する↓

処理AがファイルBの処理Bを呼ぶ↓

処理BがファイルCの処理Cを呼ぶ↓

処理Cが完了する↓

処理Cの完了後に処理Bが完了する↓

処理Bの完了後に処理Aが完了する。

ただし、処理cは、失敗したり、時間がかかる可能性があるものとします。

※※注意!!※※

そのような、失敗する可能性があるPromiseは、実際に失敗した場合の処理をきちんと書くことが推奨されます。具体的には、.thenの二つ目の引数や、.catchに処理を書きます。しかし、この記事内では簡単のため省略しています。


うまくいかないコード

すごく素朴に書くと、こんな感じになります。あ、コードはTypeScriptとしますね。JavaScriptの場合は、型宣言を読み飛ばしていただければ同じです。


fileA.ts

import { ClassB } from './fileB';

const instanceB: ClassB = new ClassB();
instanceB.functionB();//処理Bを実行してから
console.log("A is done.");//処理Aの完了を報告(のつもり)


fileB.ts

import { ClassC } from './fileC';

export class ClassB{
functionB():void{
const instanceC: ClassC = new ClassC();
instanceC.functionC();//処理Cを実行してから
console.log("B is done.");//処理Bの完了を報告(のつもり)
}
}


fileC.ts

export class ClassC{           

functionC():void{
setTimeout(()=>{
console.log("C is done.");
},1000);//処理Cの実行には1秒かかる
}
}

想定している出力は、実行後1秒経ってから

C is done.

B is done.
A is done.

が表示される、という流れですが、残念ながらこうなりません。

やってみると

B is done.

A is done.
C is done.

となります。BとAの行が速攻で表示され、Cの行がその1秒後に表示されます。

つまり、Cの処理完了を待たずにBとAが先走ってるわけですね。

これだと、例えば、処理Bの後半に処理Cの完了が前提となるような処理があった場合に、動きがメチャクチャになってしまいます。

こういう「他の処理の完了を待たない」という動きはJavaScriptの特徴で、メリットもあるんですが、今回はそれを防ぎたいわけです。


処理Bが処理Cを待つように書き換える

ということで、いよいよPromiseを使っていきます。

まずは、処理Bが処理Cを待つように書き換えてみましょう。

fileCをPromiseを返すように書き換え、

fileBはそのPromiseの処理完了を待ってから続きの処理を実行するように書き換えます。

fileAはそのままです。


fileA.ts

import { ClassB } from './fileB';

const instanceB: ClassB = new ClassB();
instanceB.functionB();
console.log("A is done.");


fileB.ts

import { ClassC } from './fileC';

export class ClassB{
functionB():void{
const instanceC:ClassC = new ClassC();
instanceC.functionC().then(()=>{//処理Cの完了を待つ
console.log("B is done.");
});
}
}


fileC.ts

export class ClassC{

functionC():Promise<void>{//functionCの返り値はPromise
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log("C is done.");
resolve();//これが処理Cの完了通知
},1000);
});
}
}

fileCではPromiseを生成しています。

でも多分、多くの人は、Promiseを生成する機会ってあまりなくて、基本的には受け取る立場になると思うので、見て欲しいのはfileBです。

console.log("B is done.")を、引数を受け取らないアロー関数として、thenに与えています。

実行結果はこちら。

A is done.

C is done.
B is done.

一行目が即表示され、一秒まってから、二行目と三行目が一気に表示されます。

処理Bが処理Cを待ってくれたんですね。

いい感じです。


処理Aが処理Bを待つように書き換える

処理Aを書き換えますが、処理BもPromiseを返すように、書き換えが必要です。fileCはそのままでOK。


fileA.ts

import { ClassB } from './fileB';

const instanceB: ClassB = new ClassB();
instanceB.functionB().then(()=>{//処理Bの完了を待つ
console.log("A is done.");
});


fileB.ts

import { ClassC } from './fileC';

export class ClassB{
functionB():Promise<void>{//functionBの返り値はPromise
const instanceC:ClassC = new ClassC();
return instanceC.functionC().then(()=>{//thenの返り値をreturn
console.log("B is done.");
});
}
}


fileC.ts

export class ClassC{

functionC():Promise<void>{
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log("C is done.");
resolve();
},1000);
});
}
}

fileAの書き換えは先程のfileBの時と同じで、console.log("A is done.")を、引数を受け取らないアロー関数としてthenに与えています。

fileBの書き換えがキモで、instanceC.functionC().then(...の実行結果をreturnしています。

Promiseのthenメソッドの返り値は、またPromiseになります。

それに伴い、functionB()の返り値の型はvoidからPromise<void>に変化しています。

実行結果

C is done.

B is done.
A is done.

コマンド実行してから1秒経過後に、この三行が一気に表示されます。想定どおりの動きです!


Promiseで値を返す場合

処理Bが処理Aに値を返す場合を考えてみましょう。

fileAを次のようします。


fileA.ts

import { ClassB } from './fileB';

const instanceB: ClassB = new ClassB();
instanceB.functionB().then(x=>{
console.log("A has received: " + x);
});

thenに与えるアロー関数が値を受け取るようになっていて、その値をコンソールに出力するようになっています。

fileBのfunctionBが何か値を返してくれることを想定しているわけです。そのように書き換えましょう。


こう書くと失敗します!


fileB.ts

import { ClassC } from './fileC';

export class ClassB{
functionB():Promise<string>{
const instanceC:ClassC = new ClassC();
instanceC.functionC().then(()=>{//ここが間違い1
console.log("B is done.");
return Promise.resolve("OH! Yeah!!");//ここが間違い2
});
}
}

console.log("B is done.");の後に、Promiseを生成して返しているように見えます。

Promise.resolve(値)で、その値を持ったPromiseを生成できるんです。

でもこれ実行すると、次のエラーメッセージが出ます。

error TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.

Promise<string>型の値を返すと言っているのに、値を返してないぜ!」という内容です。

これは何故かと言うと、よく見るとreturn new Promise.resolve("OH! Yeah!!");は、thenに与えているアロー関数の中に書いてあるんですね。

つまりこの生成したPromiseは、thenに与えた関数の返り値であって、functionBの返り値になってないんですよ。

ではどうすればいいかと言うと、このthenを実行しているPromiseの行をまるごとreturnすれば良いんです。

thenの返り値もまたPromiseになるんでしたね。そしてそのPromiseの中身は、thenに与えた関数の返り値が入るんです。


こう書けば良かったんです!


fileB.ts

import { ClassC } from './fileC';

export class ClassB{
functionB():Promise<string>{
const instanceC:ClassC = new ClassC();
return instanceC.functionC().then(()=>{//thenの返り値をreturn
console.log("B is done.");
return "OH! Yeah!!";//ここでは単にstringを返す
});
}
}

これで、functionBの返り値はPromiseになり、そのPromiseの中には"OH! Yeah!!"という文字列が入ります。

実行結果

C is done.

B is done.
A has received: OH! Yeah!!

コマンド実行から1秒経過後に、この三行が一気に表示されます。


IonicのNavControllerはProviderからは使えない

最後にIonicの話をします。

Providerで何か処理をして、その処理のあとでユーザーを別のページに移動させたい時、Provider内の処理の中にNavControllerによる移動処理を書いても、動きません。

まあ、「データのやりとりをする」というProviderの働きを考えると、ユーザの移動制御が専門外というのもまあわかります。

ということで、こちらの質疑応答の方法を参考に、解決しましょう。

https://forum.ionicframework.com/t/controlling-navigation-from-within-a-provider/94376/2

リンク先の方法を要約すると、Providerから処理の最後にObservable(RxJSライブラリ)経由で合図を送って、それをきっかけにPageやComponent側で移動処理をするというものです。

でも別にこれRxJSライブラリのObservableじゃなくて、最初から使えるPromiseでいけます。

Observableのメリットは、一つのObservableから複数の値を繰り返し送出したり、便利なオペレータでそれらの値を処理することができることですが、

今回のケースでは、そういうことをしませんからね。

Observableについてわかりやすく書いた、僕の過去記事はこちら。


コード

importとかexportとかクラス宣言は省略します。各クラスの内側の部分だけ書きます。


fugaComponent.ts

...

constructor(public navCtrl: NavController, private hogeProvider: HogeProvider){}
...
fuga():void{
this.hogeProvider.functionHoge().then(()=>{//Provider側の処理を待つ
this.navCtrl.push("AnotherPage");//lazy loadを使用する場合、この引数は文字列
}).catch(e=>console.log(e));
}
...


hogeProvider.ts

...

functionHoge():Promise<void>{
return プロミスが返る処理1.then(()=>{
プロミスが返る処理1の後にすべき処理;
}).catch(e=>console.log(e));
}
...

ざっくりこんな感じですかね。

ということでこの記事は以上です。

誰かの参考になれば幸いです。


記事内サンプルの実行環境について

・TypeScriptを使います。JavaScriptの場合は型宣言を適当に読み飛ばしてください。

・tsc Version 2.7.2

・node v8.9.4

この環境で、他ファイルをモジュールとしてimportするようなTypeScriptコードを書いて、tscでJavaScriptに翻訳してnodeで実行するのですが

この状況でimport機能とPromise機能を両方使うには、ちょっとだけ面倒な手順が必要です。nodeが早くimportに対応してくれると良いんですが…。

実際に何かしらのフレームワーク環境(Ionicとか)で作業している場合は、あまり気にしなくていいです。


手順


  1. importを含むTypeScriptコードを書きます

  2. コマンドtsc -t es2017 fileA.tsでJavaScriptに翻訳します

  3. できた全てのJavaScriptファイルの拡張子を.jsから.mjsに変更します

  4. コマンドnode --experimental-modules fileA.mjsで実行します

と、このようになります。拡張子変更が面倒ですね。

tscのオプション-t es2017で、翻訳するJavaScript(ECMA Script)のバージョンを指定します。es2015やes2017を指定しないと、肝心のPromiseが使えません。

コマンドnodeには、オプション--experimental-modulesと、拡張子.mjsのファイルを与えます。2018年10月14日現在、nodeがデフォルトでimportに対応していないためです。

詳しくはこちら

実行環境については以上です。再掲ですが、実際に何かしらのフレームワーク環境(Ionicとか)で作業していて問題が発生していない場合は、あまり気にしなくていいです。


シリーズ一覧

最近(2018年10月)、Ionic3でAndroidアプリを作って、Play Storeで公開しました。

そこに至るにはいろいろと罠がありましたので、その記録を思い出せるうちに書いて残しておこうと思います。この記事はその中の一つというわけです。

困ったり解決したりする度にツイートしてたからいけるはず……!!

いくつかの記事にわけて、シリーズとしてまとめておきます。

なお、なんとかリリースできたアプリはこちら。あなたの日本語入力速度を測定します。→フリックとローマジ 〜日本語入力スピード測定〜