请选择 进入手机版 | 继续访问电脑版
点击联系客服
客服QQ:509006671 客服微信:mengfeiseo

广州老站长门户

 找回密码
 立即注册
查看: 99|回复: 50

可以学习封装自己的前端自动化构建工作流(gulp)

[复制链接]

1

主题

1

帖子

-7

积分

限制会员

积分
-7
发表于 2021-4-13 15:48:33 | 显示全部楼层 |阅读模式
说到前端自动化构建,我相信做前端的小伙伴不会成为陌生人,也许第一感觉是web  pack。但是实际上,web包本质上应该是强大的模块包。从门户文件开始,合并文件之间的各种参考关系,将各种复杂的文件打包为一个或多个浏览器可以识别的文件。因此,与自动化构建工具相比,webpack是一个模块包。今天,我们将介绍强大的自动化构建工具gulp。

什么是自动化构建

对自动化构建的简单理解是将源代码转换为生产环境代码的过程。

由于它的出现,消除了我们许多手工的重复性工作,并在一定程度上提高了我们的开发效率。

常用的自动化构建工具

GruntGulpFIS区别

Grunt和gulp本身更像一个构建平台,要完成实际构建,需要使用各种插件来执行具体的构建任务。因此,gurnt和gulp实际上可以相互转换。也就是说,可以用grunt做什么,可以用gulp做什么,可以用gulp做什么,可以用grunt做什么。Grunt作业是基于临时文件构建的。也就是说,grunt解析文件时,首先读取文件,然后在插件处理后先写入临时文件,然后在其他插件执行后续处理时读取此临时文件的内容,在插件处理后写入其他临时文件,从而表明grunt的每一步操作构建都涉及磁盘读取和写入。因此,部署速度可能会减慢。因此,目前使用的人也以基于内存的方式构建gulp操作。换句话说,gulp将文件解析为文件流,首先读取文件的文件流,将其写入内存,经过中间插件处理后写入目标文件(生产代码)。因此,gulp仅设计为第一阶段和最后阶段的磁盘读取和写入,其他中间部分在内存中执行,因此构建速度非常快。因此,gulp是目前最主流的自动化建设工具FIS百度团队推出的自动化建设工具,规模大,完整,集成了很多功能,很容易上手。但是现在没怎么维护,用的人也很少初识Gulp

gulp工作原理





上图是对gulp工作原理的一个很好的解释。Gulp的主要工作原理是读取文件,中间经过一系列处理,转换为生产环境所需的内容,然后写入目标文件。

这个过程中最重要的是gulp的管道pipe()。gulp使用pipe()实现到下一个过程的转换。详细信息请查看代码
de class="prism language-javascript">const fs = require('fs')
const stream = (done) => {
  const readStream = fs.createReadStream('package.json') // 读取流,读取文件
  const writeStream = fs.createWriteStream('temp.txt') // 写入流,写入文件
  const transform = new Transform({
    transform: (chunk, encoding, callback) => {
      // 这里可以对读取的流进行各种转换操作,具体如何转换我就不写了
    }
  }) // 转换流
  return readStream // 读取
    .pipe(transform) // 转换
    .pipe(writeStream) // 写入
    // return 读取流 实际会调用readStream的end事件,告知结束任务
}
module.exports = {
  stream
}

如上,gulp核心工作原理就是这样,通过pipe这样一个管道将上一步处理完的东西传递给下一步进行处理。全部处理完成后,最终写入目标文件

gulp需要有一个gulpfile.js文件,实现这些构建任务的代码一般就写在这个gulpfile.js文件中,如以上代码就是写在gulpfile.js中的

但是,以上代码我们是通过node.js原生实现的,实际读取文件,写入文件以及中间对文件进行各种处理,gulp都给我们提供了各种插件以及方法,我们都可以直接安装或者直接使用

gulp常用Api
const { src, dest, parallel, series, watch } = require('gulp')

  • src:创建读取流,可直接src(‘源文件路径’) 来读取文件流
  • dest:创建写入流,可直接dest(‘目标文件路径’) 来将文件流写入目标文件中
  • parallel:创建一个并行的构建任务,可并行执行多个构建任务 parallel(‘任务1’,‘任务2’,‘任务3’,…)
  • series:创建一个串行的构建任务,会按照顺序依次执行多个任务 series(‘任务1’,‘任务2’,‘任务3’,…)
  • watch:对文件进行监视,当文件发生变化时,可执行相关任务
    watch(‘src/assets/styles/*.scss’, 执行某个任务)

    从0到1实现一个完整的自动化工作流
    下面我们利用一个例子来从0到1实现一个完整的自动化工作流

    首先,我们得准备一份开发时得源代码


    代码目录大家可以通过脚手架去生成

    目录介绍
    1、public下存放不需要经过转换得静态资源
    2、src下存放项目源文件



    3、assets下存放其他资源文件,如,样式文件,脚本文件,图片,字体等


    下面,我们要利用gulp来实现一个自动化构建工作流,将这些文件都能够自动转化为生产环境可用得资源文件

    目标
    1、将html文件转化为html文件,存放到dist下,并且处理html中得一些模板解析,以及资源文件得引入问题(如html文件中引入了css,js 等)。并对html文件进行压缩处理

    2、将scss文件转化为浏览器可识别得css文件,并压缩

    3、将js文件转化为js文件,并处理js代码中一些浏览器无法识别得语法转化为可识别得。如ES6.ES7转ES5

    4、将图片进行压缩

    5、将字体进行压缩

    6、实现一个开发服务器,实现边开发,边构建

    7、相关优化

    8、封装自动化工作流,将我们完成得gulpfile.js 封装成一个公用模块,便于后续其他类似项目可以直接按照这个模块就可立即使用

    开始实现
    准备工作
    按照gulp,并引入相关api
    yarn add gulp --dev

    在项目根目录下创建gulpfile.js文件,在文件中引入gulp相关方法

    const { src, dest, parallel, series } = require('gulp')

    1、创建相关得构建任务,并测试
    创建样式编译任务

    // 定义样式编译任务
    const sass = require('gulp-sass') // 编译scss文件得
    const scss = () => {
      return src('./src/assets/styles/main.scss', {base: 'src'}) // 读取文件
        .pipe(sass()) // sass编译处理
        .pipe(dest('./dist')) // 写入到dist文件夹下
    }
    // 导出相关任务
    module.exports = {
      scss
    }

    以上src方法中第二个参数 是为了指定基础路径。如果不指定,打包后则会丢失路径,直接将打包后的css文件放在dist目录下。
    如果指定了,就会将指定的目录后面的目录都保留下来,即 assets/styles/main.css

    运行yarn gulp scss 运行构建任务



    其他构建任务也都一样创建
    思路:
    先建立不同类型文件的编译构建任务,将需要编译的各个任务进行编译构建,并一个个进行测试,确保构建没问题
    当然,编译不同文件需要用到不同的插件。故同时需要安装相应的插件,并引入相关插件(引入的代码我就不贴了)

  • 编译scss 需要gulp-scss插件 (任务scss)
  • 编译脚本 需要gulp-babel插件,同时需要安装@babel/core,gulp-babel的作用主要就是去调用@babel/core插件,
    同时为了能够转换ES6及以上新特性代码,还需要安装@babel/preset-env插件,用于转换新特性 (任务script)
  • 编译html 需要gulp-swig插件,用于传入模板所需要的数据 (任务html)
  • 编译image图片以及font字体文件,需要 gulp-imagemin插件,用于对图片和字体进行压缩 (任务image和font)
  • 建立其他不需要编译的文件的构建任务,不需要编译的就直接拷贝到目标路径中 (任务copy)
    附上以上6个任务代码

    // html模板中需要的数据
    const data = {
      menus: [
        {
          name: 'Home',
          icon: 'aperture',
          link: 'index.html'
        },
        {
          name: 'About',
          link: 'about.html'
        },
        {
          name: 'Contact',
          link: '#',
          children: [
            {
              name: 'Twitter',
              link: 'https://twitter.com/w_zce'
            },
            {
              name: 'About',
              link: 'https://weibo.com/zceme'
            },
            {
              name: 'divider'
            },
            {
              name: 'About',
              link: 'https://github.com/zce'
            }
          ]
        }
      ],
      pkg: require('./package.json'),
      date: new Date()
    }
    // 定义样式编译任务
    const scss = () => {
      return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(sass())
        .pipe(dest('./dist'))
    }
    // 定义脚本编译任务
    const script = () => {
      return src('./src/assets/scripts/*.js', { base: 'src'})
        .pipe(babel({ presets: ['@babel/preset-env'] })) // 指定babel去解析ECMAScript新特性代码
        .pipe(dest('./dist'))
    }
    // 定义html模板编译任务
    const html = () => {
      return src('./src/**/*.html', { base: 'src' })
        .pipe(swig({ data })) // 指定html模板中的数据
        .pipe(dest('./dist'))
    }
    // 定义图片编译任务
    const image = () => {
      return src('./src/assets/images/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('./dist'))
    }
    // 定义字体编译任务
    const font = () => {
      return src('./src/assets/fonts/**', { base: 'src' })
        .pipe(imagemin())
        .pipe(dest('./dist'))
    }
    // 定义其他不需要经过编译的任务
    const copy = () => {
      return src('./public/**', { base: 'public' })
        .pipe(dest('./dist'))
    }
    module.exports = { scss, script, html, image, font, copy }

    然后运行yarn gulp 任务名 来运行构建任务进行测试

    这里说明下,html任务中传入的data,因为html源文件中用到了模板引擎,里面用到了相关数据,故我们解析时,需要传入相关的数据



    2、合并任务
    因以上6个任务在构建过程中户不影响,故可以进行并行构建,故此时,我们可以利用gulp提供的parallel方法来新建一个并行任务
    但在建立任务之前,我们可以把任务进行分类,前面5个为都需要进行编译的任务,我们可以先合并为一个compile任务。然后再用这个compile任务
    和copy任务并行合并为一个新的任务build

    // 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
    const compile = parallel(scss, script, html, image, font)
    // 将需要编译的任务和不需要进行编译的任务合并为一个构建任务
    const build = parallel(compile, copy)

    下面我们测试一下
    运行 yarn gulp build




    可以看到,相关任务,就都被打包了

    3、任务初步优化
    1、 每次构建时,都会把构建后的文件写入到dist目录下,那么我们是不是要在每次写入dist之前,将dist目前清空一下会比较好啊,可以防止多余无用代码的出现
    怎么做:新增del模块,可以用于帮我们删除指定目录下的文件(yarn add del --dev)

    const del = require('del')
    // 定义清除目录下的文件任务
    const clean = () => {
      return del(['dist'])
    }

    此时,我们需要将新增的这个clean任务加入到构建流程中,此时,我们要想,我们是不是希望在其他任务将文件写入dist之前去清除dist目录下的文件啊
    那么,此时,clean任务是不是就得在其他构建任务之前去执行啊。所以此时,我们需要将原来得build任务,串行加上一个clean任务

    // 合并构建任务
    const build = series(clean, parallel(compile, copy))

    2、我们之前安装了很多gulp插件(gulp-开头得插件),每次我们新安装一个,就得引入一次,如果以后插件多了,是不是就会有很多插件得引用啊,此时我们可以借助gulp得另一个插件来解决这个问题gulp-load-plugins, 此插件会帮我们加载gulp下得所有插件,故我们只需要引入这个插件后,就可以直接通过这个插件,拿到gulp下得所有插件,下面,我们来修改一下代码,前面插件得引入,我们就不需要了

    const loadPlugins = require('gulp-load-plugins')
    const plugins = loadPlugins()
      // 定义样式编译任务
      const scss = () => {
        return src('./src/assets/styles/*.scss', { base: 'src' })
          .pipe(plugins.sass())
          .pipe(dest('./dist'))
      }
      // 定义脚本编译任务
      const script = () => {
        return src('./src/assets/scripts/*.js', { base: 'src'})
          .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
          .pipe(dest('./dist'))
      }
      // 定义html模板编译任务
      const html = () => {
        return src('./src/**/*.html', { base: 'src' })
          .pipe(plugins.swig({ data }))
          .pipe(dest('./dist'))
      }
      // 定义图片编译任务
      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'))
      }

    4、起一个开发服务器
    下面,我们开始起一个开发服务器,完成开发时边开发边构建的功能
    起一个开发服务器需要用到插件browser-sync
    安装browser-sync插件 yarn add browser-sync
    引入插,并创建一个开发服务器

    const browserSync = require('browser-sync')
    // 创建一个开发服务器
    const bs = browserSync.create()
    const serve = () => {
      bs.init({
        notify: false, // 关闭页面打开时browser-sync的页面提示
        port: 2080, // 设置端口
        server: {
          baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
          routes: {
            '/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
          }
        }
      })
    }

    上面说一下routes选项
    主要是指定打包后,html文件中直接引入的node_modules下的包文件的问题,告知开发服务器直接去根目录下的node_modules文件夹下面找对应的文件



    此时,我们开发服务器已经起了一个了,并告知了服务器去取dist下的文件作为运行文件。但是此时,还会有问题,那就是,如果dist下的文件发生了变化后,我们的开发服务器是无法得知的,此时我们需要配置一个files属性,来对dist下的文件进行监视。

    const serve = () => {
      bs.init({
        notify: false, // 关闭页面打开时browser-sync的页面提示
        port: 2080, // 设置端口
        files: 'dist/**', // 监听dist下所有文件
        server: {
          baseDir: 'dist', // 设置开发服务器的根目录,会取此目录下的文件运行
          routes: {
            '/node_modules': 'node_modules' // 解决dist后的文件直接引入node_modules下文件的问题
          }
        }
      })
    }

    此时,我们已经可以监听dist下的文件了。

    5、开发服务器优化
    虽然我们现在能对dist下的文件进行监视了,但是,依然是无法实现开发过程中,页面能即时响应的目的的。因为我们开发过程中修改的是源代码,而不是dist下的代码。那如何实现呢。继续往下看

    5.1 监听构建前的源文件,保证开发过程中能够实现修改代码后,页面立刻得到相应
    实现方式:利用gulp自带的watch模块对src下的源文件进行监听,源文件发生变化时,重新执行对应的构建任务,那么会重新构建,构建后,dist下的文件就会发生变化,serve通过files属性就能监听到

    const serve = () => {
      // watch监听相关源文件
      watch('src/assets/styles/*.scss', scss)
      watch('src/assets/scripts/*.js', script)
      watch('src/*.html', html)
      watch('src/assets/images/**', image)
      watch('src/assets/fonts/**', font)
      watch('public/**', copy)
      bs.init({
        notify: false,
        port: 2080,
        files: 'dist/**',
        server: {
          baseDir: 'dist',
          routes: {
            '/node_modules': 'node_modules'
          }
        }
      })
    }

    5.2 进一步优化,上面我们已经实现了开发过程中,修改文件页面能即时响应。但是我们上面6个watch监听了6类文件,每类文件发生变化后,我们都重新执行了对应的构建任务。
    我们试想,在开发过程中,我们只需要当文件发生变化时,页面能即时响应就行了,像html,scss,js等文件,需要编译成浏览器可识别的文件我们才能看到页面发生变化,故每次这类文件发生变化时,我们都去启动对应的任务重新构建一次这无可厚非。但是,像图片,字体以及不需要编译的静态文件。我们只需要看到变化就行了,有必要调用对应构建任务吗,像图片,字体,都是对它们进行了压缩,但我们实际开发阶段,这个完全没必要。
    故,我们对这类开发阶段不需要处理的文件做个特殊处理。

    5.2.1 我们在监听图片,字体,和public下的静态文件时,不再启动对应的构建任务,而是直接调用browserSync的reload()方法去重新加载页面
    那么此时,我们开发服务器要拿到这些文件是不是就不能在dist下拿了啊,因为我们没有重新构建,故dist下不会有改变后的文件。
    此时,我们修改baseDir的根目录为一个数组[‘dist’, ‘src’, ‘public’]。那么,服务器会优先去dist下找文件,如果找不到,会依次去src和public目录下寻找。像图片,字体,以及相关静态文件,开发服务器是不是就会去src和public下去加载啊

    const serve = () => {
      // watch监听相关源文件
      watch('src/assets/styles/*.scss', scss)
      watch('src/assets/scripts/*.js', script)
      watch('src/**/*.html', html)
      // watch('src/assets/images/**', image)
      // watch('src/assets/fonts/**', font)
      // watch('public/**', copy)
      watch(
        [
          'src/assets/images/**',
          'src/assets/fonts/**',
          'public/**'
        ],
        bs.reload
      )
      bs.init({
        notify: false,
        port: 2080,
        files: 'dist/**',
        server: {
          baseDir: ['dist', 'src', 'public'],
          routes: {
            '/node_modules': 'node_modules'
          }
        }
      })
    }

    5.3 有一个容易忽略的问题,我们上面serve服务器是以dist下的文件为跟目录,也就是服务器启动,会默认去取dist目录下的文件,如果找不到,就会去取src和public下的文件。那如果重来没有执行过build命令,那么dist下是不是空的啊,这么一来,像样式文件,js文件,html文件,他都会取src下面找,那找到的文件能运行吗,是不是不能啊。所以,我们需要新建一个develop任务,此任务在启动serve前,先执行一次compile任务。

    // 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
    const compile = parallel(scss, script, html)
    // 合并构建任务
    const build = series(clean, parallel(compile, copy, image, font))
    // 开发构建任务
    const develop = series(compile, serve)

    我们新建了一个develop任务,让起串行先执行compile和serve
    同时,我们修改了一下compile任务,将image和font任务放入到build中了,这样我们develop中便不需要执行这两个任务了

    5.4 上面我们说过,serve服务器是通过files属性去监听dist目录下的文件变化来实现即时更新的。可是像上面的图片,字体以及静态文件,我们好像并没有用到这个files属性,也实现了浏览器的实时更新吧。那我们其他文件,是不是也可以这样呢。对的,也可以这样,具体用法,见下面代码

    // 定义样式编译任务
    const scss = () => {
      return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest('./dist'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义脚本编译任务
    const script = () => {
      return src('./src/assets/scripts/*.js', { base: 'src'})
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('./dist'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义html模板编译任务
    const html = () => {
      return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({ data }))
        .pipe(dest('./dist'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }

    以上添加了一个stream:true,意思是重新加载不需要进行读写操作,而是直接以流的方式往浏览器中推

    好了,开发服务器的优化就到这了
    下面,我们继续来优化生产环境的构建build

    6、build的构建任务的优化
    6.1 上面我们说过,serve服务器中配置了一个routes,是因为构建后的html文件引入了一些外部的资源文件,我们去处理那些资源文件了。

    但是,build环境中,这些文件可能就找不到了,因为dist下没有node_modules文件夹,那么我们构建的时候该如何去处理这种构建后的资源引用问题呢
    首先,我们可以看下构建后的html


    可以看出,这种资源文件,构建后,会生成对应的build注释,标识了后续可将两个注释中间的部分合并成为一个新的文件(vendor.css)。那么如何处理这种情况呢。
    gulp提供了一种叫useref的插件来处理这种情况,他会将注释中间引用的资源合并成为一个新的资源文件
    安装 gulp-useref (yarn add gulp-useref --dev)
    新建任务用此插件去处理这种情况

    const useref = () => {
      return src('dist/*html', { base: 'dist' }) // 读取的是构建后的文件,故是dist下
        .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 请求的资源路径去哪找
        .pipe(dest('dist'))
    }

    上面的searchPath 是指定构建时,请求的资源文件去什么地方找,如上图中的main.css,我们可以直接在dist下找,如果找不到,那么我们去当前根目录下找,故配置了第二个 ‘.’ 这个 . 就代表当前根目录 。比如上面的bootstrap.css 就会去根目录下找,找到后,直接将引入的这个css打包进dist下,并合并成vendor.css 。
    这个合并,可能你们不大理解,看下图 ,你们就理解了


    这个注释中间引入了3个文件,那么都会被打包成vendor.js一个文件。同时会将注释删除

    此时,其实还会有点问题,大家可以看到读取文件是从dist下去读取,写入文件又是写入到dist下面,这其实会产生冲突,从同一个地方又读又写,是不是有问题啊。
    此时,我们可以通过一个中间文件来进行一个过度。如何过度,请看6.2

    6.2 我们可以在构建的时候,可以先让他构建到一个中间目录中,比如temp,然后useref再去temp中去读文件,读取后,再通过useref插件进行处理,然后再写入到dist中。那么我们原来的构建任务的写入路径就都要改了。但是这个只针对html,style,js 因为useref是处理引入的html以及js,css等资源路径的

    // 定义样式编译任务
    const scss = () => {
      return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest('./temp')) // 改成temp
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义脚本编译任务
    const script = () => {
      return src('./src/assets/scripts/*.js', { base: 'src'})
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('./temp')) // 改成temp
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义html模板编译任务
    const html = () => {
      return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({ data }))
        .pipe(dest('./temp')) // 改成temp
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    const useref = () => {
      return src('temp/*html', { base: 'temp' }) // 改成从temp下去读取文件流
        .pipe(plugins.useref({ searchPath: ['dist', '.']})) // 改成从temp下去读取文件流
        .pipe(dest('dist')) // 写入到dist
    }
    // 定义清除目录下的文件任务
    const clean = () => {
      return del(['dist', 'temp']) // 添加清除temp
    }

    然后修改构建流程,将useref放到compile之后再执行,同时,我们构建完以后,是不是还要将temp目录给清除啊,因为他只是个临时目录

    // 清除temp
    const cleanTemp = () => {
      return del('temp')
    }
    // 合并构建任务
    const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))

    6.3 文件压缩
    前面我们利用useref构建的html,css,js等是不是还没有给他进行压缩处理啊,我们build任务一般是打包线上代码,那么这些文件肯定都是要进行压缩的。那么如何压缩呢
    当然是针对不同的文件利用不同的插件进行压缩了
    html 使用插件gulp-htmlmin yarn add gulp-htmlmin --dev
    js 使用插件gulp-uglify yarn add gulp-uglify --dev
    css 使用插件cleanCss yarn add gulp-clean-css --dev
    同时,我们知道useref任务中是一个读取流可能读取到不同类型的文件(html或css或js),因此,我们还需要一个gulp-if插件来做判断

    const useref = () => {
      return src('temp/*html', { base: 'temp' }) // 读取的是构建后的文件,故是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, // 压缩html
          minifyCss: true, // 压缩html文件中的内嵌样式
          minifyJs: true // 压缩html文件中内嵌的js
        })))
        .pipe(dest('dist'))
    }

    6.4 导出相关指令
    上面我们一般都是只暴露了develop 和 build两个任务,但一般还有个clean任务,我们也是比较常用的,我们将这个任务也单独导出

    // 导出相关任务
    module.exports = {
      clean,
      build,
      develop
    }

    导出后,我们可以在package.json文件中去配置相关指令,以便我们更方便去执行我们的命令

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

    此时,我们可以直接通过yarn build去进行项目构建了

    整个构建流程基本已经完成了。
    下面我们来附上gulpfile.js完整代码

    // 实现这个项目的构建任务
    // 引入相关依赖
    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 sass = require('gulp-sass')
    // const babel = require('gulp-babel')
    // const swig = require('gulp-swig')
    // const imagemin = require('gulp-imagemin')
    // 定义html模板需要得数据
    const data = {
      menus: [
        {
          name: 'Home',
          icon: 'aperture',
          link: 'index.html'
        },
        {
          name: 'About',
          link: 'about.html'
        },
        {
          name: 'Contact',
          link: '#',
          children: [
            {
              name: 'Twitter',
              link: 'https://twitter.com/w_zce'
            },
            {
              name: 'About',
              link: 'https://weibo.com/zceme'
            },
            {
              name: 'divider'
            },
            {
              name: 'About',
              link: 'https://github.com/zce'
            }
          ]
        }
      ],
      pkg: require('./package.json'),
      date: new Date()
    }
    /* 定义相关构建任务 */
    // 定义样式编译任务
    const scss = () => {
      return src('./src/assets/styles/*.scss', { base: 'src' })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest('./temp'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义脚本编译任务
    const script = () => {
      return src('./src/assets/scripts/*.js', { base: 'src'})
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest('./temp'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义html模板编译任务
    const html = () => {
      return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({ data }))
        .pipe(dest('./temp'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义图片编译任务
    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 copy = () => {
      return src('./public/**', { base: 'public' })
        .pipe(dest('./dist'))
    }
    // 定义清除目录下的文件任务
    const clean = () => {
      return del(['dist', 'temp'])
    }
    // 清除temp
    const cleanTemp = () => {
      return del('temp')
    }
    // 初始化开发服务器
    const serve = () => {
      // watch监听相关源文件
      watch('src/assets/styles/*.scss', scss)
      watch('src/assets/scripts/*.js', script)
      watch('src/**/*.html', html)
      // watch('src/assets/images/**', image)
      // watch('src/assets/fonts/**', font)
      // watch('public/**', copy)
      watch(
        [
          'src/assets/images/**',
          'src/assets/fonts/**',
          'public/**'
        ],
        bs.reload
      )
      bs.init({
        notify: false,
        port: 2080,
        // files: 'dist/**',
        server: {
          baseDir: ['dist', 'src', 'public'],
          routes: {
            '/node_modules': 'node_modules'
          }
        }
      })
    }
    const useref = () => {
      return src('temp/*html', { base: 'temp' }) // 读取的是构建后的文件,故是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, // 压缩html
          minifyCss: true, // 压缩html文件中的内嵌样式
          minifyJs: true // 压缩html文件中内嵌的js
        })))
        .pipe(dest('dist'))
    }
    // 因以上任务都是需要编译的任务,且工作过程互相不受影响,故可以并行执行,故将以上5个任务合并成一个并行任务
    const compile = parallel(scss, script, html)
    // 合并构建任务
    const build = series(clean, parallel(series(compile, useref, cleanTemp), copy, image, font))
    // 开发构建任务
    const develop = series(compile, serve)
    // 导出相关任务
    module.exports = {
      clean,
      build,
      develop
    }

    但是,此时,我们发现没有,我们写了这么多,只是用于处理了这一个项目的构建任务,但是我们肯定是希望我们所写的这些东西,能够作为和当前项目结构相似的一类项目的自动化构建工具。那么,最好的办法是不是将这个gulpfile.js封装成一个模块,然后发布到npm上面去啊。
    那么以后人家需要使用的时候,是不是可以直接通过按照这个模块,就立马可以进行项目构建了啊。下面我们就来封装一下这个工作流

    自动化构建工作流封装
    1、首先,我们需要新建一个node_modules包。包名我们定为cgp-build
    我这里使用了一个脚手架工具(caz)生成node_modules包的一些基础目录

    我们先全局安装这个脚手架
    yarn global add caz

    运行caz nm cgp-build生成我们的包的基本目录


    这个包中,lib下的index.js就是我们这个包的入口文件(一般包的入口文件都是lib下的index.js文件,而cli指令文件的入口文件一般是bin下的cli.js或者index.js)

    也就是说,我们原来写在gulpfile.js中的代码,现在要放到lib/index.js中来,这里当别人执行这个包时,才会执行到这些具体的构建代码

    2、将gulpfile.js中的代码拷贝到index.js中来

    此时,gulpfile.js这个依赖了很多插件,所以这些插件都会被作为我们封装得这个包得生产依赖。故我们需要把之前那个打包项目中得package.json中devDependencies都拷贝到我们这个包目录中得package.json文件中的dependencies中



    那么此时,后面有项目安装了我们这个cgp-build的时,就会自动安装这个包所依赖的这些插件。

    3、提取项目中的数据
    此时,还有问题,我们往上去看gulpfile.js中的代码,发现在解析html时,是不是传入了一个data数据啊。而data我们是直接定义在gulpfile.js中的。但是我们都知道,这个data数据,是不是项目的数据啊,不同的项目可能这个数据就不一样了,可能有的项目html文件中还没有这种模板数据。所以说,这个data,是不是应该提到项目中去啊。那么提到哪呢。
    我们知道,很多项目中 是不是都有config.js文件啊,比如vue的vue.config.js。那么我们是不是也可以定义一个config文件啊,比如就叫page.config.js。那么用我们这个cgp-build进行自动化构建的项目都需要创建一个page.config.js文件,那我们是不是可以把这个data放到config文件中,当作配置数据传入啊。
    而此时,我们lib/index.js文件中,我们就可以通过引入这个config.js文件中的配置,然后在构建的时候再使用这个配置数据

    那么: 如何拿到项目目录下的config.js文件呢
    我们分析下:我们这个包,最终是会被安装在项目目录的node_modules文件夹下的
    那么我们这个包中的lib/index 相当于是在项目目录(我们假设项目目录是page-demo)下的node_models/cgp-build/lib/index.js 。
    那么我们不是拿到了项目的根目录,就能拿到项目中的page.config.js文件啊。
    node,js提供了一个全局api process.cwd() 可以获取到当前项目根目录

    // 获取根目录
    const cwd = process.cwd()
    let config = {} // 定义配置文件,这里面可能会有些默认配置
    try {
      const loadConfig = require(`${cwd}/page.config.js`) // 获取项目目录中的配置文件
      config = Object.assign({}, config, loadConfig) // 合并config和loadConfig
    } catch (err) {
      throw err
    }
    // 定义html模板编译任务
    const html = () => {
      return src('./src/**/*.html', { base: 'src' })
        .pipe(plugins.swig({ data: config.data })) // 修改为config中的data
        .pipe(dest('./temp'))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }

    那么,page.config.js的配置文件,格式如下



    现在,数据问题解决了,但是很多文件的路径我们是不是还是写死的啊


    像这种路径,不同项目,是不是可能不一样啊,所以我们这样写死,也是不合理的,也应该抽象到page.config.js的配置中去

    4、抽象路径
    我们先在/lib/index.js中写入一份默认配置,当项目中配置了相关配置后,会覆盖index.js中的默认配置

    // 获取根目录
    const cwd = process.cwd()
    let config = {
      build: {
        src: 'src',
        dist: 'dist',
        temp: 'temp',
        public: 'public',
        paths: {
          styles: 'assets/styles/*.scss',
          scripts: 'assets/styles/*.js',
          pages: '*.html',
          images: 'assets/images/**',
          fonts: 'assets/fonts/**'
        }
      }
    } // 定义配置文件,这里面可能会有些默认配置

    然后将index.js中的路径都用config变量去代替

    // 定义样式编译任务
    const scss = () => {
      return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.sass({ outputStyle: 'expanded' }))
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义脚本编译任务
    const script = () => {
      return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义html模板编译任务
    const html = () => {
      return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.swig({ data: config.data })) // 修改为config中的data
        .pipe(dest(config.build.temp))
        .pipe(bs.reload({ stream: true })) // 构建任务每次执行后,都reload一次
    }
    // 定义图片编译任务
    const image = () => {
      return src(config.build.paths.images, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.imagemin())
        .pipe(dest(config.build.dist))
    }
    // 定义字体编译任务
    const font = () => {
      return src(config.build.paths.fonts, { base: config.build.src, cwd: config.build.src })
        .pipe(plugins.imagemin())
        .pipe(dest(config.build.dist))
    }
    // 定义其他不需要经过编译的任务
    const copy = () => {
      return src('**', { base: config.build.public, cwd: config.build.public })
        .pipe(dest(config.build.dist))
    }
    // 定义清除目录下的文件任务
    const clean = () => {
      return del([config.build.dist, config.build.temp])
    }
    // 清除temp
    const cleanTemp = () => {
      return del(config.build.temp)
    }
    // 初始化开发服务器
    const serve = () => {
      // watch监听相关源文件
      watch(config.build.paths.styles, {cwd: config.build.src}, scss)
      watch(config.build.paths.scripts, {cwd: config.build.src}, script)
      watch(config.build.paths.pages, {cwd: config.build.src}, html)
      // watch('src/assets/images/**', image)
      // watch('src/assets/fonts/**', font)
      // watch('public/**', copy)
      watch(
        [
          config.build.paths.images,
          config.build.paths.images,
          `${config.build.public}/**`
        ],
        bs.reload
      )
      bs.init({
        notify: false,
        port: 2080,
        // files: 'dist/**',
        server: {
          baseDir: [config.build.dist, config.build.src, config.build.public],
          routes: {
            '/node_modules': 'node_modules'
          }
        }
      })
    }
    const useref = () => {
      return src(config.build.paths.pages, { base: config.build.temp, cwd: config.build.temp }) // 读取的是构建后的文件,故是dist下
        .pipe(plugins.useref({ searchPath: [config.build.temp, '.']})) // 请求的资源路径去哪找
        .pipe(plugins.if(/\.js$/, plugins.uglify()))
        .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(/\.html$/, plugins.htmlmin({
          collapseWhitespace: true, // 压缩html
          minifyCss: true, // 压缩html文件中的内嵌样式
          minifyJs: true // 压缩html文件中内嵌的js
        })))
        .pipe(dest(config.build.dist))
    }

    我们在上面很多地方加了个cwd选项,是因为我们抽象出来的路径,去掉了src,所以我们需要通过cwd去指定去哪个目录下找这个路径



    5、包装gulp-cli
    下面我们要包装一下我们自己的cli命令,为什么要包装呢,因为gulp构建时,默认是找gulpfile.js文件的,而我们现在是放在/bin/index.js中,对于项目而言,这个文件在/node_modules/cgp-build/lib/index.js中,所以在项目中运行yarn gulp build是会报错的,报错,找不到gulpfile.js文件


    此时,我们需要手动去指定gulpfile.js文件为哪个文件

    yarn gulp build --gulpfile ./node_modules/cgp-build/lib/index.js --cwd .

    –cwd . 的意思是以当前项目目录作为根目录,因为gulp会默认以gulpfile.js文件所在目录为根目录,所以我们需要特别指定一下根目录

    那么这么弄,是不是很繁琐啊,每次我需要执行下构建任务时,都要输入这么一大串。此时,我们就可以自定义这个包文件自己的cli指令,将这些 --gulpfile --cwd等参数都集成到指令中去

    如何定义cli
    在包文件目录下新建bin文件夹,并在bin中新建cli.js。然后在package.json文件中添加bin字段




    cli.js文件需要加个文件头 #!/usr/bin/env node(cli入口文件都需要的)



    这里我解释一下:一般包的cli指令文件都是在包目录下的bin目录下,比如webpack,当你运行webpack main.js命令去打包main.js时,也是会先去找node_modules/webpack/bin/*.js文件的

    那么,此时,我们cli.js 文件中需要写什么呢,我们分析下
    大家想啊,我们本质是要去执行gulp build --gulpfile …这种命令,
    只是我们先去执行了我们自己的cli命令 cgp-build,那cgp-build执行后,去找了/bin/cli.js文件后,我们是不是只需要在这里去执行gulp的构建命令就可以了啊,那执行gulp的构建命令本质上是不是去执行gulp/bin/**.js文件啊。所以此时,我们只需要在我们的cli.js文件中去运行gulp/bin/gulp.js文件就行了



    这样一来,当我们执行cgp-build build时,实际上就会执行gulp build命令

    但这样还不够啊,我们前面是不是说了啊,我们需要携带参数去查找gulpfile.js文件以及指定根目录啊。此时,我们可以借助全局方法process.argv 这个可以拿到的其实就是参数列表,是个数组,如:–gulpfile /node_modules/cgp-build/lib/index.js 数组中就是[’–gulpfile’, ‘/node_modules/cgp-build/lib/index.js’]

    那么,我们可以通过push方法往参数中添加参数



    此时,整个cli的封装就完成了。

    npm提交
    npm提交我们上篇文章已经说过了,这里就随便提一下了,
    1、将包上次至开源库,如github
    2、npm publish 或者 yarn publish上传至npm库中

    提交完后,我们测试下
    先在本地准备一个项目目录gulp-demo,里面放入我们之前那个项目
    然后安装我们提交至npm的包 cgp-build
    yarn add cgp-build --dev


    然后运行yarn cgp-build build 或者 cgp-build build



    可以看出,是没有问题的,正常打包成功


    好了,自动化构建就写到这了。喜欢请点个赞,谢谢

  • 回复

    使用道具 举报

    0

    主题

    741

    帖子

    -226

    积分

    限制会员

    积分
    -226
    发表于 2021-4-13 15:53:00 | 显示全部楼层
    我是来刷分的,嘿嘿
    回复

    使用道具 举报

    0

    主题

    823

    帖子

    -260

    积分

    限制会员

    积分
    -260
    发表于 2021-4-13 16:19:50 | 显示全部楼层
    支持一下
    回复

    使用道具 举报

    1

    主题

    810

    帖子

    -285

    积分

    限制会员

    积分
    -285
    发表于 2021-4-13 16:41:39 | 显示全部楼层
    写的真的很不错
    回复

    使用道具 举报

    1

    主题

    821

    帖子

    -281

    积分

    限制会员

    积分
    -281
    发表于 2021-4-13 17:02:05 | 显示全部楼层
    LZ真是人才
    回复

    使用道具 举报

    0

    主题

    832

    帖子

    -251

    积分

    限制会员

    积分
    -251
    发表于 2021-4-13 17:22:47 | 显示全部楼层
    有竞争才有进步嘛
    回复

    使用道具 举报

    0

    主题

    828

    帖子

    -298

    积分

    限制会员

    积分
    -298
    发表于 2021-4-13 17:46:20 | 显示全部楼层
    不错
    回复

    使用道具 举报

    1

    主题

    795

    帖子

    -228

    积分

    限制会员

    积分
    -228
    发表于 2021-4-13 18:07:25 | 显示全部楼层
    沙发!沙发!
    回复

    使用道具 举报

    1

    主题

    803

    帖子

    -251

    积分

    限制会员

    积分
    -251
    发表于 2021-4-13 18:35:08 | 显示全部楼层
    帮你顶下哈!!
    回复

    使用道具 举报

    0

    主题

    832

    帖子

    -299

    积分

    限制会员

    积分
    -299
    发表于 2021-4-13 18:55:45 | 显示全部楼层
    不错,支持下楼主
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|无图版|手机版|小黑屋|广州@IT精英团

    GMT+8, 2021-5-8 12:35 , Processed in 0.105086 second(s), 19 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表