旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section4 ~Gulpで処理を自動化しよう~

  • 202
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

この記事は「旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門」の4つ目の記事です。

シリーズの最初から読みたい方は
旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section1 ~すぐにでも現代っぽく出来るワンポイントまとめ~
へどうぞ。

また、このシリーズではECMAScript5を概ね対応するブラウザを対象としています。

もっと平たくいうと、IE8以下は切り捨てます。ご了承ください。

そしてプロによるマサカリ :knife: 対策として一つ重要な注意書きをします。

この記事中で出てくる「CommonJSモジュール」という表現は全て「CommonJSのModules 1.0仕様をベースとして、Node.jsが独自に拡張したCommonJS派生のモジュール仕様及びインターフェース(require/exports.○○/module.exports)」を指すものとします。
標準仕様策定(プロジェクト)としてのCommonJS」や「CommonJS Modules 1.0仕様そのもの」を指すものではありません。

このシリーズを通して、原則として厳密さよりも分かりやすさを優先するためこのようにします。予めご了承ください。

目次

Section4 ~Gulpで処理を自動化しよう~

前回、Browserifyを用いてCommonJSモジュールをブラウザ向けに動作できるような変換処理について簡単に説明しました。今回はその一連の流れを自動化する話をしたいと思います。

因みに処理の自動化周りの話というのは、大規模開発において重要なポジションを担います。
そのため、実際に検索をしてみるとプロ :sunglasses: によるテクニカルな部分もある側面があります。

このシリーズでは様々な技術の紹介はしていきたいので、Gulp(というかタスクランナー)の話も当然触れていきたいわけですが、あくまでもこのシリーズはタイトルにもある通り「超入門」なので、一番最初の触りの部分だけ触れていきたいです :beginner:

そもそも「処理を自動化する」ということについて

一番最初に注意書きをします。

処理を自動化したからといって、何もないところから勝手にソースコードが生まれてくるわけではありません:exclamation: :exclamation: :exclamation:

自動化しても、あくまで(自分が)書いたスクリプトの範囲内で、手続きが自動化されるだけなのです。

要するに我々は「プログラムを生み出すためのプログラムを書く必要がある」ということを注意しなければなりません。

これがどういう意味かというと、

「将来長期に渡って管理・保守をしない場合には、自動化スクリプトを書く暇あったら、(本当に書きたい)プログラムの実装を進めたほうが早い場合もある。

ということです。 :confounded:

この性質を理解していないと、我々は「プログラムを生むためのプログラムを生むためのプログラムを…………を生むための自動化スクリプト」を書くことになります。
(無限ループって怖くね? :scream: )

ところで、ある程度しっかりしたプログラムになると長期サポートが必要になってきますよね。
その度につらつらとシステムコマンドやファイル操作をするのは面倒ですし、そもそも複雑なコマンドはヒューマンエラーのもとです。 :no_good:
(ついでに言えば、大型プロジェクトは複数人で作ることがほとんどです。他の人に「糞長い詠唱コマンド覚えるの面倒くせえ…」と言われないように自動化芸が必要になります。)

自動化は上手に付き合えば便利です。反面下手に付き合えば無限に面倒くさいです。

この点を留意しつつ、この機会にぜひとも自動化を覚えましょう :thumbsup: :sparkles:

Gulpとは何か

さて、本題のGulpの話に入ります。
前回話したとおり、gulpはNode.js上で動くタスクランナーの一つです。

http://gulpjs.com/

かなり有名なタスクランナーの一つで、今も盛んに利用されているものの一つです。

例えばMicrosoft製マルチプラットフォームテキストエディタのVS Codeのような有名なOSSにも利用されています。

https://github.com/Microsoft/vscode

↑のGitHubにアクセスすると「gulpfile.js」っていうのがルートディレクトリにあるじゃろ?それがgulpのメインスクリプトじゃw :triumph:

GulpとBrowserify環境を作る

前回のBrowserifyの時とかなり似ています。ただし、今回はプロジェクトディレクトリも作ります。

  1. プロジェクトを作りたい適当な場所にディレクトリに移動します。
  2. そこにフォルダを作ります。ただし「半角の小文字英数および-」で作ります。(分かる人だけに正規表現で説明すると /^[a-z][a-z0-9-]*$/ です)
  3. (Windowsユーザーは)Shiftキーを押しながら、今作成したフォルダを右クリック⇒「コマンドウィンドウをここで開く」を選択します(コマンドプロンプトが開きます) コマンドウィンドウで開く.png
  4. 出てきたコマンドプロンプト上で「npm init」と打ち、エンター
  5. ごちゃごちゃ出てきますが全部気にせず「エンター」連打します :beginner:
  6. 出てきたコマンドプロンプト上で「npm i -g gulp-cli」と打ち、エンター(初回のみ)
  7. 出てきたコマンドプロンプト上で「npm i -D gulp」と打ち、エンター(毎回)
  8. 出てきたコマンドプロンプト上で「npm i -D browserify」と打ち、エンター(毎回)
  9. 「gulpfile.js」というファイルを作る(中身何も書かなくていいです)
  10. 「src」という名前のフォルダと「dist」という名前の空フォルダを作る

(初回)のみ、って書いてる奴は今回だけ。(毎回)って書いてる奴はプロジェクトを作る度に必要になる手順です :relaxed:

ここまでを行うとフォルダの中身は

  • node_modules
  • src
  • dist
  • gulpfile.js
  • package.json

の5つになってるかと思います。
フォルダ構成.png

これである程度の環境は整いました :laughing:

(補足)npmについて

前回のセクションからしれっとnpmの説明もせずにnpmコマンド連打してます。
これについて話すとこれだけで1つ記事が出来るぐらい内容が濃いので、ここでは触れません。
詳しく知りたい方は公式の英語を読むといいです。 :pray:

https://docs.npmjs.com/getting-started/what-is-npm

実際に使ってみる

実際に自動化してみましょう。まずは動作確認をします。
ということでいきなりbrowserifyを使用しないで、単純にファイルの入出力だけします :smirk:

なお、前回と同じサンプルも流石に飽きてきたと思いますので、今回は新しいモジュールを作り、それをサンプルとします :two_hearts:
(というかjQueryとかいう紛らわしい名前のサンプルだったっていうのもありましたし)

HtmlSpecialChars.js
module.exports = HtmlSpecialChars;

function HtmlSpecialChars(list){
    if (!(this instanceof HtmlSpecialChars)) {
        return new HtmlSpecialChars(list);
    }

    // privateメンバを列挙しないようにする
    Object.defineProperties(this,{
        _escapeList: {
            writable: true,
        },
        _unescapeList: {
            writable: true,
        },
        _escapeRegExp: {
            writable: true,
        },
        _unescapeRegExp: {
            writable: true,
        },
    });

    // 引数なしは受け流す
    if(list === void(0)) return;

    this.escapeList = list;
}

Object.defineProperties(HtmlSpecialChars.prototype,{
    escapeList: {
        enumerable: true,
        set: function(value){
            if(typeof(value) !== "object"){
                throw new TypeError("escapeListはObject型のプロパティです");
            }

            // privateメンバに格納
            var escapeList = this._escapeList = value;

            // {key => value}の連想配列から、{value => key}の逆連想配列を生成する
            var keys   = Object.keys(escapeList);
            var values = keys.map(function(key){
                return escapeList[key];
            });
            this._unescapeList = keys.reduce(function(obj,key,i){
                obj[values[i]] = key;
                return obj;
            },{});

            // キーの情報からエスケープする際の正規表現を作る
            this._escapeRegExp = new RegExp("(" + keys.map(escapeRegExp).join("|") + ")","g");

            // バリューの情報からアンエスケープする際の正規表現を作る
            this._unescapeRegExp = new RegExp("(" + values.map(escapeRegExp).join("|") + ")","g");
        },
        get: function(){
            return this._escapeList;
        },
    },
    unescapeList: {
        enumerable: true,
        get: function(){
            return this._unescapeList;
        },
    },
    escapeRegExp: {
        enumerable: true,
        get: function(){
            return this._escapeRegExp;
        },
    },
    unescapeRegExp: {
        enumerable: true,
        get: function(){
            return this._unescapeRegExp;
        },
    },
    escape: {
        value: function(str){
            var _this = this;
            return str.replace(this.escapeRegExp,function($0){
                return _this.escapeList[$0];
            });
        },
    },
    unescape: {
        value: function(str){
            var _this = this;
            return str.replace(this.unescapeRegExp,function($0){
                return _this.unescapeList[$0];
            });
        },
    },
});

// エスケープが必要な正規表現シーケンスをエスケープする
// (※サンプルコードなので実装ガバガバです)
function escapeRegExp(str){
    return str.replace(/[/*.[\]()|]/g,function($0){
        return "\\" + $0;
    });
}
index.js
var HtmlSpecialChars = require("./HtmlSpecialChars.js");

var h = new HtmlSpecialChars({
    '&': '&',
    '<': '&lt;',
    '>': '&gt;',
});
console.log("エスケープ");
var escapedStr = h.escape('<script>alert("XSS");</script>');
console.log(escapedStr); // '&lt;script&gt;alert("XSS");&lt;/script&gt;'
console.log("アンエスケープ");
var unescapedStr = h.unescape(escapedStr);
console.log(unescapedStr); // '<script>alert("XSS");</script>;'

実装はわりとどうでもいいのでindex.jsの方だけ見て入出力を把握してもらえればと思います。
命名がペチパーチックなのは仕様です。でもPHPは嫌いです :broken_heart:

閑話休題、この「index.js」と「HtmlSpecialChars.js」を先ほど作った「src」ディレクトリの中に置きます。

  • node_modules
  • src
    • index.js
    • HtmlSpecialChars.js
  • dist
  • gulpfile.js
  • package.json

そして、gulpfile.jsを以下のように記述します。

gulpfile.js
var gulp = require("gulp");

gulp.task("default",function(){
    return gulp.src("./src/*.js")
        .pipe(gulp.dest("./dist"));
});

ナンデ!?gulpのCommonJSモジュール自作してないのに
い、いつの間にかgulpがrequire出来るようになってる…!!!! :flushed: :flushed: :flushed:

…はい。勘の良い人は気づくかもしれませんが、実は先ほどの作業の中で

出てきたコマンドプロンプト上で「npm i -D gulp」と打ち、エンター

と魔法の呪文を唱えたタイミングで、えらい人が作った「gulp」のCommonJSモジュールを、ネット上からDLしてきており、
その瞬間あなたのPC上で使えるようにいたのです!!!! :stuck_out_tongue_winking_eye: (ナ、ナンダッテー)

gulpの使い方について

先ほどのコマンドプロンプト(フォルダをShift+右クリックで開いたやつ)で、

gulp default

もしくは、defaultの処理の場合は単に

gulp

と打ちます。するとgulpfileの中身に従って処理が行われます。
今回のサンプルコードだと、「index.js」と「HtmlSpecialChars.js」が「dist」フォルダにコピーされたら成功です :heart:

  • node_modules
  • src ←この中に入ってるデータが
    • index.js
    • HtmlSpecialChars.js
  • dist ←ここの中へコピーされる
    • index.js
    • HtmlSpecialChars.js
  • gulpfile.js
  • package.json

gulpfileの書き方について

まず、なにはともあれ思考停止でgulpモジュールをrequireします

gulpfile.js
var gulp = require("gulp");

そして、タスクを定義します。
タスクっていうのは、大雑把に言うとコマンドプロンプト上で「gulp ○○」
って打ち込む、「○○」に相当する部分を定義するものです。

task関数は実は引数が2つのものと3つのものとあるのですが、簡単な2つの方を使用しています。

gulpfile.js
// "default"だけは特別で、「gulp default」でも「gulp」でもこのタスクが呼ばれる
gulp.task("default",function(){
    // ここにやりたい処理を書く
});

// "default"の部分を"qiita"書き換えると、このタスクは
// 「gulp qiita」で実行できるようになります。
gulp.task("qiita",function(){
    // ここにやりたい処理を書く
});

で、実際の処理についてなのですが、「gulp.src()」で入力をまず指定します。
指定方法は、globと呼ばれる形式です。Unix系の利用者だと馴染みある形式かと思いますが、Windowsユーザーだとあまり馴染みないと思います。知らない方は各自ググってください。

globにも色々記述あるのですが基本的に、「*」でワイルドカードだけ覚えとけば幸せです()

ここでは、「srcフォルダ直下にある任意の「.js」という拡張子で終わるファイル」を指定しています。

gulpfile.js
gulp.src("./src/*.js")

そして、.srcで指定した入力に対して、.pipeで様々な処理を連結していくことでgulpのタスクを記述していきます。

今回の例では、「distフォルダに出力する」という処理を.pipeで連結しています。
この処理で、結果的に

「srcフォルダの中身(.jsファイル)をdistへのコピーするタスク」

となります :two_hearts:

gulpfile.js
gulp.src("./src/*.js") // .jsで終わるファイルを入力として受け付ける
    // .pipe(間にはさみたい処理1)
    // .pipe(間にはさみたい処理2)
    // .pipe(間にはさみたい処理3)
    // ...(以下略)
    .pipe(gulp.dest("./dist")); // 処理結果をdistフォルダに出力する

Browserifyの処理をpipeする

さて、これでgulpの使い方を完全に理解しました。あなたはプロです :sunglasses:
というわけで、先ほどの記述にBrowserifyの処理を間に挟んで自動化しましょう :heart:

因みに先ほどのコピー処理でdistに生まれたファイル達について話しますが、
以降はBrowserify処理をするので、distの中身を :boom: 削除 :boom: して空っぽにしておいてください :point_up:

Browserifyの処理のはさみ方ですが、Browserifyぐらい有名なモジュールだとgulp公式の「recipes」のページにやり方が書いてあったりします。

https://github.com/gulpjs/gulp/tree/master/docs/recipes#recipes
https://github.com/gulpjs/gulp/blob/master/docs/recipes/browserify-with-globs.md

しかしこのコードは、Browserifyするだけではなく、uglify(ソースコードを圧縮して容量を軽くしたり読みづらくする処理)や、SourceMap(読みづらくなった後でも元ファイルとのコード対応関係を見れるようにする定義)についても加えています :anguished:

今回は問題を簡単にするために、それら処理は取り除いて、このrecipesからBrowserifyの処理だけ抜き出してコピペします

コピペは正義だ。コピペは生活を豊かにする

gulpfile.js
'use strict';

var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var globby = require('globby');
var through = require('through2');

gulp.task('default', function () {
  // gulp expects tasks to return a stream, so we create one here.
  var bundledStream = through();

  bundledStream
    // turns the output bundle stream into a stream containing
    // the normal attributes gulp plugins expect.
    .pipe(source('index.js'))
    // the rest of the gulp task, as you would normally write it.
    // here we're copying from the Browserify recipe.
    .pipe(buffer())
    .pipe(gulp.dest('./dist'));

  // "globby" replaces the normal "gulp.src" as Browserify
  // creates it's own readable stream.
  globby(['./src/*.js']).then(function(entries) {
    // create the Browserify instance.
    var b = browserify({
      entries: entries,
    });

    // pipe the Browserify stream into the stream we created earlier
    // this starts our gulp pipeline.
    b.bundle().pipe(bundledStream);
  }).catch(function(err) {
    // ensure any errors from globby are handled
    bundledStream.emit('error', err);
  });

  // finally, we return the stream, so gulp knows when this task is done.
  return bundledStream;
});

そして、コピペしたソースコードを読むと、幾つかのCommonJSモジュールがまだインストールできてないので、例のnpm魔法コマンドでDLしてきます :sunglasses:

  1. 「npm i -D vinyl-source-stream」と打ち、エンター
  2. 「npm i -D vinyl-buffer」と打ち、エンター
  3. 「npm i -D globby」と打ち、エンター
  4. 「npm i -D through2」と打ち、エンター

この状態で、「gulp」もしくは「gulp default」と打ち込んでもらうと、「dist」フォルダに「index.js」というファイルが生成されてるかと思います。

  • node_modules
  • src ←この中に入ってるデータが
    • index.js
    • HtmlSpecialChars.js
  • dist
    • index.js ←Browserifyされてブラウザ上で動く単一ファイルにまとめられる
  • gulpfile.js
  • package.json

「index.js」がCommonJSモジュールを変換してブラウザ上で動作するようにしたjsファイルです。
無事にBrowserifyが完了しましたね :blush:

gulp.watchを使って監視する

Browserifyへの変換を自動化することに成功しましたが、どうせなら

元ファイルを書き換えたタイミングで自動でBrowserifyして欲しい

って思いませんか?思いますよね?思わなくても思うって言ってくださいね?

実は、gulpにはファイル変化を監視する機能があります。
試しに、browserifyの処理を監視するタスク「watch」を定義しましょう。

gulpfile.js
'use strict';

var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var globby = require('globby');
var through = require('through2');

gulp.task('default', function () {
    // ここにさっきコピペした糞長いコードがつらつらと
});

// 新たに「watch」という名前のタスクを定義する
gulp.task('watch', function () {
    gulp.watch('./src/*.js', ['default']);
});

これで「./src/*.js」のファイルを監視して、ファイルの内容が書き換わったタイミングで自動で「default」タスクの処理を実行する

という内容のタスクを定義することが出来ました :kissing_smiling_eyes:

実行は「gulp watch」で出来ましたね。やってみましょう
gulpwatch.png

watchというタスクを実行して、監視を始めました。

因みに監視を中断するには、コマンドプロンプト上で「Ctrl+C」を押せばいつでも中断できます(バッチジョブ云々が出る場合はもう一回Ctrl+Cを押してください)

試しにこの状態で「./src/index.js」のファイルの末尾を書き換えてみましょう

index.js
var HtmlSpecialChars = require("./HtmlSpecialChars.js");

var h = new HtmlSpecialChars({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
});
console.log("エスケープ");
var escapedStr = h.escape('<script>alert("XSS");</script>');
console.log(escapedStr); // '&lt;script&gt;alert("XSS");&lt;/script&gt;'
console.log("アンエスケープ");
var unescapedStr = h.unescape(escapedStr);
console.log(unescapedStr); // '<script>alert("XSS");</script>;'

console.log("ファイルを書き換えたぞ監視しろ"); // ←この行を追加

書き換えて、スクリプトを上書き保存すると、コンソール上で「default」タスクが実行されたことが表示されるかと思います。
gulpwatch.png

そして、出力された「./dist/index.js」の中身も自動で書き換わっていることが確認できると思います :heartpulse:

index.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = HtmlSpecialChars;

function HtmlSpecialChars(list){
    //めっちゃ長いので省略
}

Object.defineProperties(HtmlSpecialChars.prototype,{
    //めっちゃ長いので省略
});

// エスケープが必要な正規表現シーケンスをエスケープする
// (※サンプルコードなので実装ガバガバです)
function escapeRegExp(str){
    //めっちゃ短いけど省略
}

},{}],2:[function(require,module,exports){
var HtmlSpecialChars = require("./HtmlSpecialChars.js");

var h = new HtmlSpecialChars({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
});
console.log("エスケープ");
var escapedStr = h.escape('<script>alert("XSS");</script>');
console.log(escapedStr); // '&lt;script&gt;alert("XSS");&lt;/script&gt;'
console.log("アンエスケープ");
var unescapedStr = h.unescape(escapedStr);
console.log(unescapedStr); // '<script>alert("XSS");</script>;'

console.log("ファイルを書き換えたぞ監視しろ"); // ←この行を追加

},{"./HtmlSpecialChars.js":1}]},{},[1,2]);

無事末尾にconsole.logが追加されていますね :sunglasses:


というわけで、Section4についての内容はここまでとなります。

Gulpについてはもっともっともっともっともーっっと語るべきことがあるんですが、入門編としてはこの辺りが限度なので留めておきます :confounded:

次回Section5では、現代的なJS文法について触れていきます。
Section5で一旦GulpやBrowserifyといった話から離れますが、Section6辺りで再びGulp周りの話が出てきます :grin:
(というかこの話をするために延々と書いてきたようなもんですし。)

次の話はこちらっ
旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section5 ~ES2015文法を覚えよう(前編)~