Gulp 核心概念
Gulp 的核心概念和工作原理详解
📋 目录
Stream(流)
什么是 Stream
Stream 是 Gulp 的核心概念,所有文件操作都通过流完成。流是数据的管道,可以在不保存中间文件的情况下处理数据。
// 传统方式:读取文件 → 写入临时文件 → 处理 → 写入结果
const fs = require('fs');
const data = fs.readFileSync('src/app.js');
fs.writeFileSync('temp/app.js', data);
// ... 处理 temp/app.js
fs.writeFileSync('dist/app.js', processedData);
// Gulp Stream 方式:内存中处理
const { src, dest } = require('gulp');
src('src/app.js') // 读取文件(输入流)
.pipe(uglify()) // 压缩处理(转换流)
.pipe(dest('dist/')); // 写入文件(输出流)Stream 类型
// 1. 可读流(Readable Stream)
const readable = fs.createReadStream('src/app.js');
// 2. 可写流(Writable Stream)
const writable = fs.createWriteStream('dist/app.js');
// 3. 转换流(Transform Stream)
const { Transform } = require('stream');
const transform = new Transform({
transform(chunk, encoding, callback) {
// 处理数据
this.push(processedChunk);
callback();
}
});Gulp Stream 工作流程
文件系统
↓
gulp.src() ← 读取文件,创建可读流
↓
.pipe() ← 应用插件,转换流
↓
.pipe() ← 应用插件,转换流
↓
gulp.dest() ← 写入文件,创建可写流
↓
文件系统
Task(任务)
什么是 Task
Task 是 Gulp 的基本执行单元,一个 Task 是一个函数,执行特定的构建操作。
// Gulp 3.x 方式(已废弃)
gulp.task('scripts', function() {
return gulp.src('src/js/*.js')
.pipe(uglify())
.pipe(gulp.dest('dist/js'));
});
// Gulp 4.x 方式(推荐)
function scripts() {
return src('src/js/*.js')
.pipe(uglify())
.pipe(dest('dist/js'));
}
// 导出任务
exports.scripts = scripts;Task 类型
// 1. 异步任务(返回 Stream)
function scripts() {
return src('src/js/*.js')
.pipe(uglify())
.pipe(dest('dist/js'));
}
// 2. Promise 任务
function clean() {
return del(['dist/*']);
}
// 3. async/await 任务
async function build() {
await scripts();
await styles();
console.log('Build complete');
}
// 4. Callback 任务
function callbackTask(cb) {
console.log('Task running');
cb(); // 必须调用 callback
}Task 执行
# 执行单个任务
gulp scripts
gulp styles
gulp build
# 执行默认任务
gulpSrc(源文件)
基本用法
const { src } = require('gulp');
// 读取单个文件
src('src/app.js')
// 读取多个文件
src(['src/app.js', 'src/utils.js'])
// 使用通配符
src('src/**/*.js') // 所有 JS 文件
src('src/*.{js,css}') // 所有 JS 和 CSS 文件通配符模式
// * 匹配任意字符(除了 /)
src('src/*.js') // src/app.js ✓, src/utils.js ✓
// src/sub/main.js ✗
// ** 匹配任意层级目录
src('src/**/*.js') // src/app.js ✓
// src/sub/main.js ✓
// src/sub/deep/nested.js ✓
// ! 排除文件
src(['src/**/*.js', '!src/test/**/*.js']) // 排除 test 目录
// {} 匹配多个选项
src('src/*.{js,css,html}')Src 选项
src('src/**/*.js', {
allowEmpty: true, // 允许源文件为空
base: 'src', // 设置基础目录
since: lastRunTime, // 只处理修改过的文件
dot: false, // 是否包含隐藏文件
followSymlinks: true, // 是否跟随符号链接
sourcemaps: false // 是否生成 sourcemap
})实际示例
// 只处理修改过的文件(增量构建)
const { src, lastRun } = require('gulp');
function scripts() {
return src('src/js/**/*.js', {
since: lastRun(scripts) // 只返回自上次运行后修改的文件
})
.pipe(uglify())
.pipe(dest('dist/js'));
}Dest(目标文件)
基本用法
const { src, dest } = require('gulp');
// 写入到指定目录
function scripts() {
return src('src/js/*.js')
.pipe(uglify())
.pipe(dest('dist/js')); // 写入 dist/js 目录
}保持目录结构
// 源文件: src/js/app.js, src/js/utils.js
src('src/js/**/*.js')
.pipe(dest('dist'))
// 结果: dist/js/app.js, dist/js/utils.js
// 使用 base 选项
src('src/js/**/*.js', { base: 'src' })
.pipe(dest('dist'))
// 结果: dist/js/app.js, dist/js/utils.jsDest 选项
dest('dist', {
cwd: process.cwd(), // 当前工作目录
mode: 0o777, // 文件权限
dirMode: 0o777, // 目录权限
overwrite: true, // 是否覆盖已存在文件
sourcemaps: './maps' // sourcemap 输出目录
})Watch(监听)
基本用法
const { watch } = require('gulp');
// 监听文件变化
watch('src/js/**/*.js', scripts);
// 监听多个文件
watch(['src/js/**/*.js', 'src/css/**/*.css'], build);Watch 选项
watch('src/js/**/*.js', {
delay: 200, // 防抖延迟(毫秒)
ignoreInitial: false, // 是否忽略初始文件
queue: true, // 是否排队执行
events: 'all', // 监听事件类型
followSymlinks: true // 是否跟随符号链接
}, scripts);监听事件
// 监听不同事件
const watcher = watch('src/js/**/*.js');
watcher.on('change', (path, stats) => {
console.log(`File ${path} was changed`);
});
watcher.on('add', (path) => {
console.log(`File ${path} was added`);
});
watcher.on('unlink', (path) => {
console.log(`File ${path} was removed`);
});
watcher.on('error', (error) => {
console.error('Watcher error:', error);
});实际示例
// 开发模式监听
function dev() {
// 监听 JS 文件
watch('src/js/**/*.js', scripts);
// 监听 CSS 文件
watch('src/css/**/*.css', styles);
// 监听 HTML 文件
watch('src/**/*.html', html);
// 监听图片文件
watch('src/images/**/*', images);
}
exports.dev = dev;Series(串行)
基本用法
const { series } = require('gulp');
// 串行执行任务(按顺序)
exports.build = series(clean, scripts, styles, html);
// 等价于:
// 1. 执行 clean
// 2. clean 完成后执行 scripts
// 3. scripts 完成后执行 styles
// 4. styles 完成后执行 html错误处理
// 如果某个任务出错,后续任务不会执行
function clean(cb) {
// 模拟错误
cb(new Error('Clean failed'));
}
function scripts() {
// 不会执行
return src('src/js/*.js')
.pipe(dest('dist/js'));
}
exports.build = series(clean, scripts); // scripts 不会执行实际示例
// 清理 → 构建 → 压缩
gulp.task('build', series(
clean, // 1. 清理 dist 目录
parallel( // 2. 并行构建
scripts,
styles,
html
),
zip // 3. 打包压缩
));Parallel(并行)
基本用法
const { parallel } = require('gulp');
// 并行执行任务(同时)
exports.build = parallel(scripts, styles, html);
// 等价于:
// scripts、styles、html 同时执行Series + Parallel 组合
// 清理 → (JS/CSS/HTML 并行) → 压缩
exports.build = series(
clean,
parallel(scripts, styles, html),
zip
);实际示例
// 构建任务
function build() {
return parallel(
series(cleanJS, scripts), // 清理 JS → 构建 JS
series(cleanCSS, styles), // 清理 CSS → 构建 CSS
series(cleanImages, images) // 清理图片 → 构建图片
);
}
exports.build = build;Pipe(管道)
基本用法
// 管道式处理
src('src/js/*.js')
.pipe(uglify()) // 压缩
.pipe(rename({ suffix: '.min' })) // 重命名
.pipe(dest('dist/js')); // 输出管道工作原理
// Gulp 插件通常是转换流
function uglify() {
return through2.obj(function(file, encoding, callback) {
// 1. 接收文件
const content = file.contents.toString();
// 2. 处理文件
const minified = UglifyJS.minify(content).code;
// 3. 返回文件
file.contents = Buffer.from(minified);
// 4. 传递给下一个插件
callback(null, file);
});
}管道错误处理
// 错误处理
src('src/js/*.js')
.pipe(uglify().on('error', function(err) {
console.error('Uglify error:', err);
this.emit('end'); // 继续执行
}))
.pipe(dest('dist/js'));
// 使用 plumber 插件
const plumber = require('gulp-plumber');
src('src/js/*.js')
.pipe(plumber()) // 防止管道因错误中断
.pipe(uglify())
.pipe(dest('dist/js'));条件管道
const gulpif = require('gulp-if');
const { argv } = require('yargs');
// 只在生产环境压缩
function scripts() {
return src('src/js/*.js')
.pipe(gulpif(argv.production, uglify())) // 条件判断
.pipe(dest('dist/js'));
}核心概念总结
数据流生命周期
gulp.src() // 1. 创建可读流
↓
.pipe() // 2. 应用转换流
↓
.pipe() // 3. 应用转换流
↓
gulp.dest() // 4. 创建可写流
任务执行模式
// 串行执行
series(task1, task2, task3)
// task1 → task2 → task3
// 并行执行
parallel(task1, task2, task3)
// task1、task2、task3 同时执行
// 混合执行
series(clean, parallel(scripts, styles), zip)
// clean → (scripts + styles) → zip文件处理流程
// 完整的工作流
function build() {
return src('src/**/*', { since: lastRun(build) }) // 1. 读取源文件
.pipe(sourcemaps.init()) // 2. 初始化 sourcemap
.pipe(uglify()) // 3. 压缩
.pipe(rename({ suffix: '.min' })) // 4. 重命名
.pipe(sourcemaps.write('.')) // 5. 写入 sourcemap
.pipe(dest('dist')); // 6. 输出到目标
}