自动化构建之--Gulp实现

340 阅读9分钟

在项目根目录安装gulp模块

yarn add gulp --dev
npm install --save-dev gulp

根目录下创建gulpfile.js文件, 作为gulp配置的入口文件

导入gulp模块的部分api:

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

1.style任务:

  • 样式编译

  • 定义为私有任务,通过module export导出具体任务

  • src创建读取流, dest创建写入流, dist用于分发发布

  • 这里想通过编译后,与src中的目录结构相同,那么需要指定目标路径

  • 如果每个任务都手动指定目标路径显然重复臃肿,可以通过在读取流中添加配置项,指定写入流路径

  • saas({ outputStyle: "expanded" })通配展开样式

  • 通过插件转换文件yarn gulp-sass --dev安装转换插件并引入

const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(sass({ outputStyle: "expanded" }))
    .pipe(dest("dist"))
}

module.exports = {
  style
}

通过yarn gulp style命令运行编译任务。

2.脚本任务:

  • 同样定义为私有任务, 通过module export导出任务
  • 安装插件yarn add @babel/core @babel/preset-env --dev, 转换js
  • 此时运行报错
  • 原因: 这里的Babel只是帮你唤起转换进程, 并没有像sass模块那样真正的转换, 所以需要手动安装@babel/core模块来实现编译
  • 解决办法: yarn add @babel/core @babel-preset-env --dev
  • preset会默认把所有的ECMAScript代码全部做转换, 并在转换流中添加配置项
  • 这里如果不指定配置项{ presets: ["@babel/preset-env"] }, 那么编译后的文件与源文件没差, 是因为BabelECMAScript的平台, 具体转换是Babel内部的插件, preset-env就是最新的打包集合.
const script = () => {
  return src("src/assets/scripts/*.js", { base: "src" })
    .pipe(babel({ presets: ["@babel/preset-env"] }))
    // .pipe(babel())
    .pipe(dest("dist"))
}

通过yarn gulp script命令运行编译任务。

3.页面模板编译-html文件

  • 安装插yarn add gulp-swig --de
  • 通过指swi配置项配``htm`中的动态数据
  • 由于html文件不止在src下, 也有在其子目录下, 所以这里使用通配符 **/*.html
const page = () => {
  return src("src/**/*.html", { base: "src" })
    // .pipe(swig({ data }))
    .pipe(swig())
    .pipe(dest("dist"))
}

4.组合任务

通过parallel方法, 创建组合任务

// 导入parallel模块创建并行任务

const compile = parallel模块(style, script, page)

// 导出
module.exports = {
  compile
}

通过yarn gulp compile命令运行编译任务。执行结果:

5.图片转换

  • 安装插件yarn add gulp-imagemin --dev
  • 图片是无损压缩, 只是删除了源数据一些, svg格式只是代码格式化处理
/**
 * 图片任务
 */
const image = () => {
  return src("src/assets/images/**", { base: "src" })
    .pipe(imagemin())
    .pipe(dest("dist"))
}

通过yarn gulp image命令运行编译任务。执行结果:

6.文字任务

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

添加到组合任务中并导出:

// 组合任务
const compile = parallel(style, script, page, image, font)

// 导出模块
module.exports = {
  compile
}

7.其他文件

/**
 * 其他任务
 */
const extra = () => {
  return src("public/**", { base: "public" })
    .pipe(dest("dist"))
}

compile任务构建src项目中的需要编译的文件,为了不混淆,这里重新创建一个任务

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

// 导出模块
module.exports = {
  build
}

8.文件清除

  • 安装插件: yarn add del --dev
  • del模块返回promise对象, 可以支持clean方法标记完成
  • del模块不属于gulp
  • 清除任务执行在编译之前, 先清除后编译, 不能使用并行, 导入serice串行执行。
const { src, dest, parallel, series } = require("gulp");
const del = require("del");

// 定义清除任务
const clean = () => {
  return del(["dist"])
}

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

通过yarn gulp build命令运行编译任务。执行结果:

8.自动加载插件

  • 随着编译任务越来越多, 手动加载插件就变得繁重, 这里采用一个插件加载.
  • 安装插件: yarn add gulp-load-plugins --dev
  • plugins是一个对象, 所有插件会成为这个对象上的属性
  • 将所有插件重命名为plugins.{name}的形式
const loadPlugins = require("gulp-load-plugins");
const plugins = loadPlugins();

// const plugins.sass = require("gulp-sass");
// const plugins.babel = require("gulp-babel");
// const plugins.swig = require("gulp-swig");
// const plugins.imagemin = require("gulp-imagemin");

9.热更新开发服务器

  • 开发调试应用, 实现代码更改后自动编译, 自动刷新浏览器页面
  • 安装模块: yarn add browser-sync --dev
  • browser-sync模块支持代码更改后热更新到浏览器
  • 并不是gulp插件, 提供create方法自动创建开发服务器
  • baseDir方法指定运行到浏览器的路径, 这里指定编译后的dist
// 导入插件
const loadPlugins = require("gulp-load-plugins");
// 创建开发服务器
const bs = browserSync.create();
// 初始化并启动
const serve = () => {
  bs.init({
    server: {
      baseDir: "dist"
    }
  })
}

此时启动后, 会没有加载node_modules下的一些资源拷贝, 所以页面并没有加载全部, 通过给bs模块配置路由指向本地目录的node_modules, 如果routes有配置会先执行, 然后再执行baseDir中的路径.

// 单独定义开发服务器启动
const serve = () => {
  bs.init({
    notify: false, // 启动时不弹出提示
    port: 2080, // 端口
    // open: false,
    server: {
      baseDir: "dist",
      routes: {
        "/node_modules": "node_modules"
      }
    }
  })
}

此时加入热更新, 执行顺序应该是先clean清除后, 执行build编译源代码到dist目录, 编译完成后再执行serve更新到浏览器页面, 这里暂不考虑源代码变更后执行, 先加入dist目录下文件变化后更新. 加入files指定路径, 这里就是dist下的所有文件.

const serve = () => {
  bs.init({
    notify: false, // 启动时不弹出提示
    port: 2080, // 端口
    // open: false,
    files: "dist/**",
    server: {
      baseDir: "dist",
      routes: {
        "/node_modules": "node_modules"
      }
    }
  })
}

通过yarn gulp serve启动服务, 修改dist下任意文件可观察到已生效.

10.监视变化以及构建优化

  • 借助gulp中的watch方法监听. 此方法第一个参数接收通配符, 第二个参数即执行函数, 在serve启动的时候监听文件变化.
  • 此时在启动serve的时候, 就会先执行watch的监听, 一旦发现有变化, 执行对应的编译任务, 编译完后启动服务器, 那么此时久完成了之前src变化后, 同步到浏览器页面. 如下:
// 导入watch方法
const { src, dest, parallel, series, watch } = require("gulp");

const serve = () => {
  watch("src/assets/styles/*.scss", style)
  watch("src/assets/styles/*.js", script)
  watch("src/**/*.html", page)
  watch("src/assets/images/**", image)
  watch("src/assets/fonts/**", font)
  watch("public/**", extra)

  bs.init({
    notify: false, // 启动时不弹出提示
    port: 2080, // 端口
    // open: false,
    files: "dist/**",
    server: {
      baseDir: "dist",
      routes: {
        "/node_modules": "node_modules"
      }
    }
  })
}

那么这里还存在两个问题:

  • 上面的图片, 字体以及public目录下的文件只是做了拷贝, 并不会影响源文件, 所以这些文件只需在上线之前进行编译压缩即可, 所以开发阶段忽略这些文件的编译.如下: serve中的watch方法不再监听图片文字等, baseDir改为数组, 会按照顺序依次向后查询.
  • 如果是首次编译, 直接执行serve, 那么此时dist文件还没有生成, 那么就找不到对应路径, 报错, 所以这里将serve任务和build组合, 编译生成dist目录后再启动web服务器, 串行执行.
  • 也希望上面三类文件变化后自动更新浏览器, 可使用watch[]的形式
// 单独定义开发服务器启动
const serve = () => {
  watch("src/assets/styles/*.scss", style)
  watch("src/assets/styles/*.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: ["dist", "src", "public"],
      routes: {
        "/node_modules": "node_modules"
      }
    }
  })
}

组合任务, 开发模式下, 编译后在执行serve:

const compile = parallel(style, script, page)

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

const develop = series(compile, serve)

// 导出模块
module.exports = {
  clean,
  compile,
  build,
  serve,
  develop
}

通过yarn gulp develop命令运行编译任务。执行结果:

11.useref文件引入处理

  • node_modules/下的文件引入, 之前的处理是指向本地的开发文件, 上线后dist并没有这些文件
  • 安装插件: yarn add gulp-useref --dev
  • 原理: 这个插件会根据dist目录下的标识将这些引入的文件编译到一个新的文件.如图
const useref = () => {
  return src("dist/*.html", { base: "dist" })
    .pipe(plugins.useref({ searchPath: ["dist", "."] }))
    .pipe(dest("dist"))
}

通过yarn gulp useref命令运行编译任务。执行后的目录结构:

12.文件压缩

  • 安装插件: yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev
  • 同时需要判断是哪类文件, 执行哪类的压缩安装if插件: yarn add gulp-if --dev 此时存在一个问题: dist目录同时进行读写操作, 会造成文件冲突, 有的文件不能写入, 所以这时需要指定临时目录temp, 在编译完后, 进行useref对引入的文件进行处理, 最后写入到dist目录. 那么此时的gulpfile.js如下:
// 实现这个项目的构建任务

/**
 * 导入gulp模块的部分api
 */
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 clean = () => {
  return del(["dist", "temp"])
}

/**
 * 样式编译
 * 定义为私有任务,通过module export 导出具体任务
 * src 创建读取流 dest创建写入流 dist用于分发发布
 * 这里想通过编译后,与src中的目录结构相同,那么需要指定目标路径
 * 如果每个任务都手动指定目标路径显然重复臃肿,可以通过在读取流中添加配置项,指定写入流路径
 * 通过插件转换文件yarn gulp-sass --dev 安装转换插件并引入
 */
const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(plugins.sass({ outputStyle: "expanded" }))
    .pipe(dest("temp"))
    .pipe(bs.reload({ stream: true })) // 以流的形式推到浏览器, serve就不需要files参数了
}


/**
 * 脚本任务
 * base拷贝的基本目录
 * 安装插件yarn add @babel/core @babel/preset-env --dev, 转换js
 */
const script = () => {
  return src("src/assets/scripts/*.js", { base: "src" })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    // .pipe(babel())
    .pipe(dest("temp"))
    .pipe(bs.reload({ stream: true }))
}

/**
 * 页面编译任务
 * 通过指定swig配置项配置html中的动态数据
 */
const page = () => {
  return src("src/**/*.html", { base: "src" })
    // .pipe(swig({ data }))
    .pipe(plugins.swig())
    .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/styles/*.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", "."] }))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({ 
      collapseWhitespace: true,
      minifiCss: true,
      minifiJs: true 
    })))
    .pipe(dest("dist"))
}



// 组合任务
const compile = parallel(style, script, page)
/**
 * compile任务构建src项目中的需要编译的文件,为了不混淆,这里重新创建一个任务
 * 清除任务执行在编译之前,先清除后编译,不能使用并行。
 */
const build = series(
  clean, 
  parallel(
    series(compile, useref), 
    extra, 
    image, 
    font
  )
);

const develop = series(compile, serve)

// 导出模块
module.exports = {
  clean,
  compile,
  build,
  serve,
  develop,
  useref
}

13.补充

package.json文件中, 加入上面的任务命令, 更方便的使用

"scripts": {
    "clean": "gulp clean",
    "lint": "gulp lint",
    "serve": "gulp serve",
    "build": "gulp build",
    "start": "gulp start",
    "deploy": "gulp deploy --production"
  },