自动化构建:一切重复工作本应自动化

构建工具

构建工具的使用就是为了解决自动化的问题,它可以将我们在开发阶段的代码自动构建为生产部署所需的代码。常用的构建工具有grunt, gulp, fis等,webpack本质为模块打包工具。

基本流程

  1. 首先我们需要有 package.json,它是 npm 依赖管理体系下的基础配置文件。
  2. 然后选择使用 npm 或 Yarn 作为包管理器,这会在项目里添加上对应的 lock 文件,来确保在不同环境下部署项目时的依赖稳定性。
  3. 确定项目技术栈,团队习惯的技术框架是哪种?使用哪一种数据流模块?是否使用 TypeScript?使用哪种 CSS 预处理器?等等。在明确选择后安装相关依赖包并在 src 目录中建立入口源码文件。
  4. 选择构建工具,目前来说,构建工具的主流选择还是 webpack (除非项目已先锋性地考虑尝试 nobundle 方案),对应项目里就需要增加相关的 webpack 配置文件,可以考虑针对开发/生产环境使用不同配置文件。
  5. 打通构建流程,通过安装与配置各种 Loader 、插件和其他配置项,来确保开发和生产环境能正常构建代码和预览效果。
  6. 优化构建流程,针对开发/生产环境的不同特点进行各自优化。例如,开发环境更关注构建效率和调试体验,而生产环境更关注访问性能等。
  7. 选择和调试辅助工具,例如代码检查工具和单元测试工具,安装相应依赖并调试配置文件。
  8. 最后是收尾工作,检查各主要环节的脚本是否工作正常,编写说明文档 README.md,将不需要纳入版本管理的文件目录记入 .gitignore 等。
    1. package.json 1) npm 项目文件
    2. package-lock.json 2) npm 依赖 lock 文件
    3. public/ 3) 预设的静态目录
    4. src/ 3) 源代码目录
    5. main.ts 3) 源代码中的初始入口文件
    6. router.ts 3) 源代码中的路由文件
    7. store/ 3) 源代码中的数据流模块目录
    8. webpack/ 4) webpack 配置目录
    9. common.config.js 4) webpack 通用配置文件
    10. dev.config.js 4) webpack 开发环境配置文件
    11. prod.config.js 4) webpack 生产环境配置文件
    12. .browserlistrc 5) 浏览器兼容描述 browserlist 配置文件
    13. babel.config.js 5) ES 转换工具 babel 配置文件
    14. tsconfig.json 5) TypeScript 配置文件
    15. postcss.config.js 5) CSS 后处理工具 postcss 配置文件
    16. .eslintrc 7) 代码检查工具 eslint 配置文件
    17. jest.config.js 7) 单元测试工具 jest 配置文件
    18. .gitignore 8) Git 忽略配置文件
    19. README.md 8) 默认文档文件

Grunt

基本使用

1. 在项目中安装grunt

yarn add grunt -D or npm install grunt --save-dev

2. 在项目根目录创建gruntfile.js文件

Gruntfile可以定义为Gruntfile.jsGruntfile.coffee,用来配置或定义任务(task)并加载Grunt插件的。

3. 编写简单的gruntfile配置

Gruntfile中导出一个函数,其接收一个参数grunt,通过参数grunt可以调用grunt暴露的api。通过grunt.registerTask()方法(grunt.registerTask()grunt.task.registerTask()的别名),可以创建一个任务,registerTask方法有三种用法:

  1. 第一个参数为任务名称,第二个参数为回调函数,执行具体的任务内容
  2. 第一个参数为任务名称,第二个参数为任务说明,第三个参数为回调函数,执行具体的任务内容
  3. 第一个参数为任务名称,第二个参数为数组,串行执行多个任务
  1. module.exports = grunt => {
  2. grunt.registerTask('foo', () => {
  3. console.log('hello grunt');
  4. });
  5. grunt.registerTask('bar', '任务描述', () => {
  6. console.log('other task');
  7. });
  8. grunt.registerTask('baz', ['foo', 'bar'])
  9. }

通过yarn grunt taskNamenpx grunt taskName可以执行具体的任务,通过yarn grunt --helpnpx grunt --help可查看帮助信息,其中包含在gruntfile中定义的任务。

grunt中的任务

上述已经简单介绍了自定义任务的方法,这里再具体进行介绍。

1. 默认任务

指定任务名称为default可以设置默认任务,通过yarn gruntornpx grunt不加任务名可以直接执行默认任务。

  1. grunt.registerTask('default', '任务描述', () => {
  2. console.log('default task');
  3. });

默认任务的通常用法为传入一个数组

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

2. 异步任务

grunt任务默认支持同步模式,如果需要执行异步任务,需要使用this.async()及其返回值进行异步模式任务的创建

例:this.async()返回一个函数,在任务结束时,可以调用这个函数结束任务

  1. grunt.registerTask('async-task', function() {
  2. const done = this.async();
  3. setTimeout(() => {
  4. console.log('async task working~');
  5. done();
  6. })
  7. })

注意: 在异步任务中registerTask里传入的函数不能是箭头函数,因为这里需要使用this.async(),而箭头函数中没有this的概念

3. 失败的任务

对于普通的任务,可以通过返回false进行标识任务失败了,对于异步任务,需要为done(false)传入fasle代表任务失败

  1. // 普通的失败任务
  2. grunt.registerTask('foo', () => {
  3. console.log('hello grunt');
  4. return fasle
  5. });
  6. // 异步失败任务
  7. grunt.registerTask('async-task', function() {
  8. const done = this.async();
  9. setTimeout(() => {
  10. console.log('async task working~');
  11. done(false);
  12. })
  13. })

如果串联执行多个任务,其中有任务失败后grunt就会停止执行后续任务,可以通过yarn grunt taskName --force的形式指定强制执行后续的任务。

4. 任务配置选项

grunt中提供了一个initConfig的方法为为当前项目初始化一个配置对象。grunt.initConfig()grunt.config.init()方法的别名。深入了解配置相关api

其参数为一个字对象,对象的key一般与任务名保持一致。可以通过grunt.config(key)获取到具体的配置信息,其中key可以使用key1.key2的形式获取深层的配置信息

  1. grunt.initConfig({
  2. greeting: {
  3. hello: 'hello world',
  4. }
  5. });
  6. grunt.registerTask('greet', () => {
  7. console.log('config: ', grunt.config('greeting'))
  8. console.log('config: ', grunt.config('greeting.hello'))
  9. })

以上代码运行yarn grunt greet后输出

  1. Running "greet" task
  2. config: { hello: 'hello world' }
  3. config: hello world
  4. Done.
  5. Done in 0.84s.

5. 多目标模式任务

通过grunt.registerMultiTask()方法可以创建一个多目标任务,也叫复合任务(grunt.registerMultiTask()grunt.task.registerMultiTask()的别名)

多目标任务必须配合initConfig配置对应的目标

例:initConfig参数对象中的key值需要与对应的任务名相同,该key对应的值也必须为一个对象,除了options选项之外,其中一个key就代表一个目标。

  1. grunt.initConfig({
  2. build: {
  3. js: {
  4. options: {
  5. hi: 'hi',
  6. },
  7. js: 'js'
  8. },
  9. css: 'css',
  10. options: {
  11. hello: 'hello'
  12. }
  13. }
  14. });
  15. // 多目标模式任务
  16. grunt.registerMultiTask('build', function() {
  17. console.log('build task')
  18. console.log('target: ', this.target, 'data: ', this.data, 'options: ', this.options())
  19. })

在多目标任务中,我们可以通过this.target获取到当前的任务目标,通过this.data获取到目标对应的配置值,通过this.options()方法可以获取到配置选项,这里的配置选项如果没有在目标中配置,则会取值为build任务下配置的options选项。

运行yarn grunt build输出如下:

  1. Running "build:js" (build) task
  2. build task
  3. target: js data: { options: { hi: 'hi' }, js: 'js' } options: { hello: 'hello', hi: 'hi' }
  4. Running "build:css" (build) task
  5. build task
  6. target: css data: css options: { hello: 'hello' }
  7. Done.
  8. Done in 0.54s.

通过yarn grunt multiTask:target的方式还可以单独执行某人任务目标,例如yarn grunt build:js 输出如下

  1. Running "build:js" (build) task
  2. build task
  3. target: js data: { options: { hi: 'hi' }, js: 'js' } options: { hello: 'hello', hi: 'hi' }
  4. Done.
  5. Done in 1.97s.

grunt插件

grunt拥有很多插件,插件的命名一般为grunt-contrib-pluginName,且插件一般都是多目标任务,通过grunt.initConfig()方法可以为插件配置需要的配置项。

插件的使用通过grunt.loadNpmTasks(pluginName)方法进行加载,grunt.loadNpmTasks(pluginName)grunt.task.loadNpmTasks(pluginName)的别名。如果安装的是本地的插件则可使用grunt.loadTasks(pluginName)grunt.task.loadTasks(pluginName)

插件的使用示例:

1. 安装插件(以grunt-contrib-clean为例)

  1. yarn add grunt-contrib-clean -D

or

  1. npm install grunt-contrib-clean -D

2. 加载插件

  1. grunt.loadNpmTasks('grunt-contrib-clean');

3. 按照插件文档在initConfig()中为插件添加配置项

  1. grunt.initConfig({
  2. clean: {
  3. temp: 'temp/app.js'
  4. }
  5. })
  6. grunt.loadNpmTasks('grunt-contrib-clean');

通过yarn grunt clean即可执行任务,改操作会根据我们的配置删除掉temp/app.js文件

grunt常用插件与搜索,点击可以查看grunt官方的插件列表,里面有一些常用的grunt插件,点击到具体的插件中也会有具体的插件使用方式。

load-grunt-task解决多插件带来的多次引入插件问题

如果我们的项目需要加载多个插件,则要多次进行loadNpmTasks的调用load-grunt-task包解决了这个问题,通过yarn add load-grunt-task,使用方式为

  1. const loadGruntTasks = require('load-grunt-task')
  2. moudle.exports = grunt => {
  3. ...
  4. loadGruntTasks(grunt);
  5. }

接下来就只需在initConfig中进行各种插件的配置就好了,但是相应的插件还是需要通过npm下载的,只是可以不在gruntfile中显式加载。

Gulp

Gulp是目前最为流行的构建工具之一,使用简单且高效。gulp中文官网

gulp的工作原理

gulp官方介绍gulp是基于流(stream)的自动化构建工具,也正是基于流,gulp 在构建过程中并不把文件立即写入磁盘,从而提高了构建速度。

gulp基于流的构建流程为:

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

在中间转换流中,可以对文件进行不同的操作,最后通过写入流将文件写入目标位置即可完成构建。

gulp的基本使用

gulp之所以流行,原因之一也是它使用简单,api也非常少,这里就来介绍一下gulp的使用方式。

1. 安装gulp

  1. yarn add gulp -D
  2. or
  3. npm install gulp --save-dev

2. 在项目根目录创建gulpfile文件

gulpfile文件为一个gulpfile.js的js文件

例:

  1. exports.default = defaultTask(done) {
  2. console.log('this is default gulp task')
  3. done();
  4. }

以上通过exports.defaults导出了一个默认的gulp任务,运行yan gulp则会运行默认的任务。

gulp中的任务

以上介绍了gulp中默认任务的定义方式,下面介绍其他类型的任务定义

1. 普通的自定义任务

gulp中的任务通过以下exports.taskName的方式进行定义

  1. exports.taskName = function(done){
  2. // 任务具体内容
  3. done()
  4. }

最新的gulp中取消了同步任务,因此在定义普通的任务时,参数中接收一个done回调,在任务结束后需要执行done(),否则会报错。

gulp之前的任务定义方式目前仍然被保留着,通过taskapi仍然可已定义任务(不推荐)

例:

  1. const { task } = require('gulp');
  2. function build(done) {
  3. // 任务具体内容
  4. done();
  5. }
  6. task(build);

通过yarn gulp taskNamenpx gulp taskName的方式即可运行自定义的任务

2. 组合任务

gulp中提供了两个api用来创建组合任务,分别为seriesparallel

  • series():将任务函数和/或组合操作组合成更大的操作,这些操作将按顺序依次执行。
  • parallel():将任务功能和/或组合操作组合成同时执行的较大操作。

对于使用 series()parallel() 进行嵌套组合的深度没有强制限制。

例:

  1. // 串行任务示例
  2. const { series } = require('gulp');
  3. function foo(done) {
  4. console.log('foo task');
  5. done()
  6. }
  7. function bar(done) {
  8. console.log('foo task');
  9. done()
  10. }
  11. exports.build = series(foo, bar);

执行yarn gulp build,输出

  1. [18:49:35] Starting 'build'...
  2. [18:49:35] Starting 'foo'...
  3. foo task
  4. [18:49:35] Finished 'foo' after 1.08 ms
  5. [18:49:35] Starting 'bar'...
  6. foo task
  7. [18:49:35] Finished 'bar' after 436 μs
  8. [18:49:35] Finished 'build' after 3.49 ms
  9. Done in 1.93s.
  1. // 并行任务示例
  2. const { parallel } = require('gulp');
  3. function foo(done) {
  4. console.log('foo task');
  5. done()
  6. }
  7. function bar(done) {
  8. console.log('foo task');
  9. done()
  10. }
  11. exports.build = parallel(foo, bar);

执行yarn gulp build,输出

  1. [18:51:35] Starting 'build'...
  2. [18:51:35] Starting 'foo'...
  3. [18:51:35] Starting 'bar'...
  4. foo task
  5. [18:51:35] Finished 'foo' after 830 μs
  6. foo task
  7. [18:51:35] Finished 'bar' after 1.14 ms
  8. [18:51:35] Finished 'build' after 2.51 ms
  9. Done in 0.99s.

通过观察执行时间,也可以发现并行任务时间是比串行短的。在多个任务独立的情况下可以使用并行任务组合,如需顺序执行则需要使用串行任务进行组合。

3. 异步任务

gulp中的任务都是异步任务。异步任务带来的问题就是何时确定异步任务执行完毕。

  • 回调形式的异步任务:
    第一种为我们以上介绍的通过传参传入一个done回调函数,在运行done()之后确定任务执行结束。

    1. function foo(done) {
    2. console.log('foo task');
    3. done()
    4. }
    5. module.exports = {
    6. foo
    7. }
  • 由于done也是错误优先的回调,因此任务失败则需向done回调中传入一个错误即可

    1. function foo(done) {
    2. console.log('foo task');
    3. done(new Error('task failed'))
    4. }
    5. module.exports = {
    6. foo
    7. }
  • 如果一个任务失败,那么后续任务将不会继续执行。

  • promise类型的异步任务:
    第二种方式为promise类型的异步任务,函数通过返回promise,如果promise状态为resolve则任务成功,如果为reject则失败

    1. // 成功任务
    2. const promise_success_task = () => {
    3. return Promise.resolve()
    4. }
    5. // 失败任务
    6. const promise_failed_task = () => {
    7. return Promise.reject(new Error('task failed'))
    8. }
    9. module.exports = {
    10. promise_success_task
    11. promise_failed_task
    12. }
  • 通过async/await语法糖的方式也是一样的。

  • 返回 stream的方式
    除了上述方式,gulp还支持其他的异步任务,详情见异步执行文档,其中第一个介绍的就是stream,这也是在gulp中最常用的一种方式,因为gulp中处理文件都是通过流进行处理的。
    例:gulp监听了流的结束事件,我们也可以通过手动执行done回调进行处理 ```javascript // 直接返回stream exports.stream = () => { const readStream = fs.createReadStream(‘./package.json’); const writeStream = fs.createWriteStream(‘temp.txt’); readStream.pipe(writeStream); return readStream; // readStream完成后会触发end事件,gulp通过end事件就可以得知任务处理完成 }

// 自己模拟gulp处理stream的操作 exports.stream1 = done => { const readStream = fs.createReadStream(‘./package.json’); const writeStream = fs.createWriteStream(‘temp.txt’); readStream.pipe(writeStream); readStream.on(‘end’, () => { done(); }) } ```