Gulp
ビルドツール
Gulp
概要
Gulpは、Node.jsベースのJavaScriptタスクランナーです。Eric Schoffstall氏によって2013年に開発され、「ストリーミングビルドシステム」として設計されています。Gruntの設定ベースアプローチとは対照的に、コードベースのアプローチを採用し、Node.jsストリームAPIを活用してファイル変換処理を効率的に実行します。Gulpfile.jsでタスクを定義し、プラグインを組み合わせることで、Sass/LESSコンパイル、JavaScript圧縮、ファイル監視、ライブリロードなどの開発ワークフローを自動化します。現在はWebpackやViteに移行しているプロジェクトが多いものの、シンプルな処理には今でも選ばれています。
詳細
主要機能
- ストリーミング処理: メモリ効率的なファイル変換処理
- コードベース設定: JavaScriptによる柔軟なタスク定義
- プラグインエコシステム: 4,000以上の豊富なプラグイン
- ファイル監視: ファイル変更の検出と自動タスク実行
- 並列・直列処理: 効率的なタスク実行順序制御
- glob パターン: 柔軟なファイル選択機能
- エラーハンドリング: 詳細なエラー処理とデバッグ機能
アーキテクチャ
Node.jsストリームAPIを基盤とし、vinyl-fsによるファイルシステム抽象化。プラグインによる変換パイプラインとガルプタスクシステムによる効率的な処理フロー。
エコシステム
gulp-*プラグイン群、gulp-cli、vinyl、orchestrator。現在はWebpack、Rollup、Parcel等のモダンバンドラーが主流。
メリット・デメリット
メリット
- 高いパフォーマンス: ストリーミング処理による高速なファイル操作
- コードベース: JavaScript知識でタスクを直感的に記述
- 学習コストの低さ: Node.jsの知識があれば理解しやすい
- 柔軟性: プログラマブルな制御とカスタマイズ性
- メモリ効率: 中間ファイルを作成せずメモリ内処理
- プラグイン豊富: 多様な用途に対応するプラグイン群
- デバッグしやすさ: JavaScriptなので標準的なデバッグ手法が使用可能
デメリット
- 学習曲線: ストリーミング概念の理解が必要
- 設定の複雑化: 大規模プロジェクトでは設定が複雑
- プラグイン依存: 多くの機能がプラグインに依存
- モダンツールとの差: WebpackやViteと比較して機能不足
- メンテナンス状況: 新機能開発が停滞気味
- エラーハンドリング: ストリーム処理でのエラー追跡が困難な場合
参考ページ
書き方の例
インストールとプロジェクトセットアップ
# Gulp CLI をグローバルインストール
npm install -g gulp-cli
# プロジェクトでのGulp本体インストール
npm install gulp --save-dev
# プロジェクト初期化
npm init -y
# 基本的なプラグインインストール
npm install gulp-sass --save-dev
npm install gulp-uglify --save-dev
npm install gulp-concat --save-dev
npm install gulp-cssnano --save-dev
npm install gulp-imagemin --save-dev
npm install gulp-watch --save-dev
npm install browser-sync --save-dev
# Gulpfile.js作成
touch gulpfile.js
# タスク実行
gulp # デフォルトタスク実行
gulp build # buildタスク実行
gulp watch # watchタスク実行
gulp --tasks # タスク一覧表示
基本的なgulpfile.js
const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const uglify = require('gulp-uglify');
const concat = require('gulp-concat');
const cssnano = require('gulp-cssnano');
const imagemin = require('gulp-imagemin');
const browserSync = require('browser-sync').create();
// パスの定義
const paths = {
styles: {
src: 'src/scss/**/*.scss',
dest: 'dist/css/'
},
scripts: {
src: 'src/js/**/*.js',
dest: 'dist/js/'
},
images: {
src: 'src/images/**/*',
dest: 'dist/images/'
},
html: {
src: 'src/**/*.html',
dest: 'dist/'
}
};
// Sassコンパイルタスク
function styles() {
return gulp.src(paths.styles.src)
.pipe(sass({
outputStyle: 'expanded',
includePaths: ['node_modules']
}))
.on('error', sass.logError)
.pipe(cssnano())
.pipe(gulp.dest(paths.styles.dest))
.pipe(browserSync.stream());
}
// JavaScript処理タスク
function scripts() {
return gulp.src(paths.scripts.src)
.pipe(concat('app.js'))
.pipe(uglify())
.pipe(gulp.dest(paths.scripts.dest))
.pipe(browserSync.stream());
}
// 画像最適化タスク
function images() {
return gulp.src(paths.images.src)
.pipe(imagemin([
imagemin.gifsicle({interlaced: true}),
imagemin.mozjpeg({quality: 80, progressive: true}),
imagemin.optipng({optimizationLevel: 5}),
imagemin.svgo({
plugins: [
{removeViewBox: true},
{cleanupIDs: false}
]
})
]))
.pipe(gulp.dest(paths.images.dest));
}
// HTMLコピータスク
function html() {
return gulp.src(paths.html.src)
.pipe(gulp.dest(paths.html.dest))
.pipe(browserSync.stream());
}
// ブラウザ同期タスク
function serve(done) {
browserSync.init({
server: {
baseDir: './dist'
},
port: 3000
});
done();
}
// ファイル監視タスク
function watchFiles() {
gulp.watch(paths.styles.src, styles);
gulp.watch(paths.scripts.src, scripts);
gulp.watch(paths.images.src, images);
gulp.watch(paths.html.src, html);
}
// タスクエクスポート
exports.styles = styles;
exports.scripts = scripts;
exports.images = images;
exports.html = html;
exports.serve = serve;
exports.watch = watchFiles;
// 複合タスク
const build = gulp.series(
gulp.parallel(styles, scripts, images, html)
);
const dev = gulp.series(
build,
gulp.parallel(serve, watchFiles)
);
// デフォルトタスク
exports.build = build;
exports.dev = dev;
exports.default = dev;
TypeScriptとLintingの統合
const gulp = require('gulp');
const ts = require('gulp-typescript');
const eslint = require('gulp-eslint');
const sourcemaps = require('gulp-sourcemaps');
const browserify = require('browserify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const uglify = require('gulp-uglify');
const rename = require('gulp-rename');
// TypeScriptプロジェクト設定
const tsProject = ts.createProject('tsconfig.json');
// パス設定
const paths = {
typescript: {
src: 'src/ts/**/*.ts',
dest: 'dist/js/'
},
javascript: {
src: 'src/js/**/*.js',
dest: 'dist/js/'
}
};
// TypeScriptコンパイル
function typescript() {
return gulp.src(paths.typescript.src)
.pipe(sourcemaps.init())
.pipe(tsProject())
.on('error', function(error) {
console.log('TypeScript compilation error:', error.toString());
this.emit('end');
})
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest(paths.typescript.dest));
}
// ESLint
function lint() {
return gulp.src([paths.javascript.src, paths.typescript.src])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
}
// Browserifyバンドル
function bundle() {
return browserify({
entries: 'src/js/main.js',
debug: true
})
.bundle()
.pipe(source('bundle.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}))
.pipe(uglify())
.pipe(rename({suffix: '.min'}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('dist/js/'));
}
// テスト実行
function test() {
return gulp.src('test/**/*.js')
.pipe(require('gulp-mocha')({
reporter: 'spec',
timeout: 5000
}));
}
// 監視とライブリロード
function watchFiles() {
gulp.watch(paths.typescript.src, gulp.series(lint, typescript));
gulp.watch(paths.javascript.src, gulp.series(lint, bundle));
gulp.watch('test/**/*.js', test);
}
// タスクエクスポート
exports.typescript = typescript;
exports.lint = lint;
exports.bundle = bundle;
exports.test = test;
exports.watch = watchFiles;
// 複合タスク
exports.build = gulp.series(
lint,
gulp.parallel(typescript, bundle)
);
exports.dev = gulp.series(
exports.build,
gulp.parallel(test, watchFiles)
);
exports.default = exports.dev;
高度なワークフローとプラグイン統合
const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));
const postcss = require('gulp-postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const webpack = require('webpack-stream');
const babel = require('gulp-babel');
const imagemin = require('gulp-imagemin');
const webp = require('gulp-webp');
const svgSprite = require('gulp-svg-sprite');
const rev = require('gulp-rev');
const revRewrite = require('gulp-rev-rewrite');
const del = require('del');
const notify = require('gulp-notify');
const plumber = require('gulp-plumber');
// 環境変数
const isDevelopment = process.env.NODE_ENV !== 'production';
// エラーハンドリング
const handleErrors = function() {
return plumber({
errorHandler: notify.onError({
title: 'Gulp Error',
message: '<%= error.message %>'
})
});
};
// クリーンアップ
function clean() {
return del(['dist/**', '!dist']);
}
// スタイル処理
function styles() {
const processors = [
autoprefixer({browsers: ['last 2 versions']}),
...(isDevelopment ? [] : [cssnano()])
];
return gulp.src('src/scss/**/*.scss')
.pipe(handleErrors())
.pipe(sass({
includePaths: ['node_modules'],
outputStyle: isDevelopment ? 'expanded' : 'compressed'
}))
.pipe(postcss(processors))
.pipe(gulp.dest('dist/css/'));
}
// JavaScript処理(Webpack統合)
function scripts() {
return gulp.src('src/js/main.js')
.pipe(handleErrors())
.pipe(webpack({
mode: isDevelopment ? 'development' : 'production',
output: {
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
devtool: isDevelopment ? 'source-map' : false
}))
.pipe(gulp.dest('dist/js/'));
}
// 画像最適化とWebP変換
function images() {
return gulp.src('src/images/**/*.{jpg,jpeg,png,gif}')
.pipe(imagemin([
imagemin.mozjpeg({quality: 80, progressive: true}),
imagemin.optipng({optimizationLevel: 5}),
imagemin.gifsicle({interlaced: true})
]))
.pipe(gulp.dest('dist/images/'))
.pipe(webp({quality: 80}))
.pipe(gulp.dest('dist/images/'));
}
// SVGスプライト生成
function svgSpriteGeneration() {
return gulp.src('src/icons/**/*.svg')
.pipe(svgSprite({
mode: {
symbol: {
sprite: '../sprite.svg',
example: {
dest: '../sprite-preview.html'
}
}
}
}))
.pipe(gulp.dest('dist/images/'));
}
// アセット・リビジョン(キャッシュバスティング)
function revision() {
return gulp.src(['dist/**/*.{css,js,jpg,jpeg,png,gif,webp,svg}'])
.pipe(rev())
.pipe(gulp.dest('dist/'))
.pipe(rev.manifest())
.pipe(gulp.dest('dist/'));
}
// HTMLでのアセット参照更新
function revisionRewrite() {
const manifest = gulp.src('dist/rev-manifest.json');
return gulp.src('dist/**/*.html')
.pipe(revRewrite({manifest}))
.pipe(gulp.dest('dist/'));
}
// 開発サーバー
function serve(done) {
const browserSync = require('browser-sync').create();
browserSync.init({
server: {
baseDir: './dist'
},
port: 3000,
open: true,
notify: false
});
// ファイル監視
gulp.watch('src/scss/**/*.scss', styles);
gulp.watch('src/js/**/*.js', scripts);
gulp.watch('src/images/**/*', images);
gulp.watch('src/**/*.html', copyHtml);
// ブラウザリロード
gulp.watch('dist/**/*').on('change', browserSync.reload);
done();
}
// HTML複製
function copyHtml() {
return gulp.src('src/**/*.html')
.pipe(gulp.dest('dist/'));
}
// ファイル監視
function watchFiles() {
gulp.watch('src/scss/**/*.scss', styles);
gulp.watch('src/js/**/*.js', scripts);
gulp.watch('src/images/**/*', gulp.series(images, svgSpriteGeneration));
gulp.watch('src/**/*.html', copyHtml);
}
// タスクエクスポート
exports.clean = clean;
exports.styles = styles;
exports.scripts = scripts;
exports.images = images;
exports.svgSprite = svgSpriteGeneration;
exports.serve = serve;
exports.watch = watchFiles;
// 開発用ビルド
exports.dev = gulp.series(
clean,
gulp.parallel(styles, scripts, images, svgSpriteGeneration, copyHtml),
serve
);
// 本番用ビルド
exports.build = gulp.series(
clean,
gulp.parallel(styles, scripts, images, svgSpriteGeneration, copyHtml),
revision,
revisionRewrite
);
// デフォルトタスク
exports.default = exports.dev;
カスタムプラグインとストリーム操作
const gulp = require('gulp');
const through2 = require('through2');
const Vinyl = require('vinyl');
const path = require('path');
// カスタムプラグイン例:ファイル情報の追加
function addFileInfo() {
return through2.obj(function(file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
if (file.isBuffer()) {
const info = {
filename: path.basename(file.path),
size: file.contents.length,
modified: file.stat ? file.stat.mtime : new Date()
};
// ファイル情報をヘッダーコメントとして追加
const header = `/* File: ${info.filename}, Size: ${info.size} bytes, Modified: ${info.modified} */\n`;
file.contents = Buffer.concat([
Buffer.from(header),
file.contents
]);
}
callback(null, file);
});
}
// カスタムプラグイン:複数ファイルを1つに結合
function bundleFiles(filename) {
let files = [];
return through2.obj(
function(file, encoding, callback) {
if (file.isNull()) {
return callback();
}
files.push(file);
callback();
},
function(callback) {
if (files.length === 0) {
return callback();
}
const bundled = new Vinyl({
path: filename,
contents: Buffer.concat(files.map(f => f.contents))
});
this.push(bundled);
callback();
}
);
}
// 条件付きパイプライン
function conditionalPipe(condition, transform) {
return condition ? transform : through2.obj(function(file, encoding, callback) {
callback(null, file);
});
}
// 並列ストリーム処理
function processFiles() {
const jsStream = gulp.src('src/js/**/*.js')
.pipe(addFileInfo())
.pipe(conditionalPipe(process.env.NODE_ENV === 'production', require('gulp-uglify')()))
.pipe(gulp.dest('dist/js/'));
const cssStream = gulp.src('src/css/**/*.css')
.pipe(addFileInfo())
.pipe(conditionalPipe(process.env.NODE_ENV === 'production', require('gulp-cssnano')()))
.pipe(gulp.dest('dist/css/'));
return require('merge-stream')(jsStream, cssStream);
}
// ファイル変換ストリーム
function transformMarkdown() {
const marked = require('marked');
return through2.obj(function(file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
if (file.isBuffer() && path.extname(file.path) === '.md') {
const html = marked(file.contents.toString());
file.contents = Buffer.from(html);
file.path = file.path.replace('.md', '.html');
}
callback(null, file);
});
}
// 複雑なファイル操作例
function advancedProcessing() {
return gulp.src('src/**/*.{js,css,md}')
.pipe(through2.obj(function(file, encoding, callback) {
// ファイルタイプ別の分岐処理
const ext = path.extname(file.path);
switch(ext) {
case '.js':
// JavaScript固有の処理
console.log(`Processing JavaScript: ${file.path}`);
break;
case '.css':
// CSS固有の処理
console.log(`Processing CSS: ${file.path}`);
break;
case '.md':
// Markdown処理
console.log(`Processing Markdown: ${file.path}`);
break;
}
callback(null, file);
}))
.pipe(transformMarkdown())
.pipe(gulp.dest('dist/'));
}
// エラーハンドリング付きストリーム
function robustProcessing() {
return gulp.src('src/**/*.js')
.pipe(through2.obj(function(file, encoding, callback) {
try {
// 何らかの処理
const processed = someComplexProcessing(file.contents);
file.contents = processed;
callback(null, file);
} catch (error) {
// エラーをストリームに伝播
callback(new Error(`Processing failed for ${file.path}: ${error.message}`));
}
}))
.on('error', function(error) {
console.error('Stream error:', error.message);
this.emit('end'); // ストリームを正常終了
})
.pipe(gulp.dest('dist/'));
}
// 模擬的な複雑処理関数
function someComplexProcessing(contents) {
// 実際の処理ロジック
return contents;
}
// タスクエクスポート
exports.addInfo = function() {
return gulp.src('src/**/*.js')
.pipe(addFileInfo())
.pipe(gulp.dest('dist/'));
};
exports.bundle = function() {
return gulp.src('src/lib/**/*.js')
.pipe(bundleFiles('bundle.js'))
.pipe(gulp.dest('dist/'));
};
exports.process = processFiles;
exports.markdown = advancedProcessing;
exports.robust = robustProcessing;
exports.default = processFiles;
パフォーマンス最適化とデバッグ
const gulp = require('gulp');
const log = require('fancy-log');
const colors = require('ansi-colors');
const size = require('gulp-size');
const debug = require('gulp-debug');
const cache = require('gulp-cache');
const remember = require('gulp-remember');
const cached = require('gulp-cached');
const newer = require('gulp-newer');
const filter = require('gulp-filter');
// パフォーマンス測定
function measurePerformance(taskName) {
const start = Date.now();
return through2.obj(function(file, encoding, callback) {
callback(null, file);
}, function(callback) {
const duration = Date.now() - start;
log(colors.blue(`Task ${taskName} completed in ${duration}ms`));
callback();
});
}
// 増分ビルド
function incrementalStyles() {
return gulp.src('src/scss/**/*.scss')
.pipe(cached('styles')) // キャッシュチェック
.pipe(debug({title: 'Processing:'}))
.pipe(sass())
.pipe(remember('styles')) // キャッシュに保存
.pipe(concat('main.css'))
.pipe(gulp.dest('dist/css/'))
.pipe(size({showFiles: true, title: 'Styles'}));
}
// 変更されたファイルのみ処理
function onlyChanged() {
return gulp.src('src/images/**/*')
.pipe(newer('dist/images/')) // 新しいファイルのみ
.pipe(debug({title: 'New images:'}))
.pipe(imagemin())
.pipe(gulp.dest('dist/images/'))
.pipe(size({showFiles: true, title: 'Images'}));
}
// 条件付きフィルタリング
function conditionalProcessing() {
const jsFilter = filter('**/*.js', {restore: true});
const cssFilter = filter('**/*.css', {restore: true});
return gulp.src('src/**/*.{js,css}')
.pipe(jsFilter)
.pipe(debug({title: 'JS files:'}))
.pipe(uglify())
.pipe(jsFilter.restore)
.pipe(cssFilter)
.pipe(debug({title: 'CSS files:'}))
.pipe(cssnano())
.pipe(cssFilter.restore)
.pipe(gulp.dest('dist/'));
}
// メモリ使用量監視
function monitorMemory() {
const memUsage = process.memoryUsage();
log(colors.yellow(`Memory usage: ${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`));
}
// ファイル監視の最適化
function optimizedWatch() {
// 初期ビルド
gulp.series(incrementalStyles)();
gulp.watch('src/scss/**/*.scss', incrementalStyles)
.on('unlink', function(filepath) {
// ファイル削除時のキャッシュクリア
delete cached.caches.styles[filepath];
remember.forget('styles', filepath);
});
}
// デバッグ情報付きタスク
function debugBuild() {
return gulp.src('src/**/*')
.pipe(debug({
title: 'Files:',
showCount: true,
showFiles: true
}))
.pipe(measurePerformance('debug-build'))
.pipe(gulp.dest('dist/'))
.pipe(size({
title: 'Total size:',
showFiles: false,
showTotal: true
}));
}
// タスクエクスポート
exports.incremental = incrementalStyles;
exports.changed = onlyChanged;
exports.conditional = conditionalProcessing;
exports.debug = debugBuild;
exports.watch = optimizedWatch;
// パフォーマンス監視付きデフォルトタスク
exports.default = gulp.series(
function startMessage(done) {
log(colors.green('Starting optimized build...'));
monitorMemory();
done();
},
gulp.parallel(incrementalStyles, onlyChanged),
function endMessage(done) {
log(colors.green('Build completed!'));
monitorMemory();
done();
}
);