【万字长文】通过grunt、gulp和fit,彻底搞懂前端的自动化构建(二)

简介: 【万字长文】通过grunt、gulp和fit,彻底搞懂前端的自动化构建

Gulp 用法


gulp 作为目前最流行的构建工具,其特点是高效、易用、使用简单。


基本用法


首先在项目中安装 gulp。


npm i -D gulp

安装 gulp 时会自动安装一个 gulp-cli 模块。

然后在项目的根目录下编写 gulp 配置文件,配置文件约定的名字是 gulpfile.js。

gulpfile 遵循 nodejs 规范,所以需要使用 CommonJs 规范。


exports.foo = () => {
  console.log("foo task working~");
};

上面的代码就是创建了一个 foo 任务。

运行 foo 任务。


npx gulp foo

会得到以下日志:


[22:16:18] Using gulpfile ~/Documents/project-t/test-project/gulpfile.js
[22:16:18] Starting 'foo'...
foo task working~
[22:16:18] The following tasks did not complete: foo
[22:16:18] Did you forget to signal async completion?

最新版本的 gulp 中的任务都是异步任务,而非同步任务。所以在每个任务执行结束之后,需要手动标记该任务已结束。

标记的方法也很简单,每个任务函数都会传递一个默认参数,该参数就是一个函数,调用该函数就意味着任务完成。


exports.foo = (done) => {
  console.log("foo task working~");
  done();
};

这样 foo 任务的运行就不会再有问题了。


默认任务


如果 exports 的属性名是 default 的话,就可以作为默认任务。


exports.default = (done) => {
  console.log("default task working~");
  done();
};

这样在运行时只需要运行npx gulp,而不需要指定任务名。


gulp 4.x 之前的任务写法


在 gulp4.0 之前,注册任务的写法与现在不同。


const gulp = require("gulp");
gulp.task("bar", (done) => {
  console.log("bar task working~");
  done();
});


组合任务


组合任务又分为并行任务和串行任务,概念上和电路图是一回事。

串行任务是指多个任务依次执行。

并行任务是指多个任务同时执行。

它们分别由 series 和 parallel 来创建。

下面模拟三个任务来观察它们的区别。


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.foo = series(task1, task2, task3);
exports.bar = parallel(task1, task2, task3);

运行 foo 任务,会每当一个任务执行完成才会执行下一个任务,用时大约 3 秒多。

运行 bar 任务,三个任务会同时执行,大约 1 秒以后三个任务同时结束。

互不干扰的任务可以使用并行任务,可以提高任务整体的执行效率。比如编译 js 和编译 css。

相互依赖的任务只能使用串行任务,比如部署项目,要先编译再部署。


异步任务


gulp 的所有任务都是异步任务。

异步任务在函数内部是无法得知自身完成的,一般都是通过回调函数的方式通知外部这个函数执行完成。

gulp 最常用的异步任务方法有 3 种。

第一种是回调函数的形式。


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

这个 done 函数和 nodejs 的回调函数是相同的原则,都是错误优先。如果当一个任务发生异常,阻止其他任务继续执行,就可以在 done 函数的第一个参数的位置传递一个错误对象。


exports.callback = (done) => {
  console.log("callback task");
  done(new Error("task failed!"));
};

既然用到了回调函数,那么自然可以使用 Promise 来取代回调函数。这么做的好处是避免了回调函数嵌套过深的情况。


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

直接返回一个 Promise.resolve,就意味着这个任务执行成功了。

如果任务执行失败,可以返回一个失败的 Promise。


exports.promise_error = () => {
  console.log("promise task");
  return Promise.reject(new Error("task failed"));
};

使用了 Promise ,那么也可以使用 async/await 的语法来编写代码。这么做的好处是让我们的代码更容易理解。

如果 nodejs 的环境是 8+,就可以使用 async/await 语法。


const timeout = (time) => {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
};
exports.async = async () => {
  await timeout(1000);
  console.log("async task");
};

除了上述的回调函数和异步两种方式,gulp 中最流行的还是 stream 的方式,因为构建过程中都是在操作文件。


const fs = require("fs");
exports.stream = () => {
  // 读取流
  const readStream = fs.createReadStream("package.json");
  // 写入流
  const writeStream = fs.createWriteStream("temp.txt");
  // 将读取流 导入到 写入流
  readStream.pipe(writeStream);
  // 返回这个读取流
  return readStream;
};

返回一个 stream 对象时,结束时机就是 stream 的 end 事件触发的时候。

实现原理很简单,我们可以模拟一下。


const fs = require("fs");
exports.stream = (done) => {
  const readStream = fs.createReadStream("package.json");
  const writeStream = fs.createWriteStream("temp.txt");
  readStream.pipe(writeStream);
  readStream.on("end", () => {
    done();
  });
};

可以看到,gulp 只是帮我们注册了一个 end 事件,并在回调函数中调用了 done 函数。


构建过程核心工作原理


构建过程的工作流程基本上都是读取文件、文件转换、写入文件三个步骤。

这个过程可以人工操作,但是当文件过多或者需要频繁操作时,人工就很难继续胜任这个工作。

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

构建过程核心工作原理示意图

下面使用原生的 nodejs 来模拟一下这个过程。


const fs = require("fs");
const { Transform } = require("stream");
exports.default = () => {
  // 读取流
  const read = fs.createReadStream();
  // 写入流
  const write = fs.createReadStream();
  // 转换流
  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;
};

gulp 的自述是 The streaming build system,意思就是基于流的构建系统。

为什么要使用流来实现构建过程呢?因为 gulp 希望整个构建过程像管道一样,这样可以方便添加各种扩展插件。


文件操作 API


gulp 为我们操作文件提供了一套 api,相比于 nodejs 原生的 api,功能更强大,也更易于使用。

首先通过 gulp 提供的 src 方法创建读取流,再通过插件提供的转换流来完成文件内容的加工,最后通过 gulp 提供的 dest 方法创建写入流,将内容写入新的文件。

下面是一个将 src 目录下的 css 文件拷贝到 dist 目录下的例子。


const { src, dest } = require("gulp");
exports.default = () => {
  return src("src/*.css").pipe(dest("dist"));
};

这个例子仅仅是使用读取流读取文件内容再使用写入流将内容写入到指定位置。

接下来再添加文件的转换流,来完成内容压缩和文件的重命名。

安装 gulp-clean-css 和 gulp-rename 插件。


npm i -D gulp-clean-css gulp-rename

将这两个转换流加入到任务流程中。


const { src, dest } = require("gulp");
const cleanCss = require("gulp-clean-css");
const rename = require("gulp-rename");
const css = () => {
  return src("src/*.css")
    .pipe(cleanCss())
    .pipe(rename({ extname: ".min.css" }))
    .pipe(dest("dist"));
};
exports.default = css;

这样就完成了 css 的简单构建。


Gulp 完整案例


下面演示一下使用 Gulp 来编译一个 React 项目的案例。

项目源码在 github 上面,可以使用 git clone 下载。

git 地址:git@github.com:luzhenqian/gulp-react-demo.git

完成效果如下:

效果图1效果图2

项目的整体结构如下:


.
|____public
| |____favicon.ico
| |____index.html
|____src
| |____assets
| | |____images
| | | |____zhuangzhou.jpg
| | | |____laofuzi.jpg
| | | |____wuzetian.jpg
| | | |____damo.jpg
| | | |____miyue.jpg
| | | |____xiangyu.jpg
| | | |____yangyuhuan.jpg
| | | |____lvbu.jpg
| | |____styles
| | | |____index.scss
| | |____scripts
| | | |____index.jsx
| | |____fonts
| | | |____FiraCode-Light.ttf
|____package.json

其中 public 目录下放置的都是静态文件,无需编译。

src 目录下存放的是源代码,都需要经过编译后才可以使用。

简单起见,项目中并不会涉及到模块化的处理。所有的 React 代码都会放到同一个文件中。

准备工作是把 gulp 安装上。


npm i -D gulp


样式编译


const { src, dest } = require("gulp");
const style = () => {
  return src("src/assets/styles/*.scss").pipe(dest("dist"));
};

这样就可以将 /src/assets/styles 目录下的所有 scss 文件都拷贝到 dist 下面。

但是我们希望按照 src 目录下的目录结构来放置这些文件,所以需要在 src 函数中再添加一个参数用来指定基准路径。


const { src, dest } = require("gulp");
const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" }).pipe(dest("dist"));
};
module.exports = { style };

这样就会将 src 之后的目录保留下来,放置到 dist 目录下。

这样就完成了输入和输出。

接来下就是文件的加工转换,将 scss 文件转换成浏览器可以运行的 css 文件。转换需要使用 gulp-sass 这个插件,它会自动安装 node-sass 插件。所以我们只需要安装它自己就可以。


npm i -D gulp-sass

在 style 任务中添加转换。


const { src, dest } = require("gulp");
const sass = require("gulp-sass");
const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(
      sass({ /** 指定输出样式为尾括号单独占用一行 */ outputStyle: "expanded" })
    )
    .pipe(dest("dist"));
};
module.exports = { style };

这样就完成了 sass 转换成 css 的任务。


脚本编译


按照样式文件的任务照写一份,区别是匹配文件不同和转换插件不同。

脚本文件的转换需要依赖 gulp-babel 插件。

和 gulp-sass 不同的是,gulp-babel 插件不会帮助我们安装核心转换模块,所以需要手动安装。babel 的核心转换模块是@babel/core。但是 core 只是转换的平台,还需要告诉它具体按照哪些规则去转换,所以还要安装转换 ES6 语法的库 @babel/preset-env 和转换 React 语法的库 @babel/preset-react。


npm i -D gulp-babel @babel/core @babel/preset-react

最后编写 script 任务。


// ... other code
const script = () => {
  return src("src/**/*.js?(x)", { base: "src" })
    .pipe(babel({ presets: ["@babel/preset-env", "@babel/preset-react"] }))
    .pipe(dest("dist"));
};
module.exports = { style, script };

其中 src 的第一个参数中两个星号(*)代表匹配 src 目录下任意子目录。


页面模板编译


页面中使用到了 ejs 模板语法,所以要使用 ejs。

安装 ejs。


npm install --save-dev gulp-ejs

在编译样式和脚本的过程中,我们已经知道了输出的文件路径在哪里,所以可以通过编译过程中自动注入的形式来帮我们注入生成后的样式和脚本文件。

这里借助一个叫做 gulp-html-replace 的插件。


npm i -D gulp-html-replace

编写任务。


const ejs = require("ejs");
const htmlReplace = require("gulp-html-replace");
// ... other code
const html = () => {
  return src("public/index.html", { base: "public" })
    .pipe(ejs({ title: "王者荣耀 小站" }))
    .pipe(
      htmlReplace(
        {
          js: {
            src: "./assets/scripts/",
            tpl: '<script src="%s%f.js"></script>',
          },
          css: {
            src: "./assets/styles/",
            tpl: '<link rel="stylesheet" href="%s%f.css">',
          },
        },
        { resolvePaths: true }
      )
    )
    .pipe(dest("dist"));
};

htmlReplace 会自动识别 html 文件中的注释,并在注视的位置插入生成的文件引用。


<!-- build:css -->
<!-- endbuild -->
<!-- build:js -->
<!-- endbuild -->


组合页面、脚本、样式任务


完成页面、脚本、样式的编译任务后,每次执行时都需要执行不同的任务,非常麻烦,这时就可以将这三个任务组合成一个任务。

因为这三个任务之间没有依赖关系,可以并行执行。


const { src, dest, parallel } = require("gulp");
// ... other code
const compile = parallel(style, script, html);
module.exports = { compile };


图片和字体文件转换


图片的转换只需要压缩,所以要借助 gulp-imagemin 插件。


npm i -D gulp-imagemin

图片的压缩会删除掉无用信息,但是不会影响最终呈现效果。svg 这种图片只是会格式化代码。

由于 font 一般是没办法压缩的,所以只需要拷贝过去,但是考虑到 font 中可能存在 svg,所以最好也经过 imagemin 处理一下。


// ... other code
const imagemin = require("gulp-imagemin");
const imageAndFont = () => {
  return src(["src/assets/fonts/**", "src/assets/images/**"], {
    base: "src",
  })
    .pipe(imagemin())
    .pipe(dest("dist"));
};
const compile = parallel(style, script, html, imageAndFont);

gulp 的 src 函数可以处理多个不同路径的文件,传入一个数组就可以。

最后把 imageAndFont 任务也加入到 compile 中去。


静态文件


public 目录下的文件是要被原封不动的拷贝过去的,但有个例外,就是 index.html。如果不过滤掉 index.html,就可能出现文件覆盖的问题。

因为 public 不需要编译,所以为了区分编译(compile)和构建(build),再添加一个 build 任务,来完成所有文件的构建工作。


const extra = () => {
  return src(["public/**", "!public/index.html"], { base: "public" }).pipe(
    dest("dist")
  );
};
const compile = parallel(style, script, html, imageAndFont);
const build = parallel(compile, extra);
module.exports = { compile, build };


文件清除


因为每次构建时不会清除上一次的构建结果,而是覆盖同名文件的方式。这样在开发过程中会产生很多无用的垃圾文件。

这时可以在每次构建之前删除掉上次的构建结果。

借助 del 这个插件。


npm i -D del

然后添加 clean 任务。


const del = require("del");
const clean = () => {
  return del(["dist"]);
};

del 函数返回的是一个 Promise,所以不需要手动标记任务结束,它会自动完成。

在每次编译之前,清除掉上一次的编译结果,这是一个同步任务。

改造 build 任务。


const { src, dest, parallel, series } = require("gulp");
// ... other code
const build = series(clean, parallel(compile, extra));

这样就完成了文件清理。


自动加载插件


随着构建任务的复杂度增加,所依赖的构建插件也越来越多,不利于我们阅读和回顾代码。

这时可以使用 gulp-load-plugins 来解决 require 过多的问题。


npm i -D gulp-load-plugins

接下来使用 load plugins。


const loadPlugins = require("gulp-load-plugins");
const plugins = loadPlugins();

这样所有的 gulp 插件都会被挂载到 plugins 对象上面。

对象属性的命名规则是去掉插件的前缀(gulp-),后面的名字转换为驼峰命名法。

比如 gulp-html-replace,会被转换成 plugins.htmlReplace


开发服务器


在开发阶段,我们需要一个支持热更新的开发服务器。这样在我们修改代码后,实时的对源代码进行编译并自动刷新浏览器。


npm i -D browser-sync

接下来在代码中使用 browser sync。


const browserSync = require("browser-sync");
// browser sync 提供了 create 方法,用来创建一个服务器
const bs = browserSync.create();
// ... other code
// 创建一个新任务 用于启动服务器
const serve = () => {
  bs.init({
    notify: false, // 是否显示 启动时提示
    port: 1998, // 端口
    // open: false,// 是否自动在浏览器打开
    files: "dist/**", // 监听哪些文件发生了变化,自动刷新浏览器
    server: {
      baseDir: "dist", // 基础目录
    },
  });
};

这样就可以实现一个简单的开发服务器。


监视变化


使用开发服务器启动的服务,只能监视生产代码,即 dist 目录下的代码发生变化,而不能监听到源代码发生的变化。

修改源代码,让浏览器自动刷新的思路是:监听源代码的变化,并且在源代码发生变化时,重新执行对应的构建任务,编译后的文件会覆盖掉 dist 下对应的文件,从而触发 browser sync 的监听,刷新浏览器页面。

源代码的变化,可以使用 gulp 提供的 watch 方法来实现。

watch 的用法很简单,传入两个参数,第一个是监听的文件列表,第二个是文件变化后执行的任务。


const { src, dest, parallel, series, watch } = require("gulp");
// ... other code
const serve = () => {
  // 添加 监听
  watch("src/assets/styles/*.scss", style);
  watch("src/**/*.js?(x)", script);
  watch("public/index.html", html);
  watch(["src/assets/fonts/**", "src/assets/images/**"], imageAndFont);
  watch(["public/**", "!public/index.html"], extra);
  bs.init({
    notify: false,
    port: 1998,
    // open: false,
    files: "dist/**",
    server: {
      baseDir: "dist",
    },
  });
};

这样就实现了修改源代码浏览器自动刷新。


构建优化


接下来再做一些优化。

因为图片、字体和 public 下的静态资源都是不需要编译的,它们的任务都是一些压缩工作。

这在上线部署的时候有意义,但是在开发阶段会增加无用的开销,反而会影响热更新构建速度。

但是又不能不管这些文件,因为开发阶段我们也有可能会修改这些文件。

这时的优化是修改 browser sync 的配置。


const serve = () => {
  watch("src/assets/styles/*.scss", style);
  watch("src/**/*.js?(x)", script);
  watch("public/index.html", html);
  // 删除下面两个任务
  // watch(['src/assets/fonts/**', 'src/assets/images/**'], imageAndFont)
  // watch(['public/**', '!public/index.html'], extra)
  // 修改为监听文件 刷新浏览器,而不是执行构建任务
  watch(
    [
      "src/assets/fonts/**",
      "src/assets/images/**",
      "public/**",
      "!public/index.html",
    ],
    bs.reload
  );
  bs.init({
    notify: false,
    port: 1998,
    files: "dist/**",
    server: {
      // 在baseDir 中添加 src 和 public 目录
      // 这样在 dist 中找不到时回去 后面的目录中继续找
      baseDir: ["dist", "src", "public"],
    },
  });
};

完成服务器的修改后,还需要添加一个任务。

因为目前启动开发服务器之前,没有进行编译。

所以添加一个 develop 任务,这个任务是一个同步的组合任务,先去编译,再去启动服务器。


const develop = series(compile, serve);

还可以把 compile 任务下的 imageAndFont 任务也拿到 build 任务中。


const compile = parallel(style, script, html);
const build = series(clean, parallel(compile, extra, imageAndFont));

这样就做到了以最小代价支持开发阶段所有构建而又不影响上线部署前的构建。


browser sync reload 的另一种写法


除了 files: "dist/**"这种配置写法以外,还有另外一种写法更为常见,就是在一个构建任务最后一步添加一个 pipe,在其中传入 bs.reload。

拿 style 举例,script 和 html 任务同样可以这样改造,改造完后要记得去掉 files 配置。


const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(plugins.sass({ outputStyle: "expanded" }))
    .pipe(dest("dist"))
    .pipe(bs.reload({ stream: true /** 以流的方式执行 */ }));
};

这两种写法作用上是等效的,但是第二种写法可以减少构建次数。


useref 文件引用处理


目前核心的构建任务都已经完成了,但还存在一些小问题。

在 index.html 中使用的 bootstrap.css 是通过开发服务器的路由来映射的,在开发阶段没有任何问题,但是在生产代码的构建后就会丢失这个样式。

这个情况我们可以手动拷贝对应的 css,但是这种做法并不优雅,可以使用更加优雅的方式,借助 useref 插件。


npm i -D gulp-useref

因为在 index.html 中的 css 引用具有注释,useref 会自动找到这些标记,将它们抽取出来,并生成对应的文件,再插入到 index.html 中。

这里有一个需要注意的问题,整个 link 标签或者 script 标签只能占据一行,有些自动格式化的工具会将过长的标签折换成多行。如果一个标签占用多行时会出现生成文件后无法重新写入新标签的问题。


<!-- build:css assets/styles/vendor.css -->
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css" />
<!-- endbuild -->

编写 useref 任务。


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

由于 useref 可以帮助我们抽取资源并自动转换引用地址,所以我们可以改造 index.html 来将 react、react-dom 和 react-bootstrap 打包成一个 vendor.js。

改造后的 index.html:


<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title><%= title %></title>
    <!-- build:css assets/styles/vendor.css -->
    <link
      rel="stylesheet"
      href="/node_modules/bootstrap/dist/css/bootstrap.min.css"
    />
    <!-- endbuild -->
    <!-- build:css -->
    <!-- endbuild -->
  </head>
  <body>
    <div id="app"></div>
    <!-- build:js assets/scripts/vendor.js-->
    <script src="/node_modules/react/umd/react.production.min.js"></script>
    <script src="/node_modules/react-dom/umd/react-dom.production.min.js"></script>
    <script src="/node_modules/react-bootstrap/dist/react-bootstrap.min.js"></script>
    <!-- endbuild -->
    <!-- build:js -->
    <!-- endbuild -->
  </body>
</html>

由于 useref 会将 node_modules 中的通用 js 文件都抽取出来,所以也就不需要在 index.jsx 中导入对应的包了,这样会重复引用。


// 删除掉这三行
// import React from "react";
// import ReactDOM from "react-dom";
// import { Button } from 'react-bootstrap';


文件压缩


对生成文件进行压缩。

主要是对 html、css 和 js 进行压缩。

安装三种文件对应的压缩插件。


npm i gulp-htmlmin gulp-uglify gulp-clean-css -D

对代码压缩的位置可以在 useref 中进行。

useref 在处理完 html 的文件流中会把 html、js 和 css 文件传递到下一个流。所以我们需要判断一下不同类型的文件,再进行不同的处理,这里需要借助 gulp-if 插件。


npm i -D gulp-if

修改 useref 任务。


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 /** 内联 css 压缩 */,
          minifyJS: true /** 内联 js 压缩 */,
        })
      )
    )
    .pipe(dest("release"));
};

最终写入 release 的原因是同时读写 dist 目录会出现冲突,必须写到另一个不受影响的目录。

但是这样,原来构建规划的目录结构就被打破了。


重新规划构建过程


在 useref 之前构建的产物,其实都是临时产物,并不是最终部署上线的生产代码,所以我们可以把它们提取到一个 temp 目录下。

由于改动量比较大,这里直接把代码贴出来。


const { src, dest, parallel, series, watch } = require("gulp");
const del = require("del");
const loadPlugins = require("gulp-load-plugins");
const browserSync = require("browser-sync");
const plugins = loadPlugins();
const bs = browserSync.create();
const clean = () => {
  return del(["dist", "temp"]);
};
const style = () => {
  return src("src/assets/styles/*.scss", { base: "src" })
    .pipe(plugins.sass({ outputStyle: "expanded" }))
    .pipe(dest("temp"))
    .pipe(bs.reload({ stream: true }));
};
const script = () => {
  return src("src/**/*.js?(x)", { base: "src" })
    .pipe(
      plugins.babel({ presets: ["@babel/preset-react", "@babel/preset-env"] })
    )
    .pipe(
      plugins.browserify({
        insertGlobals: true,
      })
    )
    .pipe(dest("temp"))
    .pipe(bs.reload({ stream: true }));
};
const html = () => {
  return src("public/index.html", { base: "public" })
    .pipe(plugins.ejs({ title: "lzq site" }))
    .pipe(
      plugins.htmlReplace(
        {
          js: {
            src: "./assets/scripts/",
            tpl: '<script src="%s%f.js"></script>',
          },
          css: {
            src: "./assets/styles/",
            tpl: '<link rel="stylesheet" href="%s%f.css">',
          },
        },
        { resolvePaths: true }
      )
    )
    .pipe(dest("temp"))
    .pipe(bs.reload({ stream: true }));
};
const imageAndFont = () => {
  return src(["src/assets/fonts/**", "src/assets/images/**"], {
    base: "src",
  })
    .pipe(plugins.imagemin())
    .pipe(dest("dist"));
};
const extra = () => {
  return src(["public/**", "!public/index.html"], { base: "public" }).pipe(
    dest("dist")
  );
};
const useref = () => {
  return src("temp/**", { 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 /** 内联 css 压缩 */,
          minifyJS: true /** 内联 js 压缩 */,
        })
      )
    )
    .pipe(dest("dist"));
};
const serve = () => {
  watch("src/assets/styles/*.scss", style);
  watch("src/**/*.js?(x)", script);
  watch("public/index.html", html);
  watch(
    [
      "src/assets/fonts/**",
      "src/assets/images/**",
      "public/**",
      "!public/index.html",
    ],
    bs.reload
  );
  bs.init({
    notify: false,
    port: 1998,
    // files: 'dist/**',
    server: {
      baseDir: ["temp", "src", "public"],
      routes: {
        "/node_modules": "node_modules",
      },
    },
  });
};
const compile = parallel(style, script, html);
const build = series(
  clean,
  parallel(series(compile, useref), extra, imageAndFont)
);
const develop = series(compile, serve);
module.exports = { compile, build, clean, serve, develop, useref };

到这里,基本的构建流程就已经结束了。


规整任务


在 gulp 中有非常多的任务,如果全部导出去会非常复杂。因为很多任务并不会单独使用。

分析一下,真正需要导出去的任务只有 build, clean 和 develop。


// ... other code
module.exports = { build, clean, develop };

在 package.json 中把它们添加到 scripts 下。


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

这样就可以直接通过 npm run build 的方式启动构建任务了。



相关文章
|
1天前
|
消息中间件 运维 Kubernetes
构建高效自动化运维体系:Ansible与Kubernetes的融合实践
【5月更文挑战第9天】随着云计算和微服务架构的普及,自动化运维成为确保系统可靠性和效率的关键。本文将深入探讨如何通过Ansible和Kubernetes的集成,构建一个强大的自动化运维体系。我们将分析Ansible的配置管理功能以及Kubernetes容器编排的优势,并展示如何将二者结合,以实现持续部署、快速扩展和高效管理现代云原生应用。文章还将涵盖实际案例,帮助读者理解在真实环境下如何利用这些工具优化运维流程。
|
1天前
|
运维 监控 安全
构建高效自动化运维系统:基于容器技术的持续集成与持续部署(CI/CD)实践
【5月更文挑战第14天】 随着DevOps文化的深入人心,持续集成与持续部署(CI/CD)已成为现代软件工程不可或缺的组成部分。本文将探讨如何利用容器技术,尤其是Docker和Kubernetes,构建一个高效、可扩展的自动化运维系统。通过深入分析CI/CD流程的关键组件,我们将讨论如何整合这些组件以实现代码从提交到生产环境的快速、无缝过渡。文章还将涉及监控、日志管理以及安全性策略等运维考量,为读者提供一个全面的自动化运维解决方案蓝图。
|
1天前
|
运维 Kubernetes 持续交付
构建高效自动化运维体系:基于容器技术的持续集成与部署实践
【5月更文挑战第13天】 在现代软件开发周期中,持续集成(CI)和持续部署(CD)已成为提升开发效率、保障产品质量的关键环节。随着云计算和微服务架构的普及,容器技术如Docker和Kubernetes为运维领域带来了革命性的变革。本文旨在探讨如何利用容器技术构建一个高效、可靠的自动化运维体系,实现从代码提交到产品发布的全过程自动化管理。通过深入分析容器化技术的核心原理,结合实际案例,我们将阐述如何优化持续集成流程、确保自动化测试的覆盖率、以及实现无缝的持续部署。
14 2
|
1天前
|
运维 安全 API
构建高效自动化运维体系:Ansible与Docker的协同实践
【5月更文挑战第13天】在现代IT基础设施管理中,自动化运维已成为提升效率、确保一致性和降低人为错误的关键。本文通过深入探讨Ansible和Docker的集成实践,揭示了如何构建一个灵活、可扩展且高效的自动化运维体系。我们将从理论到实践,展示如何利用这两种技术实现自动化部署、管理和扩展应用服务,以及它们如何帮助运维团队应对快速变化的业务需求和复杂的IT环境。
16 1
|
1天前
|
存储 JavaScript 前端开发
使用Vue.js构建交互式前端的技术探索
【5月更文挑战第12天】Vue.js是渐进式前端框架,以其简洁和强大的特性深受开发者喜爱。它聚焦视图层,采用MVVM模式实现数据与视图的双向绑定,简化开发。核心特性包括响应式数据绑定、组件化、模板系统和虚拟DOM。通过创建Vue实例、编写模板及定义方法,可以构建交互式前端,如计数器应用。Vue.js让复杂、交互式的前端开发变得更加高效和易维护。
|
1天前
|
Linux Shell C语言
Linux:自动化构建 - make
Linux:自动化构建 - make
13 1
|
1天前
|
Java Maven
Maven 自动化构建
Maven自动化构建确保依赖稳定性:当bus-core-api项目构建完成后,app-web-ui和app-desktop-ui项目自动构建。这两个项目依赖bus-core-api的1.0-SNAPSHOT版本。通过Maven,团队能高效管理项目间的依赖关系,实现无缝集成和更新。
|
1天前
|
数据库 Docker 容器
【Docker 专栏】使用 Dockerfile 自动化构建 Docker 镜像
【5月更文挑战第8天】Dockerfile是构建Docker镜像的关键,它包含一系列指令,用于描述应用运行环境及所需软件包。通过自动化构建,能提高效率、保证可重复性并提升灵活性。确定基础镜像、安装依赖、设置环境后,执行Dockerfile生成镜像,用于应用程序部署。虽然需要熟悉Docker技术和应用细节,但其带来的益处使其成为现代软件开发和部署的重要工具。
【Docker 专栏】使用 Dockerfile 自动化构建 Docker 镜像
|
1天前
|
运维 Kubernetes 监控
构建高效自动化运维体系:基于Ansible的策略与实践
【5月更文挑战第8天】 在当今IT基础设施管理领域,自动化不再是一个选择,而是必要的步骤。随着复杂性的增加和变更的频繁性,自动化工具如Ansible提供了一种高效、可靠的解决方案来简化配置管理和多节点部署。本文将探讨如何利用Ansible构建一个高效的自动化运维体系,涵盖其核心原理、策略设计以及在实际环境中的应用。我们将分析Ansible与其他自动化工具的不同之处,并提供一些最佳实践,以帮助运维专家提升他们的工作效率和系统稳定性。
|
1天前
|
运维 负载均衡 持续交付
构建高效自动化运维体系:Ansible与Docker的协同实践
【5月更文挑战第7天】 在当今快速迭代的软件开发环境中,自动化运维成为确保部署效率和一致性的关键。本文将探讨如何通过结合Ansible和Docker技术,构建一个高效的自动化运维体系,旨在提升运维效率,减少人为错误,并实现持续集成与持续部署(CI/CD)的流程自动化。文章详细阐述了Ansible的配置管理机制、Docker容器化的优势,以及二者在实际运维场景中的结合应用,为读者提供一套可行的自动化运维解决方案。