【学习笔记】Part2·前端工程化实战--开发脚手架及封装自动化构建工作流(三、自动化构建 -- 主Grunt)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(一、工程化概述)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(二、脚手架工具)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Grunt)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Gulp)

自动化构建简介

自动化构建,是前端工程化当中一个非常重要的组成部分。

自动化:通过机器代替手工完成一些工作,构建:转换 - 把一个东西转换成另外一个东西

开发行业的自动化构建:
把我们开发阶段写出来的源代码,自动化的转换成生产环境中可以运行的代码或者程序。
在这里插入图片描述
一般我们把这个转换的过程称为自动化构建工作流。

作用就是让我们脱离运行环境兼容带来的种种问题,在开发阶段使用一些提高效率的语法、规范和标准。

最典型的应用场景就是我们在去开发网页应用时,我们就可以使用 ECMAScript Next 最新标准去提高编码效率和质量,利用 SASS 和 Less 去增强 CSS 的可编程性,借助模板引擎去抽象页面当中重复的 HTML
在这里插入图片描述
这些用法大都不被浏览器直接支持。

这种情况下,自动化构建工具就派上用场了,通过自动化构建的方式,将这些不被支持的特性转换成能够直接运行的代码,这样我们就可以在开发过程中通过这些方式去提高我们编码效率了。

自动化构建初体验

这里通过一个小案例来体会自动化构建的便捷之处:

案例中,我们一开始使用的是直接编写 css 的方式去完成网页的样式
在这里插入图片描述
但是我们希望,通过 sass 去增强 css 的编程性,具体的实现方式就是在开发时我们添加一个构建的环节,这样让我们就可以在开发环节通过 sass 去编写样式,再通过工具将 sass 去构建为 css
在这里插入图片描述
具体看一下如何操作:

这里准备了一个普通的 html 页面,css简单的随便写了点样式
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<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>My Web App</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <h1>My Web App</h1>
  <p>这是一个普通的网页应用</p>
</body>
</html>

body {
    
    
    margin: 0 auto;
    padding: 20px;
    max-width: 800px;
    background-color: #f5f5f5;
    color: #333333;
}

但是我们这里想使用 sass ,所以就不需要 css 文件了,删除 css,新建个 sass/main.scss
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这个 scss 文件当中我们就可以按照 scss 的语法去编写我们的网页样式,相对于 css ,scss 的编程能力肯定是更强一些。

$body-bg: #F7F7F7;
$body-color: #333333;

body {
    
    
    margin:0 auto;
    padding: 20px;
    max-width:800px;
    background-color: $body-bg;
    color: $body-color;
}

但是 scss 并不能在我们的浏览器环境当中直接去使用,所以说我们需要在开发阶段通过一个工具把它转换成 css,这里使用的是 sass 官方提供的一个 sass 模块。

注意:这里是开发依赖安装

yarn add sass --dev

在这里插入图片描述
安装之后就会在 node_modules 下边 .bin 出现一个 sass 的命令文件
在这里插入图片描述
我们可以在命令行找到这个命令,可以看到输出一些信息,有 sass 的具体的用法:

.\node_modules\.bin\sass

在这里插入图片描述
运行一下

# .\node_modules\.bin\sass sass输入路径 css输出路径
.\node_modules\.bin\sass scss/main.scss css/style.css

在这里插入图片描述
自动的将我们的 sass 文件转换成 css文件了,而且还帮我们添加了对应的 sourceMap 文件,这样我们在调试阶段就可以定位到源代码当中的位置了

但是这样也有一个比较麻烦的地方,就是每次都需要重复的输入这么个复杂的命令,而且别人接手你的项目过后,他也不知道如何去运行这些构建的任务,所以说我们需要做一些额外的事情去解决这些在项目开发阶段重复去执行的命令。

Npm Scripts 主要就是用来解决这样一个问题的,可以在 npm 的 scripts 中定义一些与这个项目开发过程有关的一些脚本命令,这样一来,就可以让这些命令跟着项目去维护,便于我们在后期开发过程当中的使用。

所以这儿最好的方式就是通过 NPM Scripts 的方式去包装构建命令,具体的实现方式就是在 package.json 当中添加 scripts 项:

需要注意的是,scripts 可以自动去发现 node_modules 里边的命令,所以我们不需要写完整的路径,直接使用命令的名称就可以了(sass):
在这里插入图片描述

{
    
    
  "name": "demo01",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts":{
    
    
    "build": "sass scss/main.scss css/style.css"
  },
  "devDependencies": {
    
    
    "sass": "^1.29.0"
  }
}

命令行直接运行这个指令就行了

# npm run build
yarn build

在这里插入图片描述
另外 NPM Sctipts 也是实现自动化构建工作流最简单的方式,接下来看一下如何通过它去实现自动化构建:

安装一个 browser-sync 模块,用于启动一个测试服务器去运行我们的项目

yarn add browser-sync --dev

在这里插入图片描述
安装之后在 scripts 当中添加一个 serve 的命令,在命令当中通过 browser-sync 把当前的目录给运行起来:

  "scripts": {
    
    
    "build": "sass scss/main.scss css/style.css",
    "serve": "browser-sync ."
  },

我们去命令行运行一下 serve 的命令 yarn serve:
在这里插入图片描述

在这里插入图片描述
自动启动一个 web 服务器并且帮我们唤起浏览器运行我们当前这个网页。

但是如果在 browser-sync 工作之前,我们并没有生成我们的 css 样式,那此时它再工作的时候我们的页面就没有 css 样式。我们需要在启动 serve 命令之前,让 build 命令去工作,所以我们这里可以借助 NPM Scripts 的钩子机制,去定义一个 preserve,它会在 serve 命令执行之前自动执行。
在这里插入图片描述

  "scripts": {
    
    
    "build": "sass scss/main.scss css/style.css",
    "preserve": "yarn build",
    "serve": "browser-sync ."
  },

这个时候再去执行 serve ,它就会自动的先去执行 build 命令,build 完成过后再去执行对应的 serve。
在这里插入图片描述

在这里插入图片描述
此时我们就可以完成在启动 serve 之前,自动构建我们的 sass 文件,光有这些还不够,我们还可以为 sass 命令添加一个 --watch 的参数,有了这个参数过后,sass 在工作的时候就会监听 scss 文件的变化,一旦 scss 文件发生变化之后,他就会自动重新编译。

  "scripts": {
    
    
    "build": "sass scss/main.scss css/style.css --watch",
    "preserve": "yarn build",
    "serve": "browser-sync ."
  },

重新运行 serve 命令
在这里插入图片描述
会发现,sass 命令在工作时,命令行会阻塞在这个地方,去等待文件的变化,这样就导致了后面的 browser-sync 没办法去工作,这样就导致了我们需要同时去执行多个任务,这里可以借助于 npm-run-all 这个模块去实现。

yarn add npm-run-all --dev

在这里插入图片描述
有了这个模块之后,我们就可以在 scripts 中添加一个新的命令,这个命令我们叫做 start ,这个命令里边我们通过 npm-run-all 里边的 run-p 的命令同时去执行build 和 serve 命令:

  "scripts": {
    
    
    "build": "sass scss/main.scss css/style.css --watch",
    "serve": "browser-sync .",
    "start": "run-p build serve"
  },

命令行运行 yarn start 命令,就会发现,build 和 serve 命令同时执行了,我们尝试打开去修改 scss 文件内容:
在这里插入图片描述
在这里插入图片描述
就会发现 css 里边的内容也会跟着变化,也就证明我们的 watch 已经生效了。

但是你会发现,虽然 css 文件变了,但是浏览器的样式并没有跟着变化。

这里我们可以给 browser-sync 这个命令添加一个 --files 的参数,这个参数可以让 browser-sync 启动过后去监听项目下某些文件的变化。一旦这些文件发生变化过后,这些文件的内容 browser-sync 会自动的同步到浏览器,从而更新浏览器当中的界面,让我们可以及时查看到最新的效果,这样就避免了我们修改代码过后,再去手动刷新浏览器的重复工作。

  "scripts": {
    
    
    "build": "sass scss/main.scss css/style.css --watch",
    "serve": "browser-sync . --files \"css/*.css\"",
    "start": "run-p build serve"
  },

再次启动
在这里插入图片描述
修改 scss 文件样式再次查看:
在这里插入图片描述
在这里插入图片描述
这样我们就借助于 NPM Scripts 完成了一个简单的自动化构建的工作流。

具体就是在启动任务过后,同时运行了 build 和 serve 这两个命令,其中 build 自动监听 scss 文件的变化,去编译 scss ,browser-sync 启动一个 web 服务,当文件发生变化过后去刷新浏览器。

常用的自动化构建工具

NPM Scripts 它确实能解决一些自动化构建的一部分任务,但是对于相对复杂的构建过程,NPM Scripts 就显得相对吃力,这时候我们就需要更为专业的构建工具,这里我们怼市面上相对常用的构建工具做个介绍,先有一个整体的认识,后面更深入的探究。
在这里插入图片描述
目前使用相对较多的工具主要就是 Grunt Gulp FIS,可能有人会问: webpack 去哪了,严格来说,webpack 它实际上是一个模块打包工具,所以不在这里讨论范围之内。

这些工具都可以帮助我们解决那些重复并且无聊的工作,从而实现自动化,用法上大体相同,都是通过一些简单的代码,去组织一些插件的使用,然后就可以使用这些工具去帮你执行各种各样重复的工作了。

Grunt 算是最早的前端构建系统了,插件生态非常的完善,用官方的话来说,Grunt 几乎可以帮你自动化的完成任何你想要做的事情,但是,由于它的工作过程是基于临时文件实现的,所以它的构建速度相对较慢。

例如我们使用它去完成我们项目当中对 sass 文件的构建,我们一般会先对 sass 文件做编译操作,再去自动添加一些私有属性的前缀,最后再去压缩代码,这样一个过程当中,Grunt 每一步都会有磁盘读写操作,比如 sass 文件编译完成过后,就会将结果写入到一个临时文件,然后下一个插件再去读取这个临时文件,进行下一步操作,这样一来,处理的环节越多,读写的次数也就越多,对于超大型项目当中,项目文件会非常多,构建速度就会非常慢。

而 Gulp 很好的解决了 Grunt 构建速度非常慢的这样的问题,因为它是基于内存实现的,也就是说它对于文件的处理都是在内存当中实现的,相对于磁盘读写速度自然就快了很多,另外,它默认支持同时执行多个任务,效率自然大大提高,而且它的使用方式相对于 Grunt 更加直观易懂,它的生态也同样非常完善,所以它后来居上,目前更受欢迎。应该是目前前端最流行的构建系统了。

再看 FIS ,它是百度的前端团队推出的一款构建系统,最早只是在他们内部使用,后来开源过后开始在国内流行,相对于前面两个构建系统这种微内核的特点,FIS 更像是一种捆绑套餐,它把我们项目当中一些典型需求尽可能都集成在内部了。

例如在 FIS 中可以很轻松去处理资源加载,模块化开发,代码部署甚至是性能优化,正是因为这种大而全,所以在国内很多项目当中就流行开了。

总体来说,如果是初学者,可能 FIS 更适合,如果要求更灵活多变,可能 Gulp Grunt 是更好的选择。

还是那句话:新手是需要规则的,而老手更渴望自有。

也正式因为这个原因,现在这些小而美的框架才会得以流行。

Grunt 的基本使用

接下来我们在一个空项目中去看一下 Grunt 的具体用法:

新建一个空项目,初始化 package.json 文件

mkdir grunt-empty
cd grunt-empty
yarn init --yes

安装 grunt 依赖:

yarn add grunt

在这里插入图片描述
安装完成过后新建一个 gruntfile.js 的文件

# 直接在编辑器打开 gruntfile.js 文件,保存就生成该文件了
code gruntfile.js

在这里插入图片描述

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })
}

命令行运行 yarn grunt foo yarn 会自动帮我们找到 node_modules 里边的 grunt 指令,foo 是我们刚才注册的 foo 任务
在这里插入图片描述
不仅仅可以添加一个任务,还可以注册多个任务,如果说添加任务的时候,第二个参数指定的是一个字符串,这个字符串将会成为这个任务的描述信息,它会出现在 grunt 的帮助信息当中:

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })

    grunt.registerTask('bar', '任务描述', () => {
    
    
        console.log('other task ~')
    })
}

我们可以通过 yarn grunt --help 去得到更多的信息
在这里插入图片描述
也可以通过运行 yarn grunt bar 运行这个任务:
在这里插入图片描述
初次之外,如果定义任务名称的时候叫做 default ,这个任务将会成为 grunt 的默认任务:

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })

    grunt.registerTask('bar', '任务描述', () => {
    
    
        console.log('other task ~')
    })

    grunt.registerTask('default', '默认任务', () => {
    
    
        console.log('default task ~')
    })
}
# 不指定任务名称
yarn grunt

在这里插入图片描述
grunt 将自动调用 default

一般我们会用 default 去映射一些其他的任务,具体是在 registerTask 的第二个参数传入一个数组,这个数组当中可以指定一些任务的名字,这个时候我们执行 default 的时候,grunt 就会依次执行这些任务。

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })

    grunt.registerTask('bar', '任务描述', () => {
    
    
        console.log('other task ~')
    })

    // grunt.registerTask('default', '默认任务', () => {
    
    
    //     console.log('default task ~')
    // })

    grunt.registerTask('default', ['foo', 'bar'])
}

在这里插入图片描述
会发现,grunt 先执行了 foo, 再执行了 bar ,这样就相当于 default 将 foo 和 bar 串联到了一起

再来尝试一下 grunt 对异步任务的支持:

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })

    grunt.registerTask('bar', '任务描述', () => {
    
    
        console.log('other task ~')
    })

    // grunt.registerTask('default', '默认任务', () => {
    
    
    //     console.log('default task ~')
    // })

    grunt.registerTask('default', ['foo', 'bar'])

    grunt.registerTask('async-task', () => {
    
    
        setTimeout(() => {
    
    
            console.log('async task working ~')
        }, 1000);
    })
}

执行以下 yarn grunt async-task
在这里插入图片描述
发现 console.log 并没有直接执行。

这个是 grunt 当中的一个特点,grunt 默认支持同步模式,如果需要异步操作的话,必须要使用 this.async 得到一个回调函数,在异步操作完成过后去调用这个回调函数,标识一下这个异步已经完成。

如果说我们在函数当中使用 this 的话,就不能使用箭头函数了。

// Grunt 入口文件,用于定义一些需要 Grunt 自动执行的任务
// 导出一个函数,函数接收一个叫 grunt 的形参,内部是 grunt 提供的一些 API ,借助这些API可以快速创建一些构建任务

module.exports = grunt => {
    
    
    // 注册任务,参数:任务名、任务函数
    grunt.registerTask('foo', () => {
    
    
        console.log('hello grunt')
    })

    grunt.registerTask('bar', '任务描述', () => {
    
    
        console.log('other task ~')
    })

    // grunt.registerTask('default', '默认任务', () => {
    
    
    //     console.log('default task ~')
    // })

    grunt.registerTask('default', ['foo', 'bar'])

    // grunt.registerTask('async-task', () => {
    
    
    //     setTimeout(() => {
    
    
    //         console.log('async task working ~')
    //     }, 1000);
    // })

    grunt.registerTask('async-task', function (){
    
    
        const done = this.async() // 得到回调函数,此时 grunt 就知道这是一个异步任务,会等待 done 的执行
        setTimeout(() => {
    
    
            console.log('async task working ~')
            done() // 标识这个异步任务已经完成了
        }, 1000);
    })
}

再次执行 yarn grunt async-task
在这里插入图片描述
直到 done 的执行,grunt 才会结束这个任务的执行。

Grunt 标记任务失败

如果你在构建任务的过程中发生错误,例如需要的文件找不到了,此时我们就可以将这个任务标记为一个失败的任务,具体可以在函数体当中 return false 来实现:

module.exports = grunt => {
    
    
    grunt.registerTask('bad', () => {
    
    
        console.log('bad working')
        return false
    })
}

我们尝试运行一下 yarn grunt bad
在这里插入图片描述
终端就提示我们 bad 这个任务执行失败了。

如果说这个任务在一个任务列表当中的话,这个任务的失败会导致后续所有任务都不被执行。例如:

module.exports = grunt => {
    
    
    grunt.registerTask('bad', () => {
    
    
        console.log('bad working')
        return false
    })
    grunt.registerTask('foo', () => {
    
    
        console.log('foo working')
    })
    grunt.registerTask('bar', () => {
    
    
        console.log('bar working')
    })
    grunt.registerTask('default', ['foo', 'bad', 'bar'])
}

正常情况下,运行 default 任务,它会依次执行 foo, bad, bar 任务,但是这里的 bad 执行失败了
在这里插入图片描述
发现 bar 任务就不会被执行。命令行当中也给了提示了,给 --force 参数去强制跳过这个出错的任务,这里尝试一下:yarn grunt default --force 也可以 yarn grunt --force 一样的
在这里插入图片描述
可以看到,即使 bad 任务运行失败了,bar 任务也会正常执行。这就是如何标记任务的失败以及失败过后的一些影响。但是如果任务是一个异步任务的话,异步任务没法在任务当中 return false 去标记任务失败,此时呢,我们需要给异步任务的回调 done 指定一个 false 的实参就可以了。

module.exports = grunt => {
    
    
    grunt.registerTask('bad', () => {
    
    
        console.log('bad working')
        return false
    })
    grunt.registerTask('foo', () => {
    
    
        console.log('foo working')
    })
    grunt.registerTask('bar', () => {
    
    
        console.log('bar working')
    })
    grunt.registerTask('default', ['foo', 'bad', 'bar'])

    grunt.registerTask('bad-async', function (){
    
    
        const done = this.async()
        setTimeout(()=>{
    
    
            console.log('bad-async working bad bad bad');
            done(false)
        })
    })
}

这里执行以下异步任务:yarn grunt bad-async
在这里插入图片描述
可以看到 bad-async 是执行失败的。

Grunt 的配置方法

除了 registerTask 之外 grunt 还提供了去添加一些配置选项的 API ,叫做 InitConfig ,例如我们在使用 grunt 去为我们压缩文件时,我们就可以用这种方式去配置我们需要压缩的文件路径,我们尝试使用一下:

module.exports = grunt => {
    
    
    // 对象属性的参数,属性名(键),一般与我们的任务名称保持一致,属性值可以是任意类型的数据
    grunt.initConfig({
    
    
        foo: 'bar'
    })
    grunt.registerTask('foo', ()=>{
    
    
        grunt.config('foo') // 获取上方添加的配置
        console.log("grunt.config('foo'): ", grunt.config('foo'));
    })
}

运行:yarn grunt foo
在这里插入图片描述
打印出来我们配置的 bar ,意味着刚才的配置是成功的。

如果配置当中属性值是一个对象的话,grunt 当中还支持一种高级的用法,例如:

module.exports = grunt => {
    
    
    // 对象属性的参数,属性名(键),一般与我们的任务名称保持一致,属性值可以是任意类型的数据
    // grunt.initConfig({
    
    
    //     foo: 'bar'
    // })
    grunt.initConfig({
    
    
        foo: {
    
    
            bar: 123
        }
    })
    grunt.registerTask('foo', ()=>{
    
    
        // grunt.config('foo') // 获取上方添加的配置
        grunt.config('foo.bar') // 这里可以通过 foo.bar 的形式拿到属性值
        console.log("grunt.config('foo.bar'): ", grunt.config('foo.bar'));
    })
}

再次运行 yarn grunt foo
在这里插入图片描述

Grunt 多目标任务

除了普通模式的任务以外,grunt 还支持一种叫做多目标模式的任务 ,可以理解成 子任务的概念,这种形式的任务在我们后续通过 grunt 去实现构建任务时是非常有用的。

多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数

module.exports = grunt => {
    
    
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', () => {
    
    
        console.log('build working ~')
    })
}

运行一下这个任务:yarn grunt build
在这里插入图片描述
收到一个警告,没有为 build 任务添加 targets ,这是因为我们在设置这种多目标任务时,我们需要为这种多目标任务去配置不同的目标,方式就是通过 grunt.initConfig 的方式:

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        // 这时候就必须叫 build,并且值必须是一个对象
        // 这里就相当于为 build 任务添加了两个 targets 一个叫 css 一个叫 js
        build: {
    
    
            css: '1',
            js: '2'
        }
    })
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', () => {
    
    
        console.log('build working ~')
    })
}

再次尝试运行 yarn grunt build
在这里插入图片描述
可以看到,它会运行两个子任务(这里叫做多目标),也就是 build 任务有两个目标,一个叫 css ,一个叫 js,运行 build 任务的同时,它会去执行这两个目标,相当于以两个子任务的形式去运行。

如果想去运行指定的目标的话,可以运行:yarn grunt build:css 指定运行 build:css 这个目标
在这里插入图片描述
这时候只会运行 css 这个目标。

在我们的任务函数当中,我们可以通过,this 去拿到当前执行这个目标的名称,还可以拿到这个目标对应的数据,注意,这里要用 this ,不能使用箭头函数:

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        // 这时候就必须叫 build,并且值必须是一个对象
        // 这里就相当于为 build 任务添加了两个 targets 一个叫 css 一个叫 js
        build: {
    
    
            css: '1',
            js: '2'
        }
    })
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', function (){
    
    
        console.log(`target: ${
      
      this.target}, data: ${
      
      this.data}`)
    })
}

我们尝试运行一下:yarn grunt build
在这里插入图片描述
可以看到,目标名称拿到了,对应的值也拿到了

需要注意的是:我们在 build 当中指定的每一个属性的键都会成为 build 的目标,除了 options 以外,在 options 里指定的东西呢会以我们指定的任务的配置选项出现。

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        // 这时候就必须叫 build,并且值必须是一个对象
        // 这里就相当于为 build 任务添加了两个 targets 一个叫 css 一个叫 js
        build: {
    
    
            options: {
    
    
                foo: 'bar'
            },
            css: '1',
            js: '2'
        }
    })
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', function (){
    
    
        console.log(`target: ${
      
      this.target}, data: ${
      
      this.data}`)
    })
}

执行 build :
在这里插入图片描述
可以看到并没有一个 target 叫 options ,因为这个 options 会作为任务的配置选项出现,我们在执行任务的过程当中就可以通过 this.options() 拿到这个任务的配置选项。

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        // 这时候就必须叫 build,并且值必须是一个对象
        // 这里就相当于为 build 任务添加了两个 targets 一个叫 css 一个叫 js
        build: {
    
    
            options: {
    
    
                foo: 'bar'
            },
            css: '1',
            js: '2'
        }
    })
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', function (){
    
    
        console.log(`this.options(): `, this.options()) // 拿到任务配置选项
        console.log(`target: ${
      
      this.target}, data: ${
      
      this.data}`)
    })
}

在这里插入图片描述
除了在任务当中加配置选项之外,在目标当中如果也是一个对象的话,也可以使用 options 选项,去覆盖掉任务当中的 options,

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        // 这时候就必须叫 build,并且值必须是一个对象
        // 这里就相当于为 build 任务添加了两个 targets 一个叫 css 一个叫 js
        build: {
    
    
            options: {
    
    
                foo: 'bar'
            },
            css: {
    
    
                options:{
    
    
                    foo: 'baz'
                }
            },
            js: '2'
        }
    })
    // 多目标模式任务需要通过 grunt 提供的 registerMultiTask 去注册定义,同样接收两个参数,任务名称和函数
    grunt.registerMultiTask('build', function (){
    
    
        console.log(`this.options(): `, this.options()) // 拿到任务选项
        console.log(`target: ${
      
      this.target}, data: ${
      
      this.data}`)
    })
}

运行:yarn grunt build
在这里插入图片描述
这就是 grunt 当中的多目标任务。

Grunt 插件的使用

了解了 Grunt 的基本特性之后,再来了解一下 grunt 当中插件的使用,插件机制是 grunt 的核心,它存在的原因也非常简单,因为很多构建任务都是通用的,例如你的项目当中需要去压缩代码,别人的项目当中也同样需要,所以社区当中就出现了很多预设的插件,这些插件内部都封装了一些通用的构建任务,一般情况下,我们的构建过程都是由这些通用的构建任务组成的。

这里看一下如果去使用插件提供的构建任务。

大体上就是先通过 NPM 安装插件,再通过 gruntfile 去载入这些构建任务,最后根据这些插件的文档去完成相关的配置选项。

这里用一个非常常见的插件来尝试一下 grunt-contrib-clean 用来自动清除项目开发过程中产生的一些临时文件:

yarn add grunt-contrib-clean

在这里插入图片描述
安装完成之后,在 gruntfile 中通过 grunt.loadNpmTasks 加载这个插件当中提供的一些任务:

绝大多数情况下,grunt 插件命名规范都是 grunt-contrib-taskname ,所以说这里的 clean 插件它提供的任务名称应该叫做 clean

module.exports = grunt => {
    
    
    grunt.loadNpmTasks('grunt-contrib-clean')
}

运行一下 clean 任务 yarn grunt clean
在这里插入图片描述
可以看到一个报错信息 clean 任务没有目标,可以发现 clean 实际上就是我们上边介绍的一种多目标任务。

这里为 clean 添加一个目标:
在这里插入图片描述

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        clean: {
    
    
            temp: 'temp/app.js'
        }
    })
    grunt.loadNpmTasks('grunt-contrib-clean')
}

回到命令行重新运行这个命令,查看结果 yarn grunt clean
在这里插入图片描述
在这里插入图片描述
temp/app.js 被删除了

还可以使用通配符的方式 *.txt:

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        clean: {
    
    
            temp: 'temp/*.txt'
        }
    })
    grunt.loadNpmTasks('grunt-contrib-clean')
}

在这里插入图片描述
再次运行:
在这里插入图片描述
在这里插入图片描述
除了 *.txt 这种方式,还可以使用 ** 这种方式,这种方式也是比较常见的,它代表的是 temp 下所有的子目录以及子目录下的文件:

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        clean: {
    
    
            temp: 'temp/**'
        }
    })
    grunt.loadNpmTasks('grunt-contrib-clean')
}

在这里插入图片描述
再次运行 yarn grunt clean :
在这里插入图片描述
在这里插入图片描述
包括 temp 文件夹都没了。

总结一下使用 grunt 插件

  1. 找到对应插件,安装到模块当中
  2. gruntfile 中通过 grunt.loadNpmTasks 把插件当中提供的任务加载进来
  3. 在 initConfig 中为这些任务添加配置选项

这样插件就可以正常工作了。

Grunt 常用插件及总结

这里看几个在 grunt 中非常常用的插件:
grunt-sass,需要注意的是 grunt 官方也提供了一个 sass 模块,但是那个模块需要本机安装 sass 环境,使用起来相对太不方便,这里的 grunt-sass 是一个 NPM 模块,在内部会通过 npm 的形式依赖 sass ,这样一来使用起来就不需要对我们的机器有任何的环境要求,可以直接使用。

grunt-sass 需要 sass 模块的支持,直接使用 sass 官方提供的 sass 模块就行,记得安装到开发依赖

yarn add grunt-sass sass --dev

在这里插入图片描述
安装成功之后我们就可以在 gruntfile 中去载入了

因为 grunt-sass 肯定是一个多目标任务,我们这里需要为 grunt-sass 通过 initConfig 的方式为它配置一些目标。
在这里插入图片描述
在这里插入图片描述
src/js/app.js:

$(($) => {
    
    
  const $body = $('html, body')

  $('#scroll_top').on('click', () => {
    
    
    $body.animate({
    
     scrollTop: 0 }, 600)
    return false
  })
})

src/scss/main.scss

$jumbotron-bg: #fff;
$jumbotron-padding: 3rem;

body {
    
    
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

.jumbotron {
    
    
  margin-bottom: 0;
  padding-top: $jumbotron-padding;
  padding-bottom: $jumbotron-padding;
  background-color: $jumbotron-bg;

  @media (min-width: 768px) {
    
    
    padding-top: 6rem;
    padding-bottom: 6rem;
  }

  .jumbotron-heading {
    
    
    font-weight: 300;
  }

  .container {
    
    
    max-width: 40rem;
  }

  p:last-child {
    
    
    margin-bottom: 0;
  }
}

.site-footer {
    
    
  padding-top: 3rem;
  padding-bottom: 3rem;
  font-family: monospace;

  a {
    
    
    text-decoration: none;
  }

  @keyframes beat {
    
    
    from,
    to {
    
    
      transform: none;
    }

    50% {
    
    
      transform: scale3d(1.4, 1.4, 1.4);
    }
  }

  .heart {
    
    
    display: inline-block;
    color: #f20;
    font-weight: normal;
    font-style: normal;
    animation: beat 0.4s ease-in-out infinite;
  }
}

module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        }
    })
    grunt.loadNpmTasks('grunt-sass')
}

尝试运行一下:yarn grunt sass
在这里插入图片描述
报了一个错误,说必须有实施方案传递给 sass 任务。也就是说在 grunt-sass 中使用哪个模块去编译没指定。

const sass = require('sass') // 刚刚安装的 sass 模块
module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            options: {
    
    
                implementation: sass // 将 sass 模块最为实施方案(implementation)
            },
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        }
    })
    grunt.loadNpmTasks('grunt-sass')
}

重新运行:
在这里插入图片描述
在这里插入图片描述
在项目根目录下多了一个 dist 目录,里边的 css 就是编译完成的 css 文件,它还有更多的选项,比如 sourceMap:

const sass = require('sass')
module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            options: {
    
    
                implementation: sass,
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        }
    })
    grunt.loadNpmTasks('grunt-sass')
}

再次运行:
在这里插入图片描述
在这里插入图片描述
编译之后就会自动生成对应的 sourceMap 文件,便于调试。更多的选项可以自行查看:
https://github.com/sindresorhus/grunt-sass

还有经常遇到的需求:编译 ES6 语法

编译器呢我们使用最多的就是 babel ,在 grunt 当中我们想使用 babel 的话,也可以使用 grunt-babel 的插件,grunt-babel 也需要 babel 的核心模块 @babel/core 以及 babel 的预设 @babel/preset-env

yarn add grunt-babel @babel/core @babel/preset-env --dev

在这里插入图片描述
安装完成过后我们就可以在项目中 使用 babel 了。

随着项目越来越复杂,loadNpmTasks 操作也就越来越多,这时候,社区当中有一个模块,它可以减少我们 loadNpmTasks 的使用。

我们可以先安装一个叫 load-grunt-tasks 的模块

yarn add load-grunt-tasks --dev

在这里插入图片描述
安装之后需要先导入一下再使用,有了 load-grunt-tasks 之后就不需要每次都去 loadNpmTasks 了:

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            options: {
    
    
                implementation: sass,
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        },
        babel: {
    
    
            options: {
    
     // 同样需要一个选项,设置 babel 在转换的时候的 preset
                presets: ['@babel/preset-env'] 
            },
            main: {
    
    
                files: {
    
    
                    'dist/js/app.js': 'src/js/app.js' // 同上
                }
            }
        }
    })
    // grunt.loadNpmTasks('grunt-sass')
    loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
}

我们去执行一下 babel 这个任务 yarn grunt babel
在这里插入图片描述
在这里插入图片描述
转换后:
在这里插入图片描述
转换前:
在这里插入图片描述
同样也支持 sourceMap 选项,同样的其他选项自行找文档查看:
在这里插入图片描述
还有一个需求就是当我们修改代码过后,需要自动的去编译,这时候我们需要另外一个插件叫:grunt-contrib-watch

yarn add grunt-contrib-watch --dev

在这里插入图片描述
我们这里的 loadGruntTasks 会自动的把它加载进来,代码中不需要自己手动加载,我们可以直接给 watch 添加配置选项就行。

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            options: {
    
    
                implementation: sass,
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        },
        babel: {
    
    
            options: {
    
     // 同样需要一个选项,设置 babel 在转换的时候的 preset
                presets: ['@babel/preset-env'],
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/js/app.js': 'src/js/app.js' // 同上
                }
            }
        },
        watch: {
    
    
            js: {
    
    
                files: ['src/js/*.js'], // 不需要输出任何的文件,只需要监听源文件就行了,所以直接是个数组
                tasks: ['babel'] // 如果这里边的文件发生变化之后需要执行什么任务,这里是当 js 发生改变之后需要执行 babel 任务
            },
            css: {
    
     // 同样的 css 也需要
                files: ['src/scss/*.scss'], // 不需要输出任何的文件,只需要监听源文件就行了,所以直接是个数组
                tasks: ['sass'] // 如果这里边的文件发生变化之后需要执行什么任务,这里是当 scss 发生改变之后需要执行 sass 任务
            },
        }
    })
    // grunt.loadNpmTasks('grunt-sass')
    loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务
}

这里运行 watch 任务 yarn grunt watch
在这里插入图片描述
watch 任务执行之后它并不会直接执行 babel 和 sass 任务,它只是监视文件,一旦当文件发生变化之后才会去执行对应的任务,比如我们这里修改一下 js 文件里边的内容保存之后,你会发现 babel 任务会自动被执行:
在这里插入图片描述
也就是说我们在 watch 一开始的时候并不执行 babel 和 sass ,所以我们一般会给 watch 做一个映射,使用 default 任务:

const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')
module.exports = grunt => {
    
    
    grunt.initConfig({
    
    
        sass: {
    
    
            options: {
    
    
                implementation: sass,
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/css/main.css': 'src/scss/main.scss' // 键 => 输出路径 值 => 输入路径
                }
            }
        },
        babel: {
    
    
            options: {
    
     // 同样需要一个选项,设置 babel 在转换的时候的 preset
                presets: ['@babel/preset-env'],
                sourceMap: true
            },
            main: {
    
    
                files: {
    
    
                    'dist/js/app.js': 'src/js/app.js' // 同上
                }
            }
        },
        watch: {
    
    
            js: {
    
    
                files: ['src/js/*.js'], // 不需要输出任何的文件,只需要监听源文件就行了,所以直接是个数组
                tasks: ['babel'] // 如果这里边的文件发生变化之后需要执行什么任务,这里是当 js 发生改变之后需要执行 babel 任务
            },
            css: {
    
     // 同样的 css 也需要
                files: ['src/scss/*.scss'], // 不需要输出任何的文件,只需要监听源文件就行了,所以直接是个数组
                tasks: ['sass'] // 如果这里边的文件发生变化之后需要执行什么任务,这里是当 scss 发生改变之后需要执行 sass 任务
            }
        }
    })
    // grunt.loadNpmTasks('grunt-sass')
    loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务

    grunt.registerTask('default', ['babel', 'sass', 'watch']) // 做一个映射,让它先执行 babel 和 sass 再执行 watch 
}

运行 yarn grunt 或者 yarn grunt default
在这里插入图片描述
此时它会先做一次编译操作,然后再做监听,这时候再有文件变化的话他会去监听做编译操作。

这里介绍的三个小插件算是我们如果使用 grunt 最常用的三个插件,除此之外还有很多其他的插件,这里就不多做介绍了。

原因很简单,因为 grunt 基本上已经退出 “历史舞台” 了。

这里介绍 grunt 的原因是因为它算是最早的鼻祖,所以先介绍一下它,另外就是介绍它为后面的 gulp 做铺垫。

至于它其他的插件,可以上官网上自行查找。【查看

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(一、工程化概述)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(二、脚手架工具)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Grunt)

【学习笔记】Part2·前端工程化实战–开发脚手架及封装自动化构建工作流(三、自动化构建 – 主Gulp)

猜你喜欢

转载自blog.csdn.net/qq_38652871/article/details/109597085