Gulp Under the Hood:ストリームベースのタスク自動化ツールの構築

公開: 2022-03-11

現在、フロントエンド開発者は複数のツールを使用して日常業務を自動化しています。 最も人気のあるソリューションの3つは、Grunt、Gulp、Webpackです。 これらのツールはそれぞれ異なる哲学に基づいて構築されていますが、フロントエンドの構築プロセスを合理化するという同じ共通の目標を共有しています。 たとえば、Gruntは構成主導型ですが、Gulpはほとんど何も強制しません。 実際、Gulpは、開発者がコードを記述して、ビルドプロセスのフロー(さまざまなビルドタスク)を実装することに依存しています。

Gulp Under the Hood:ストリームベースのタスク自動化ツールの構築

これらのツールの1つを選択することになると、私の個人的なお気に入りはGulpです。 全体として、それはシンプルで高速で信頼性の高いソリューションです。 この記事では、Gulpに似た独自のツールを実装することで、Gulpが内部でどのように機能するかを確認します。

Gulp API

Gulpには、次の4つの単純な関数が付属しています。

  • gulp.task
  • gulp.src
  • gulp.dest
  • gulp.watch

これらの4つの単純な機能は、さまざまな組み合わせで、Gulpのすべてのパワーと柔軟性を提供します。 バージョン4.0では、Gulpはgulp.seriesとgulp.parallelの2つの新しい関数を導入しました。 これらのAPIを使用すると、タスクを直列または並列で実行できます。

これらの4つの機能のうち、最初の3つはGulpファイルにとって絶対に不可欠です。 コマンドラインインターフェイスからタスクを定義して呼び出すことができます。 4つ目は、ファイルが変更されたときにタスクを実行できるようにすることで、Gulpを真に自動化するものです。

Gulpfile

これは基本的なgulpfileです。

 gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); }); 

簡単なテストタスクについて説明します。 呼び出されたら、現在の作業ディレクトリにあるファイルtest.txtをディレクトリ./outにコピーする必要があります。 Gulpを実行して試してみてください。

 touch test.txt # Create test.txt gulp test

メソッド.pipeはGulpの一部ではなく、ノードストリームAPIであり、読み取り可能なストリーム( gulp.src('test.txt')によって生成される)と書き込み可能なストリーム( gulp.dest('out')によって生成される))を接続することに注意してください。 gulp.dest('out') )。 Gulpとプラグイン間のすべての通信はストリームに基づいています。 これにより、gulpfileコードをこのようにエレガントな方法で記述できます。

プラグに会う

Gulpがどのように機能するかがわかったので、Gulpのような独自のツールであるPlugを作成しましょう。

まず、plug.taskAPIから始めます。 タスクを登録できるようにし、タスク名がコマンドパラメータで渡された場合にタスクを実行する必要があります。

 var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }

これにより、タスクを登録できるようになります。 次に、このタスクを実行可能にする必要があります。 簡単にするために、個別のタスクランチャーは作成しません。 代わりに、プラグの実装に含めます。

コマンドラインパラメータで指定されたタスクを実行するだけです。 また、すべてのタスクが登録された後、次の実行ループでそれを実行しようとすることを確認する必要があります。 これを行う最も簡単な方法は、タイムアウトコールバック、またはできればprocess.nextTickでタスクを実行することです。

 process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });

次のようにplugfile.jsを作成します。

 var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })

…そしてそれを実行します。

 node plugfile.js test

次のように表示されます。

 hello plug

サブタスク

Gulpでは、タスク登録時にサブタスクを定義することもできます。 この場合、plug.taskは、名前、サブタスクの配列、およびコールバック関数の3つのパラメーターを受け取る必要があります。 これを実装しましょう。

タスクAPIを次のように更新する必要があります。

 var tasks = {}; function onTask(name) { if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){ tasks[name] = { subTasks: arguments[1], callback: arguments[2] }; } else if(typeof arguments[1] === "function"){ tasks[name] = { subTasks: [], callback: arguments[1] }; } else{ console.log('invalid task registration') } } function runTask(name){ if(tasks[name].subTasks){ tasks[name].subTasks.forEach(function(subTaskName){ runTask(subTaskName); }); } if(tasks[name].callback){ tasks[name].callback(); } } process.nextTick(function(){ if (taskName && tasks[taskName]) { runTask(taskName); } });

次に、plugfile.jsが次のようになっている場合:

 plug.task('subTask1', function(){ console.log('from sub task 1'); }) plug.task('subTask2', function(){ console.log('from sub task 2'); }) plug.task('test', ['subTask1', 'subTask2'], function(){ console.log('hello plug'); })

…それを実行する

node plugfile.js test

…表示する必要があります:

 from sub task 1 from sub task 2 hello plug

Gulpはサブタスクを並行して実行することに注意してください。 ただし、簡単にするために、実装ではサブタスクを順番に実行しています。 Gulp 4.0では、この記事の後半で実装する2つの新しいAPI関数を使用してこれを制御できます。

ソースと宛先

ファイルの読み取りと書き込みを許可しない場合、プラグはほとんど役に立ちません。 次に、 plug.srcを実装します。 Gulpのこのメソッドは、ファイルマスク、ファイル名、またはファイルマスクの配列のいずれかである引数を想定しています。 読み取り可能なノードストリームを返します。

今のところ、 srcの実装では、ファイル名のみを許可します。

 var plug = { task: onTask, src: onSrc }; var stream = require('stream'); var fs = require('fs'); function onSrc(fileName){ var src = new stream.Readable({ read: function (chunk) { }, objectMode: true }); //read file and send it to the stream fs.readFile(path, 'utf8', (e,data)=> { src.push({ name: path, buffer: data }); src.push(null); }); return src; }

ここではオプションのパラメータであるobjectMode: trueを使用していることに注意してください。 これは、ノードストリームがデフォルトでバイナリストリームで機能するためです。 ストリームを介してJavaScriptオブジェクトを送受信する必要がある場合は、このパラメーターを使用する必要があります。

ご覧のとおり、人工オブジェクトを作成しました。

 { name: path, //file name buffer: data //file content }

…そしてそれをストリームに渡しました。

一方、plug.destメソッドは、ターゲットフォルダー名を受け取り、.srcストリームからオブジェクトを受け取る書き込み可能なストリームを返す必要があります。 ファイルオブジェクトが受信されるとすぐに、それはターゲットフォルダに保存されます。

 function onDest(path){ var writer = new stream.Writable({ write: function (chunk, encoding, next) { if (!fs.existsSync(path)) fs.mkdirSync(path); fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> { next() }); }, objectMode: true }); return writer; }

pluginfile.jsを更新しましょう:

 var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })

test.txtを作成します

touch test.txt

…そしてそれを実行します:

 node plugfile.js test ls ./out

test.txt./outフォルダーにコピーする必要があります。

Gulp自体もほぼ同じように機能しますが、人工的なファイルオブジェクトの代わりに、ビニールオブジェクトを使用します。 ファイル名とコンテンツだけでなく、現在のフォルダ名、ファイルへのフルパスなどの追加のメタ情報も含まれているため、はるかに便利です。 コンテンツバッファ全体が含まれているとは限りませんが、代わりにコンテンツの読み取り可能なストリームがあります。

ビニール:ファイルよりも優れている

ビニールオブジェクトとして表されるファイルを操作できる優れたライブラリビニルfsがあります。 基本的に、ファイルマスクに基づいて読み取り可能で書き込み可能なストリームを作成できます。

Vinyl-fsライブラリを使用してプラグ関数を書き換えることができます。 ただし、最初にVinyl-fsをインストールする必要があります。

 npm i vinyl-fs

これをインストールすると、新しいプラグの実装は次のようになります。

 var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...

…そしてそれを試すために:

 rm out/test.txt node plugFile.js test ls out/test.txt

結果は同じであるはずです。

Gulpプラグイン

プラグインサービスはGulpストリーム規則を使用しているため、プラグツールと一緒にネイティブGulpプラグインを使用できます。

試してみましょう。 gulp-renameをインストールします。

 npm i gulp-rename

…そしてそれを使用するためにplugfile.jsを更新します:

 var plug = require('./app.js'); var rename = require('gulp-rename'); plug.task('test', function () { return plug.src('test.txt') .pipe(rename('renamed.txt')) .pipe(plug.dest('out')); });

プラグファイル.jsを実行しても、ご想像のとおり、同じ結果が得られるはずです。

 node plugFile.js test ls out/renamed.txt

変更の監視

最後になりましたが重要なメソッドはgulp.watchこのメソッドを使用すると、ファイルリスナーを登録し、ファイルが変更されたときに登録済みのタスクを呼び出すことができます。 それを実装しましょう:

 var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }

試してみるには、次の行をplugfile.jsに追加してください。

 plug.watch('test.txt','test');

これで、 test.txtを変更するたびに、ファイルの名前が変更されたoutフォルダーにコピーされます。

シリーズvsパラレル

GulpのAPIの基本的な機能がすべて実装されたので、さらに一歩進んでみましょう。 今後のバージョンのGulpには、より多くのAPI関数が含まれる予定です。 この新しいAPIにより、Gulpはより強力になります。

  • gulp.parallel
  • gulp.series

これらのメソッドを使用すると、ユーザーはタスクが実行される順序を制御できます。 サブタスクを並列に登録するには、現在のGulpの動作であるgulp.parallelを使用できます。 一方、gulp.seriesは、サブタスクを順番に実行するために使用できます。

現在のフォルダーにtest1.txttest2.txtがあるとします。 これらのファイルを並行してoutフォルダーにコピーするには、プラグファイルを作成します。

 var plug = require('./plug'); plug.task('subTask1', function(){ return plug.src('test1.txt') .pipe(plug.dest('out')) }) plug.task('subTask2', function(){ return plug.src('test2.txt') .pipe(plug.dest('out')) }) plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){ console.log('done') }) plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){ console.log('done') })

実装を簡素化するために、サブタスクコールバック関数はそのストリームを返すように作成されています。 これは、ストリームのライフサイクルを追跡するのに役立ちます。

APIの修正を開始します。

 var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };

タスクランチャーがサブタスクを適切に処理できるように、タスクメタ情報を追加する必要があるため、 onTask関数も更新する必要があります。

 function onTask(name, subTasks, callback){ if(arguments.length < 2){ console.error('invalid task registration',arguments); return; } if(arguments.length === 2){ if(typeof arguments[1] === 'function'){ callback = subTasks; subTasks = {series: []}; } } tasks[name] = subTasks; tasks[name].callback = function(){ if(callback) return callback(); }; } function onParallel(tasks){ return { parallel: tasks }; } function onSeries(tasks){ return { series: tasks }; }

簡単にするために、非同期関数を処理してタスクを並列または直列に実行するためのユーティリティライブラリであるasync.jsを使用します。

 var async = require('async') function _processTask(taskName, callback){ var taskInfo = tasks[taskName]; console.log('task ' + taskName + ' is started'); var subTaskNames = taskInfo.series || taskInfo.parallel || []; var subTasks = subTaskNames.map(function(subTask){ return function(cb){ _processTask(subTask, cb); } }); if(subTasks.length>0){ if(taskInfo.series){ async.series(subTasks, taskInfo.callback); }else{ async.parallel(subTasks, taskInfo.callback); } }else{ var stream = taskInfo.callback(); if(stream){ stream.on('end', function(){ console.log('stream ' + taskName + ' is ended'); callback() }) }else{ console.log('task ' + taskName +' is completed'); callback(); } } }

ストリームがすべてのメッセージを処理して閉じたときに発行されるノードストリーム「end」に依存します。これは、サブタスクが完了したことを示します。 async.jsを使用すると、コールバックの大きな混乱に対処する必要はありません。

試してみるために、最初にサブタスクを並行して実行しましょう。

 node plugFile.js test-parallel
 task test-parallel is started task subTask1 is started task subTask2 is started stream subTask2 is ended stream subTask1 is ended done

そして、同じサブタスクを連続して実行します。

 node plugFile.js test-series
 task test-series is started task subTask1 is started stream subTask1 is ended task subTask2 is started stream subTask2 is ended done

結論

これで、GulpのAPIが実装され、Gulpプラグインを使用できるようになりました。 もちろん、実際のプロジェクトではプラグインを使用しないでください。Gulpはここで実装したもの以上のものです。 この小さな演習が、Gulpが内部でどのように機能するかを理解し、より流暢に使用してプラグインで拡張できるようになることを願っています。

関連: Gulpを使用したJavaScript自動化の概要