配列の要素を関数に渡して、それらの結果から新たに配列を作るためにはmap()メソッドを使う
やりたかったこと
gulpで、配列に定義したディレクトリをつくるタスクを定義しようとしました。 別のタスクで書き出し先を決めてしまっているため、そのまえにディレクトリが存在していてほしいという感じです。
そこで、Node.jsのchild_process.exec
を使おうとしました。これはNode.jsにビルトインされている、シェルコマンドを実行するためのメソッドです。これにmkdir
コマンドと配列の要素をわたしてつくればいいんじゃね?という考えにいたりました。
しかし、このchild_process.exec
コマンドは非同期処理のコマンドです。child_process.execSync
を使うという手もあったものの、せっかくなのでしっかりPromiseを使えるようになろうという気持ちで挑戦してみました。
TL;DR
結論からいうとできたやつはこれです。
const gulp = require('gulp') const exec = require('child_process').exec const directories = [ './hoge/fuga', './hoge/piyo', './piyo' ] function execMkdir(name) { return new Promise((resolve, reject) => { exec(`mkdir -p ${name}`, (error, stdout, stderr) => { if (error) return reject(error) if (stderr) return reject(stderr) return resolve(true) }) }) } gulp.task('generate-directory', () => Promise.all(directories.map(app => execMkdir(app))) )
解説
配列を定義
作りたいディレクトリが3つあって、配列に定義しています。
const directories = [ './hoge/fuga', './hoge/piyo', './piyo' ]
これらのディレクトリを生成したいという状態です。
child_process.execメソッド
Node.jsでシェルコマンドを実行するにはchild_process
のexec
メソッドを使います。
Child Process | Node.js v8.7.0 Documentation
exec
メソッドは非同期処理なので、ターミナルに「はいこれよろしくー」と命令を出した後にさっさか次に進んでしまいます。これでは、処理が完了しているわけでもないのに次の処理が始まってしまう可能性があります。これだと意味がなくて、全てしっかり終了したことを確認してから次の処理を行う必要があります。
ちなみにchild_process
にはexecSync
というメソッドもあります。exec
メソッドを同期処理にするやつです。
Child Process | Node.js v8.7.0 Documentation
これを使えばいちいち処理が完了をまつので、順番に行われはします。しかし、非同期処理のgulp上で同期処理のメソッドをいれるのもなーとか、あとは同期・非同期をしっかり理解して扱えるようになりたいなーというのもあり、ここは苦手なPromise
に挑戦してみることにしました。
Promiseを返す関数を定義
流れ的には次の通りです。
Promise
オブジェクトをつくる- 関数を実行する
- 実行した関数のコールバックで
reject
かresolve
を返す
実際の関数は次のとおりです。
function execMkdir(name) { return new Promise((resolve, reject) => { exec(`mkdir -p ${name}`, (error, stdout, stderr) => { if (error) return reject(error) if (stderr) return reject(stderr) return resolve(true) }) }) }
この関数が呼び出されると、処理が完了するたびにPromise
オブジェクトが返されます。
Promise.allメソッドでPromiseをキャッチ
あとは、タスク側で先述の関数をループさせ、ループするたびに返されるPromise
オブジェクトを全部キャッチできた時点で「全部終わった」という判断がなされます。それらの処理をキャッチするのがPromise.all()
メソッドです。
Promise.all(iterable);
Promise.all() - JavaScript | MDN
iterable
とはES2015から追加された概念で、反復処理プロトコルのことです。iterable
の意味は「反復可能」。JavaScriptにビルトインされているものでいうとString
、Array
、TypedArray
、Map
、Set
の5つの型がiterable
だそうです。
そのほかにもジェネレーター関数を用いることでユーザー定義できるようですがここでは取り扱いません。
要は、3つの処理を全て配列(Array
)に叩き込んでPromise.all()
メソッドに渡せば処理完了という感じです。その後、メソッドチェーンされたthen()
メソッドに処理が移ります。例としては次のような形です。
var p1 = Promise.resolve(3); var p2 = 1337; var p3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, "foo"); }); Promise.all([p1, p2, p3]).then(function(values) { console.log(values); // [3, 1337, "foo"] });
mapメソッドで、配列を関数に渡してあらたな関数をつくる
先述のように一つ一つのPromiseオブジェクトや数値が名前を持っていれば、それぞれ配列の要素として代入するだけでいいのですが、今回は一つの関数を配列の要素のぶんだけループさせる処理をしたい、いったいどうすれば?と言うときに教えてもらったのがmap()
メソッドです。
Array.prototype.map() - JavaScript | MDN
map()
メソッドは、与えられた関数を配列のすべての要素に対して呼び出し、その結果からなる新しい配列を生成します。
これで、配列の全ての要素に対する処理からあらたな配列を生成することができます。
ということで、処理はこんな感じに。
const gulp = require('gulp') const exec = require('child_process').exec const directories = [ './hoge/fuga', './hoge/piyo', './piyo' ] // function execMkdir(name) { return new Promise((resolve, reject) => { exec(`mkdir -p ${name}`, (error, stdout, stderr) => { if (error) return reject(error) if (stderr) return reject(stderr) return resolve(true) }) }) } gulp.task('generate-directory', () => Promise.all(directories.map(app => execMkdir(app))) )
これで、いちいち処理するたびにPromiseを返し、全てPromiseを受け取った時点で次の処理にすすむという処理がかけました。