nayucolony

勉強したこととか

配列の要素を関数に渡して、それらの結果から新たに配列を作るためには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_processexecメソッドを使います。

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オブジェクトをつくる
  • 関数を実行する
  • 実行した関数のコールバックでrejectresolveを返す

実際の関数は次のとおりです。

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にビルトインされているものでいうとStringArrayTypedArrayMapSetの5つの型がiterableだそうです。

反復処理プロトコル - JavaScript | MDN

そのほかにもジェネレーター関数を用いることでユーザー定義できるようですがここでは取り扱いません。

function* - JavaScript | MDN

要は、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を受け取った時点で次の処理にすすむという処理がかけました。