APIが提供されていないWebサービスのデータをプログラマティックに処理したいようなときにはpuppeteerをよく使うのですが、プロジェクトの規模が大きくなってくると、複雑な非同期処理のつじつま合わせに時間をとられるようになっていき、結局書いたコードを放棄するというようなサイクルを数年前から辿っていました
async/awaitを使うと同期実行っぽく書けるのですが、毎回毎回同じ文字を関数の前に書くのも芸がないし、かといってPromiseはインデントが深くなるし、コールバック地獄になるしで、うまい方法はないか考えていましたが、自分なりにしっくりくる方法が見つかったので、紹介します
const It=class {
constructor(it){
this.it=Promise.resolve(it);
this.last=Promise.resolve();
}
does(fn, ...args){
this.last=this.last.then(
dummy => this.it.then(
it => it[fn](...args)
)
);
return this.last;
}
}
クラス名とかファイル名は何でもいいんですが、上のコードでいうと
- コンストラクタで何らかのオブジェクト(コードでは
it)を受け取って、それをPromiseでくるんで持っておく -
doesという関数で1のPromiseからitを取り出して、itのメンバ関数を実行する(doesの第一引数で呼び出したいメンバ関数名を指定、第二引数以降はメンバ関数の引数) -
lastで最後に実行された3の結果を保持しておく(これもPromiseになる)
このクラスを拡張することで例えば、以下のようなことができます(puppeteerのラッパークラス)
const Browser=class extends It {
constructor(){
super(browser);
}
}
const Page=class extends It {
constructor(){
super(
new Browser()
.does('newPage')
);
}
}
const page=new Page();
page.does('on', 'console',
msg => console.log(msg.text())
);
page.does('goto', 'https://qiita.com/');
page.does('evaluate', ()=>{
console.log('doing');
return 'done'
}).then(console.log);
// doing
// done
does('on' ~ does('goto' ~ does('evaluate' ~ はそれぞれ前の処理が完了してから次の処理が実行されます(同期実行)
Itクラスの内部でPromiseを延々とつなげているだけなんですが、インデントしないで処理がかけます
戻り値で何か処理をしたい場合はdoes('evaluate' ~の部分のようにthen/catchで受け取ることもできます
あとasync/awaitの場合は、await中は関数の処理が止まるのですが、この書き方だとあくまでPromiseなので、後続の処理で別のPromiseがあれば、待たずに実行されます
毎回doesを書くのが嫌なら、必要に応じてdoesを隠蔽するfacade関数を各クラスに実装しておけばよいです