Gulp

187 阅读13分钟

Gulp

Gulp 可以说是当下最流行的前端构建系统,特点是:高效、易用。其使用过程非常简单,大致过程如下:

graph TD
项目中安装gulp开发依赖 --> 根目录添加gulpfile.js文件 --> 编写需要gulp自动执行构建任务 --> 终端中运行构建任务

基本使用

1. 初始化工作:

mkdir 08-gulp-sample
cd 08-gulp-sample
yarn init
yarn add gulp --dev
code gulpfile.js

2. 编写 gulpfile.js 文件,添加构建任务:

// gulp 的入口文件

exports.first = () => {
    console.log("first task working...")
}

3. 运行:

yarn gulp first

此时会提示:任务执行未完成,是否忘记标记该任务的结束。因为最新的 gulp 当中取消了同步代码模式,约定了每一个任务均是异步任务。当任务执行完成过后需要调用回调函数或其他方式去标记这个任务已经完成。

image.png

4. 改进:

// gulp 的入口文件

exports.first = done => {
    console.log("first task working...")

    done() // 标识任务完成
}

运行:

image.png

5. default 任务:

以 default 命名的任务会作为 gulp 的默认任务,运行该任务时,无须指定任务名称。

// gulp 的入口文件

exports.first = done => {
    console.log('first task working...')

    done() // 标识任务完成
}

exports.default = done => {
    console.log('default task working...')
    done()
}

运行:

yarn gulp

image.png

6. gulp v4.0 以前的任务注册方式:

gulp v4.0 以前的任务注册是通过 gulp 模块里面的 task() 方法去实现。首先要载入 gulp 模块,然后使用 gulp.task() 方法来进行任务注册。

const gulp = require('gulp')

gulp.task('second', done => {
    console.log('second task working...')
    done()
})

运行:

yarn gulp second

image.png

可以发现,已经可以使用,因为在目前版本依旧保留此 api,但是此方式是不被推荐的。

创建组合任务

1. 创建串行任务:

const { series } = require('gulp')


const task1 = done => {
    setTimeout(() => {
        console.log('task1 working...')
    }, 1000);
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 working...')
    }, 1000);
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 working...')
    }, 1000);
}

exports.seriesTasks = series(task1, task2, task3)

运行:

yarn gulp seriesTasks

image.png

2. 创建并行任务:

const { series, parallel } = require('gulp')


const task1 = done => {
    setTimeout(() => {
        console.log('task1 working...')
        done()
    }, 1000);
}

const task2 = done => {
    setTimeout(() => {
        console.log('task2 working...')
        done()
    }, 1000);
}

const task3 = done => {
    setTimeout(() => {
        console.log('task3 working...')
        done()
    }, 1000);
}

exports.seriesTasks = series(task1, task2, task3)

exports.parallel = parallel(task1, task2, task3)

运行:

yarn gulp parallel

image.png

异步任务的三种方式

1. 回调函数的方式:

成功回调:

exports.callback = done => {
    console.log('callback task... ')
    done()
}

运行:

image.png

这里的回调函数与 node 当中的回调函数相似,都是错误优先回调函数。当任务运行报错是,会阻塞后续任务执行。

失败回调:

exports.callback_error = done => {
    console.log('callback_error task...')
    done(new Error('task failed!'))
}

运行:

yarn gulp callback_error

image.png

2. Promise 方式:

promise 相比于回调函数, 它能避免回调嵌套过深的问题。

promise 成功:

exports.promise = () => {
    console.log('promise task...')
    return Promise.resolve()
}

运行:

image.png

promise 失败:

exports.promise_error = () => {
    console.log('promise_error task...')
    return Promise.reject(new Error('task failed!'))
}

运行:

image.png

3. async & await 方式

exports.async = async() => {
    await timeout(1000)

    console.log('async task...')
}

运行:

image.png

4. stream 方式

这种方式下,任务函数需要返回一个 stream 对象。

const fs = require('fs')

exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')

    readStream.pipe(writeStream)

    return readStream
}

运行:

image.png

gulp 在接收到 readStream 以后,其实是为其注册了一个 end 事件,在 end 事件当中结束了任务的执行。

以上方式其实是:

const fs = require('fs')

exports.stream_end = done => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')

    readStream.pipe(writeStream)

    readStream.on('end', () => {
        done()
    })
}

运行:

image.png

构建过程核心工作原理

gulp 是一个基于流的构建系统:The streaming build system。

1. 文件压缩的构建过程:

输入(如:读取文件) --> 加工(如:压缩文件) --> 输出(如:文件写入)

2. 编码:

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
    // 文件读取流
    const read = fs.createReadStream('sample.css')

    // 文件写入流
    const write = fs.createWriteStream('sample.min.css')

    // 文件转换流
    const transform = new Transform({
        transform: (chunk, encoding, callback) => {
            // 核心转换过程实现
            // chunk => 读取流中读取到的内容(Buffer)
            const input = chunk.toString()
            const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
            callback(null, output)
        }
    })

    // 把读取出来的文件流, 先导入转换流进行转换后,再导入写入文件流
    read
        .pipe(transform) // 转换流
        .pipe(write) // 写入流

    return read
}

3. 运行:

image.png

image.png

4. 总结:

输入(读取流) --> 加工(转换流) --> 输出(写入流)

文件操作API + 插件的使用

gulp 中为我们提供了用于创建读取流和写入流的 API,相比于底层 node 的 API,gulp 的 API 更强大,且更易用。至于负责文件加工的转换流 API,一般通过独立的插件来提供。

由此,可以得到通过 gulp 来创建构建任务的流程就是:

  1. gulp 提供的 src 方法创建读取流;
  2. 借助插件实现的转换流来进行加工;
  3. gulp 提供的 dest 方法创建写入流;
const { src, dest } = require('gulp')
const cleanCss = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
    return src('src/*.css') // 创建读取流
        .pipe(cleanCss()) // 实现 css 压缩
        .pipe(rename({ extname: '.min.css' })) // 实现 css 文件重命名
        .pipe(dest('dist')) // 导入到写入流
}

运行:

image.png

image.png

自动化构建案例

项目目录如下:

image.png

1. 样式编译

const { src, dest } = require('gulp')
const sass = require('gulp-sass')(require('sass'))

const style = () => {
    // base:基准路径,增加此配置选项后,可以把 src 下面的目录结构保存下来。
    // sass 配置 outputStyle: 'expanded' 转换后的 css 代码是完全展开方式
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}

使用插件:gulp-sass

【注】:sass 模块工作的时候,以 _ 开头的文件会被认为是主文件当中依赖的文件并在转换过程中被忽略。

2. 脚本编译

const babel = require('gulp-babel')

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}

使用插件:gulp-babel

【注】:gulp-babel 需要手动安装 babel/core。

3. 页面模板编译

const swig = require('gulp-swig')

const data = {
    menus: [{
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [{
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(swig({ data: data }))
        .pipe(dest('dist'))
}

使用插件:gulp-swig

【注】:data 配置选项用于指定模板动态数据。

4. 图片和字体文件转换

const imagemin = require('gulp-imagemin')

const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('dist'))
}

使用插件:gulp-imagemin

【注】:gulp-imagemin 使用 7.0.0 版本,8.0.0 版本需要使用 import 的方式导入; gulp-imagemin 插件能自动识别图片文件进行压缩,非图片文件会被忽略。

5. 其他文件,文件清除,简单整理成组合任务


const { src, dest, parallel, series, } = require('gulp')
const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}

const del = require('del')

const clean = () => {
    return del(['dist'])
}

const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))

module.exports = {
    compile,
    build,
}

使用插件:del

【注】:del 不是 gulp 的插件,但是可以在 gulp 当中使用。因为 gulp 的任务不单单是文件流的形式,它还支持 promise 方式的任务,del 的返回就是一个 promise。

6. 自动加载插件

const { src, dest, parallel, series, } = require('gulp')

const del = require('del')

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const sass = plugins.sass(require('sass'))

const style = () => {
    // base:基准路径,增加此配置选项后,可以把 src 下面的目录结构保存下来。
    // sass 配置 outputStyle: 'expanded' 转换后的 css 代码是完全展开方式
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
}


const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
}


const data = {
    menus: [{
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [{
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(plugins.swig({ data: data }))
        .pipe(dest('dist'))
}



const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}



const clean = () => {
    return del(['dist'])
}



const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))

module.exports = {
    compile,
    build,
}

使用插件:gulp-load-plugins

【注】:这个插件提供一个方法,该方法返回一个对象,当前项目中加载过的插件都是这个对象的属性。del 不是 gulp 插件;gulp-sass 插件需要引入 sass 依赖,而 sass 也不是 gulp 插件,因此需要单独引入。

7. 开发服务器

const browserSync = require('browser-sync')
const bs = browserSync.create() // 创建开发服务器

const serve = () => {
    bs.init({
        notify: false,
        port: 2080,
        open: false,
        files: 'dist/**',
        server: {
            baseDir: 'dist',
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}

使用插件:gulp-browser-sync

【注】:

  • notify:去掉启动浏览器的提示;
  • port:设置端口号;
  • open:是否自动打开网页;
  • files:监听热更新目录,指定目录文件改变后自动更新;

8. 监听变化以及构建过程优化

const { src, dest, parallel, series, watch, } = require('gulp')

const del = require('del')

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const sass = plugins.sass(require('sass'))

const style = () => {
    // base:基准路径,增加此配置选项后,可以把 src 下面的目录结构保存下来。
    // sass 配置 outputStyle: 'expanded' 转换后的 css 代码是完全展开方式
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))
}


const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))

}


const data = {
    menus: [{
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [{
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(plugins.swig({ data: data }))
        .pipe(dest('dist'))
        .pipe(bs.reload({ stream: true }))
}



const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}



const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}



const clean = () => {
    return del(['dist'])
}




const browserSync = require('browser-sync')
const bs = browserSync.create() // 创建开发服务器

const serve = () => {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)

    // watch('src/assets/images/**', image)
    // watch('src/assets/fonts/**', font)
    // watch('public/**', extra)

    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        open: true,
        // files: 'dist/**',
        server: {
            baseDir: ['dist', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}


const compile = parallel(style, script, page)

const build = series(clean, parallel(compile, extra))

const dev = series(compile, serve)

module.exports = {
    clean,
    compile,
    build,
    dev,
}

9. useref 文件引用处理

const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(plugins.useref({ searchPath: ['dist', '.'] })).pipe(dest('dist'))
}

使用插件:gulp-useref

【注】:gulp-useref 可以将 HTML 引用的多个 CSS 和J S 合并起来,减小依赖的文件个数,从而减少浏览器发起的请求次数。它根据注释将 HTML 中需要合并压缩的区块找出来,对区块内的所有文件进行合并,只负责合并,不负责压缩。

10. 文件压缩

const useref = () => {
    return src('dist/*.html', { base: 'dist' })
        .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            minifyCss: true,
            minifyJs: true,
        })))
        .pipe(dest('release'))
}

使用插件:gulp-if gulp-uglify gulp-htmlmin gulp-clean-css

  1. gulp-if:条件判断插件;
  2. gulp-uglify:js 文件压缩插件;
  3. gulp-clean-css:css 文件压缩插件;
  4. gulp-htmlmin:html 文件压缩插件;

11. 重新规划构建过程

const { src, dest, parallel, series, watch, } = require('gulp')

const del = require('del')

const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()

const sass = plugins.sass(require('sass'))

const style = () => {
    // base:基准路径,增加此配置选项后,可以把 src 下面的目录结构保存下来。
    // sass 配置 outputStyle: 'expanded' 转换后的 css 代码是完全展开方式
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}


const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))

}


const data = {
    menus: [{
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [{
                    name: 'Twitter',
                    link: 'https://twitter.com/w_zce'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/zceme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/zce'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}

const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(plugins.swig({ data: data, defaults: { cache: false } }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}



const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('release'))
}

const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('release'))
}



const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('release'))
}



const clean = () => {
    return del(['release', 'temp'])
}



const browserSync = require('browser-sync')
const bs = browserSync.create() // 创建开发服务器

const serve = () => {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)

    // watch('src/assets/images/**', image)
    // watch('src/assets/fonts/**', font)
    // watch('public/**', extra)

    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**'
    ], bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        open: true,
        // files: 'release/**',
        server: {
            baseDir: ['temp', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}


const useref = () => {
    return src('temp/*.html', { base: 'temp' })
        .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            minifyCss: true,
            minifyJs: true,
        })))
        .pipe(dest('release'))
}


const compile = parallel(style, script, page)

const build = series(
    clean,
    parallel(
        series(compile, useref),
        image,
        font,
        extra
    )
)

const dev = series(compile, serve)

module.exports = {
    clean,
    compile,
    build,
    dev,
    useref,
}

【注】:由于 style、script、page 三个任务得到的文件还需要经过 useref 进行引用合并,以及文件压缩等操作,过程中同时对文件进行读写操作可以会发生错误,导致操作失败。引入 temp 目录,用于存放 style、script、page 三个任务得到的文件流、然后再从 useref 由 temp 目录中读取文件流,转换后写入 release 目录中。

12. 补充

module.exports = {
    clean,
    build,
    dev,
}

【注】:只暴露一些必须的任务。

"scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "dev": "gulp dev"
}

【注】:补充 scripts 方便使用。

封装工作流

1. 准备

  • 创建 git 仓库
  • 创建 npm 模块项目
  • 更新到 git 仓库

2. 提取 gulpfile

样例项目(13-gulp-demo),npm 模块项目(syc-pages)

a. 将 13-gulp-demo 中的 gulpfile.js 提取到 syc-pages 中的入口文件:

image.png

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()

const sass = require('gulp-sass')(require('sass'))

const data = {
    menus: [{
            name: 'Home',
            icon: 'aperture',
            link: 'index.html'
        },
        {
            name: 'Features',
            link: 'features.html'
        },
        {
            name: 'About',
            link: 'about.html'
        },
        {
            name: 'Contact',
            link: '#',
            children: [{
                    name: 'Twitter',
                    link: 'https://twitter.com/w_syc'
                },
                {
                    name: 'About',
                    link: 'https://weibo.com/sycme'
                },
                {
                    name: 'divider'
                },
                {
                    name: 'About',
                    link: 'https://github.com/syc'
                }
            ]
        }
    ],
    pkg: require('./package.json'),
    date: new Date()
}


const clean = () => {
    return del(['dist', 'temp', 'release'])
}


const style = () => {
    return src('src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

const page = () => {
    return src('src/*.html', { base: 'src' })
        .pipe(plugins.swig({ data, defaults: { cache: false } }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

const image = () => {
    return src('src/assets/images/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const font = () => {
    return src('src/assets/fonts/**', { base: 'src' })
        .pipe(plugins.imagemin())
        .pipe(dest('dist'))
}

const extra = () => {
    return src('public/**', { base: 'public' })
        .pipe(dest('dist'))
}

const serve = () => {
    watch('src/assets/styles/*.scss', style)
    watch('src/assets/scripts/*.js', script)
    watch('src/*.html', page)

    // watch('src/assets/images/**', image)
    // watch('src/assets/fonts/**', font)
    // watch('public/**', extra)

    watch([
        'src/assets/images/**',
        'src/assets/fonts/**',
        'public/**',
    ], bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        // open: false,
        // files: 'dist/**',
        server: {
            baseDir: ['temp', 'src', 'public'],
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}

const useref = () => {
    return src('temp/*.html', { base: 'temp' })
        .pipe(plugins.useref({ searchPath: ['temp', '.'] }))
        // html js css
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            minifyJS: true,
            minifyCSS: true,
        })))
        .pipe(dest('release'))
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build = series(
    clean,
    parallel(
        series(compile, useref),
        image,
        font,
        extra
    )
)

const dev = series(compile, serve)

module.exports = {
    clean,
    build,
    dev,
}

b. 提取 13-gulp-demo 中的 devdependencies 提取到 syc-pages 中的 dependencies 中,删除 13-gulp-demo 中的 devdependencies:

image.png

  "dependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "browser-sync": "^2.27.10",
    "del": "^6.1.1",
    "gulp": "^4.0.2",
    "gulp-babel": "^8.0.0",
    "gulp-clean-css": "^4.3.0",
    "gulp-htmlmin": "^5.0.1",
    "gulp-if": "^3.0.0",
    "gulp-imagemin": "7.0.0",
    "gulp-load-plugins": "^2.0.7",
    "gulp-sass": "^5.1.0",
    "gulp-swig": "^0.9.1",
    "gulp-uglify": "^3.0.2",
    "gulp-useref": "^5.0.0",
    "sass": "^1.53.0"
  }

c. 使用开发阶段的 syc-pages 模块:

cd ./syc-pages
yarn link

image.png

cd ./13-gulp-demo
yarn link syc-pages

image.png

c. 移除 13-gulp-demo 中的 node_modules 文件夹,重新安装 dependencies:

cd ./13-gulp-demo
yarn

image.png

d. 13-gulp-demo 中的 gulpfile.js 文件编辑:

module.exports = require('syc-pages')

e. 通过 scrips 使用已经定义的任务:

{
  "name": "13-gulp-demo",
  "version": "1.0.0",
  "main": "index.js",
  "author": "syc",
  "license": "MIT",
  "scripts": {
    "clean": "gulp clean",
    "build": "gulp build",
    "dev": "gulp dev"
  },
  "devDependencies": {},
  "dependencies": {
    "bootstrap": "4.3.1",
    "jquery": "^3.6.0",
    "popper.js": "^1.16.1"
  }
}

运行 build 任务:

yarn build

image.png

由于此前的开发依赖已经被删除,导致 scripts 执行时,无法从 node_modules 目录中找到 gulp 的可执行文件,可以先安装一下 gulp;

yarn add gulp-cli --dev

image.png

再次运行 build 任务:

image.png

还是需要先在本地安装 gulp;

yarn add gulp --dev

由于此时 syc-pages 这个模块处于开发阶段,当模块真正完成,且发布过后,在项目中安装 syc-pages 模块的同时,这些模块会被自动安装,则不会出现此类为题;

再次运行 build 任务:

image.png

由于 syc-pages 模块项目中的 build 任务使用到的数据引用了本应是 13-gulp-demo 项目中的文件。

3. 解决模块中的问题

a. 解决动态 data 问题:

约定在 13-gulp-demo 项目中创建一个 pages.config.js 配置文件,提取公共模块 syc-pages 项目中使用的 data 至 pages.config.js 配置文件中进行生命。

image.png

module.exports = {
    data: {
        menus: [{
                name: 'Home',
                icon: 'aperture',
                link: 'index.html'
            },
            {
                name: 'Features',
                link: 'features.html'
            },
            {
                name: 'About',
                link: 'about.html'
            },
            {
                name: 'Contact',
                link: '#',
                children: [{
                        name: 'Twitter',
                        link: 'https://twitter.com/w_syc'
                    },
                    {
                        name: 'About',
                        link: 'https://weibo.com/sycme'
                    },
                    {
                        name: 'divider'
                    },
                    {
                        name: 'About',
                        link: 'https://github.com/syc'
                    }
                ]
            }
        ],
        pkg: require('./package.json'),
        date: new Date()
    }
}

syc-pages 入口文件中读取配置文件中的动态数据 data:

const cwd = process.cwd() // 返回当前命令行所在的公共目录
let config = {
    // default config
}

try {
    const loadConfig = require(`${cwd}/pages.config.js`)
    config = Object.assign({}, config, loadConfig)
} catch (e) {}

b. 解决 @babel/preset-env 找不到问题:

运行 build 任务:

image.png

此时会找不到 @babel/preset-env,因为已字符串的方式传参,此时会到当前项目的 node_modules 目录中去寻找 @babel/preset-env;需要改为 require 的方式,此时会在当前文件所在目录去查找,若找不到会依次网上层目录找;

const script = () => {
    return src('src/assets/scripts/*.js', { base: 'src' })
        .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
        .pipe(dest('temp'))
        .pipe(bs.reload({ stream: true }))
}

再次运行 build 任务:

image.png

此时 build 任务可以正常执行;

4. 抽象路径配置

将 syc-pages 入口文件中的路径进行抽象:

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')

const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()

const sass = require('gulp-sass')(require('sass'))


const cwd = process.cwd() // 返回当前命令行所在的公共目录
let config = {
    // default config
    build: {
        src: 'src',
        dist: 'dist',
        temp: 'temp',
        public: 'public',
        paths: {
            styles: 'assets/styles/*.scss',
            scripts: 'assets/scripts/*.js',
            pages: '*.html',
            images: 'assets/images/**',
            fonts: 'assets/fonts/**',
        }
    }
}

try {
    const loadConfig = require(`${cwd}/pages.config.js`)
    config = Object.assign({}, config, loadConfig)
} catch (e) {}


const clean = () => {
    return del([config.build.dist, config.build.temp])
}


const style = () => {
    return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
        .pipe(sass({ outputStyle: 'expanded' }))
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true }))
}

const script = () => {
    return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.babel({ presets: [require('@babel/preset-env')] }))
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true }))
}

const page = () => {
    return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.swig({ data: config.data, defaults: { cache: false } }))
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true }))
}

const image = () => {
    return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.imagemin())
        .pipe(dest(config.build.dist))
}

const font = () => {
    return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.imagemin())
        .pipe(dest(config.build.dist))
}

const extra = () => {
    return src('**', { base: config.build.public, cwd: config.build.public })
        .pipe(dest(config.build.dist))
}

const serve = () => {
    watch(config.build.paths.styles, { cwd: config.build.src }, style)
    watch(config.build.paths.scripts, { cwd: config.build.src }, script)
    watch(config.build.paths.pages, { cwd: config.build.src }, page)

    watch([
        config.build.paths.images,
        config.build.paths.fonts,
    ], { cwd: config.build.src }, bs.reload)

    watch([
        '**',
    ], { cwd: config.build.public }, bs.reload)

    bs.init({
        notify: false,
        port: 2080,
        // open: false,
        // files: 'dist/**',
        server: {
            baseDir: [config.build.temp, config.build.src, config.build.public],
            routes: {
                '/node_modules': 'node_modules'
            }
        }
    })
}

const useref = () => {
    return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp })
        .pipe(plugins.useref({ searchPath: [config.build.temp, '.'] }))
        // html js css
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            minifyJS: true,
            minifyCSS: true,
        })))
        .pipe(dest(config.build.dist))
}

const compile = parallel(style, script, page)

// 上线之前执行的任务
const build = series(
    clean,
    parallel(
        series(compile, useref),
        image,
        font,
        extra
    )
)

const dev = series(compile, serve)

module.exports = {
    clean,
    build,
    dev,
}

抽象路径后,返回 13-gulp-demo 中运行 build 任务:

image.png

可以正常运行,然后我们可以在 13-gulp-demo 的 pages.config.js 配置文件中添加 build 选项来覆盖 syc-pages 模块中抽象出来的路径。

module.exports = {
    build: {
        src: 'src',
        dist: 'release',
        temp: '.tmp',
        public: 'public',
        paths: {
            styles: 'assets/styles/*.scss',
            scripts: 'assets/scripts/*.js',
            pages: '*.html',
            images: 'assets/images/**',
            fonts: 'assets/fonts/**',
        }
    },
    data: {
        menus: [{
                name: 'Home',
                icon: 'aperture',
                link: 'index.html'
            },
            {
                name: 'Features',
                link: 'features.html'
            },
            {
                name: 'About',
                link: 'about.html'
            },
            {
                name: 'Contact',
                link: '#',
                children: [{
                        name: 'Twitter',
                        link: 'https://twitter.com/w_syc'
                    },
                    {
                        name: 'About',
                        link: 'https://weibo.com/sycme'
                    },
                    {
                        name: 'divider'
                    },
                    {
                        name: 'About',
                        link: 'https://github.com/syc'
                    }
                ]
            }
        ],
        pkg: require('./package.json'),
        date: new Date()
    }
}

image.png

5. 包装 Gulp CLI

a. 命令行指定 gulpfile 路径及工作目录:

移除 13-gulp-demo 中的 gulpfile.js,运行;

image.png

在命令行中指定 gulpfile.js 所在路径,即 13-gulp-demo 中安装的 syc-pages 模块的入口文件路径:

yarn build --gulpfile .\node_modules\syc-pages\lib\index.js

image.png

此时 build 任务可以正常运行,但是工作目录被切换到了 ./node_modules/syc-pages/lib/index.js 所在的目录了,而不是当前项目所在的根目录,因此我们还需要指定当前的工作目录;

yarn build --gulpfile .\node_modules\syc-pages\lib\index.js --cwd .

image.png

此时 build 任务的工作目录被切换到了当前目录,可见当前项目所需要被处理的文件均能被找到。

但是这样的运行方式传参过于复杂,可以通过在 syc-pages 中创建 cli 任务,然后自动传参,然后再内部去调用 gulp 可执行程序去运行 build 任务;

b. 创建 CLI 任务:

添加 CLI 入口文件:

image.png

packages.json 中添加 bin 字段:

{
    ...
    "bin": "bin/syc-pages.js",
    ...
}

编辑 CLI 入口文件,并测试使用:

#! /usr/bin/env node

console.log('syc-pages')

image.png

CLI 任务中载入 gulp 模块:

require('gulp/bin/gulp')

image.png

获取命令行参数(process.argv):

#! /usr/bin/env node

console.log(process.argv)

require('gulp/bin/gulp')

image.png

添加命令行参数(process.argv.push):

#! /usr/bin/env node

process.argv.push('--cwd')
process.argv.push(process.cwd())

process.argv.push('--gulpfile')
process.argv.push(require.resolve('../lib')) // require 载入这个模块,require.resolve 找到这个模块对应的路径(相对路径进行传参)

console.log(process.argv)

// require('gulp/bin/gulp')

image.png

使用 CLI 任务:

#! /usr/bin/env node

process.argv.push('--cwd')
process.argv.push(process.cwd())

process.argv.push('--gulpfile')
process.argv.push(require.resolve('../lib')) // require 载入这个模块,require.resolve 找到这个模块对应的路径(相对路径进行传参)

require('gulp/bin/gulp')
cd ./13-gulp-demo
syc-pages build

image.png

6. 发布并使用模块

发布模块:

cd ./syc-pages
git add .
git commit -m 'update'
git push
yarn publish

使用模块:

mkdir sp-test-demo
cd ./sp-test-demo
# 添加测试所需要的 public、src 目录及 pages.config.js 文件
yarn init --yes
yarn add syc-pages --dev
yarn syc-pages build

image.png

添加 scripts 并使用:

{
    ...
    "scripts": {
        "clean": "syc-pages clean",
        "build": "syc-pages build",
        "dev": "syc-pages dev"
    },
    ...
}

image.png