【万字长文】通过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 的方式启动构建任务了。



相关文章
|
11天前
|
监控 jenkins 测试技术
自动化测试框架的构建与实践
【10月更文挑战第40天】在软件开发周期中,测试环节扮演着至关重要的角色。本文将引导你了解如何构建一个高效的自动化测试框架,并深入探讨其设计原则、实现方法及维护策略。通过实际代码示例和清晰的步骤说明,我们将一起探索如何确保软件质量,同时提升开发效率。
28 1
|
17天前
|
测试技术 开发者 Python
自动化测试之美:从零构建你的软件质量防线
【10月更文挑战第34天】在数字化时代的浪潮中,软件成为我们生活和工作不可或缺的一部分。然而,随着软件复杂性的增加,如何保证其质量和稳定性成为开发者面临的一大挑战。自动化测试,作为现代软件开发过程中的关键实践,不仅提高了测试效率,还确保了软件产品的质量。本文将深入浅出地介绍自动化测试的概念、重要性以及实施步骤,带领读者从零基础开始,一步步构建起属于自己的软件质量防线。通过具体实例,我们将探索如何有效地设计和执行自动化测试脚本,最终实现软件开发流程的优化和产品质量的提升。无论你是软件开发新手,还是希望提高项目质量的资深开发者,这篇文章都将为你提供宝贵的指导和启示。
|
2月前
|
JavaScript 前端开发 Docker
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
在使用 Deno 构建项目时,生成的可执行文件体积较大,通常接近 100 MB,而 Node.js 构建的项目体积则要小得多。这是由于 Deno 包含了完整的 V8 引擎和运行时,使其能够在目标设备上独立运行,无需额外安装依赖。尽管体积较大,但 Deno 提供了更好的安全性和部署便利性。通过裁剪功能、使用压缩工具等方法,可以优化可执行文件的体积。
119 3
前端全栈之路Deno篇(二):几行代码打包后接近100M?别慌,带你掌握Deno2.0的安装到项目构建全流程、剖析构建物并了解其好处
|
21天前
|
监控 前端开发 JavaScript
探索微前端架构:构建可扩展的现代Web应用
【10月更文挑战第29天】本文探讨了微前端架构的核心概念、优势及实施策略,通过将大型前端应用拆分为多个独立的微应用,提高开发效率、增强可维护性,并支持灵活的技术选型。实际案例包括Spotify和Zalando的成功应用。
|
25天前
|
缓存 监控 前端开发
前端工程化:Webpack与Gulp的构建工具选择与配置优化
【10月更文挑战第26天】前端工程化是现代Web开发的重要趋势,通过将前端代码视为工程来管理,提高了开发效率和质量。本文详细对比了Webpack和Gulp两大主流构建工具的选择与配置优化,并提供了具体示例代码。Webpack擅长模块化打包和资源管理,而Gulp则在任务编写和自动化构建方面更具灵活性。两者各有优势,需根据项目需求进行选择和优化。
57 7
|
24天前
|
缓存 前端开发 JavaScript
前端工程化:Webpack与Gulp的构建工具选择与配置优化
【10月更文挑战第27天】在现代前端开发中,构建工具的选择对项目的效率和可维护性至关重要。本文比较了Webpack和Gulp两个流行的构建工具,介绍了它们的特点和适用场景,并提供了配置优化的最佳实践。Webpack适合大型模块化项目,Gulp则适用于快速自动化构建流程。通过合理的配置优化,可以显著提升构建效率和性能。
33 2
|
25天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
36 2
|
27天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
2月前
|
运维 监控 jenkins
运维自动化实战:利用Jenkins构建高效CI/CD流程
【10月更文挑战第18天】运维自动化实战:利用Jenkins构建高效CI/CD流程
|
2月前
|
前端开发 API UED
拥抱微前端架构:构建灵活、高效的前端应用
【10月更文挑战第17天】微前端架构是一种将前端应用拆分为多个小型、独立、可复用的服务的方法,每个服务可以独立开发、部署和维护。本文介绍了微前端架构的核心概念、优势及实施步骤,并分享了业界应用案例和职业心得,帮助读者理解和应用这一新兴架构模式。
下一篇
无影云桌面