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
 
# 执行默认任务
gulp

Src(源文件)

基本用法

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.js

Dest 选项

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. 输出到目标
}

📚 相关链接


标签: gulp 核心概念 stream task pipeline