自動(dòng)化構(gòu)建

NPM Scripts
使用NPM Scripts的方式包裝構(gòu)建命令。

  • 可以定義一個(gè)preserve鉤子,這個(gè)命令會(huì)在serve之前執(zhí)行,實(shí)現(xiàn)啟動(dòng)服務(wù)之前自動(dòng)地構(gòu)建文件;
  • 可以為sass命令添加--watch參數(shù),在工作時(shí)會(huì)監(jiān)聽文件的變化,一旦代碼中的sass文件被改變就會(huì)自動(dòng)被編譯。但sass文件在工作時(shí)命令行會(huì)被阻塞,因此需要同時(shí)執(zhí)行多個(gè)任務(wù)。
  • 可以借助npm-run-all這個(gè)模塊,通過run-p同時(shí)執(zhí)行buildserve
  • 可以在browser-sync末尾添加--files參數(shù),可以讓browser-sync啟動(dòng)后監(jiān)聽項(xiàng)目中一些文件的變化,一旦文件發(fā)生變化,browser-sync會(huì)講這些內(nèi)容自動(dòng)同步到瀏覽器,從而看到最新的效果,避免修改代碼后再手動(dòng)刷新瀏覽器的操作。

常用的自動(dòng)化構(gòu)建工具

  • Grunt
  • Gulp
  • FIS

Grunt

基本使用

// gruntfile.js

// Grunt 的入口文件
// 用于定義一些需要 Grunt 自動(dòng)執(zhí)行的任務(wù)
// 需要導(dǎo)出一個(gè)函數(shù)
// 此函數(shù)接收一個(gè) grunt 的對象類型的形參
// grunt 對象中提供一些創(chuàng)建任務(wù)時(shí)會(huì)用到的 API

module.exports = grunt => {
    // 使用registerTask注冊一個(gè)任務(wù),第一個(gè)參數(shù)為任務(wù)名稱,執(zhí)行yarn grunt <name>即可執(zhí)行該任務(wù)
    grunt.registerTask('foo', '任務(wù)描述', () => {
        console.log('hello grunt')
    })

    grunt.registerTask('bar', () => {
        console.log('other task')
    })

    // default 是默認(rèn)任務(wù)名稱
    // 通過 grunt 執(zhí)行時(shí)可以省略
    // grunt.registerTask('default', () => {
    //   console.log('default task')
    // })

    // 第二個(gè)參數(shù)可以指定此任務(wù)的映射任務(wù),
    // 這樣執(zhí)行 default 就相當(dāng)于執(zhí)行對應(yīng)的任務(wù)
    // 這里映射的任務(wù)會(huì)按順序依次執(zhí)行,不會(huì)同步執(zhí)行
    grunt.registerTask('default', ['foo', 'bar'])

    // 也可以在任務(wù)函數(shù)中執(zhí)行其他任務(wù)
    grunt.registerTask('run-other', () => {
        // foo 和 bar 會(huì)在當(dāng)前任務(wù)執(zhí)行完成過后自動(dòng)依次執(zhí)行
        grunt.task.run('foo', 'bar')
        console.log('current task running~')
    })

    // 默認(rèn) grunt 采用同步模式編碼
    // 如果需要異步可以使用 this.async() 方法創(chuàng)建回調(diào)函數(shù)
    // grunt.registerTask('async-task', () => {
    //   setTimeout(() => {
    //     console.log('async task working~')
    //   }, 1000)
    // })

    // 由于函數(shù)體中需要使用 this,所以這里不能使用箭頭函數(shù)
    grunt.registerTask('async-task', function () {
        const done = this.async()
        setTimeout(() => {
            console.log('async task working~')
            done()
        }, 1000)
    })
}

標(biāo)記失敗

  • 如果是同步任務(wù),直接return false即可標(biāo)記失敗;
  • 如果是異步任務(wù),則將false作為參數(shù)傳遞給異步函數(shù)。
    done(false)

配置選項(xiàng)方法

module.exports = grunt => {

  grunt.initConfig({
    // 對象的屬性名一般與任務(wù)名保持一致。
    // foo: 'bar'
    foo: {
      bar: 123
    }
  })

  grunt.registerTask('foo', () => {
    // console.log(grunt.config('foo')) // bar
    console.log(grunt.config('foo.bar')) // 123
    // grunt的config支持通過foo.bar的形式獲取屬性值,也可以通過獲取foo對象,然后取屬性
  })
}

多目標(biāo)模式

module.exports = grunt => {
  // 多目標(biāo)模式,可以讓任務(wù)根據(jù)配置形成多個(gè)子任務(wù)
  grunt.initConfig({
    build: {
      // 作為目標(biāo)的配置選項(xiàng)
      options: {
        msg: 'task options'
      },
      foo: {
        options: {
          msg: 'foo target options'
        }
      },
      bar: '456'
    }
  })

  grunt.registerMultiTask('build', function () {
    // 通過this.target獲取當(dāng)前執(zhí)行目標(biāo)名稱,this.data獲取這個(gè)執(zhí)行目標(biāo)對應(yīng)的配置數(shù)據(jù)
    console.log(`task: build, target: ${this.target}, data: ${this.data}`)
    // 獲取配置選項(xiàng)
    console.log(this.options())
  })
}

Grunt插件的使用

  1. 找到對應(yīng)插件并通過NPM安裝;
  2. 在gruntfile中通過loadNpmTasks方法將這些任務(wù)加載進(jìn)來;
  3. initConfig中為這些任務(wù)添加配置選項(xiàng)。

常用插件

  • grunt-sass
  • grunt-babel
  • grunt-contrib-watch

load-grunt-tasks會(huì)自動(dòng)加載所有g(shù)runt插件中的任務(wù)。

// gruntfile.js
const sass = require('sass')
const loadGruntTasks = require('load-grunt-tasks')

module.exports = grunt => {
  grunt.initConfig({
    // 將sass編譯為css
    sass: {
      options: {
        sourceMap: true,
        // 指定處理sass編譯使用的模塊
        implementation: sass,
      },
      main: {
        files: {
          // 輸出文件路徑: 輸入文件原路徑
          'dist/css/main.css': 'src/scss/main.scss'
        }
      }
    },
    // 進(jìn)行ES最新特性轉(zhuǎn)換
    babel: {
      options: {
        sourceMap: true,
        presets: ['@babel/preset-env'],
      },
      main: {
        files: {
          'dist/js/app/js': 'src/js/app.js'
        }
      }
    },
    // 實(shí)現(xiàn)文件修改后自動(dòng)編譯
    watch: {
      js: {
        files: ['src/js/*.js'],
        // 文件修改后需要執(zhí)行的任務(wù)
        tasks: ['babel'],
      },
      css: {
        // scss是sass的新拓展名
        files: ['src/scss/*.scss'],
        tasks: ['sass'],
      },
    }
  })

  loadGruntTasks(grunt)  // 自動(dòng)加載所有的 grunt 插件中的任務(wù)
  grunt.registerTask('default', ['sass', 'babel', 'watch'])
}

Gulp

基本使用

yarn init
yarn add gulp
code gulpfile.js
yarn gulp <name>
gulpfile.js

組合任務(wù)

  • series串行執(zhí)行任務(wù),會(huì)按照順序依次執(zhí)行任務(wù);
  • parallel并行執(zhí)行任務(wù),比如同時(shí)編譯js文件和css文件,這二者互不干擾。

異步任務(wù)

// 異步任務(wù)
const fs = require('fs')

// 1. 回調(diào)函數(shù)的方式
exports.callback = done => {
    console.log('callback task')
    done()
}

exports.callback_error = done => {
    console.log('callback task')
    done(new Error('task failed'))
}

// 2. promise
exports.promise = () => {
    console.log('promise task')
    return Promise.resolve()
}

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

// 3. async/await 語法糖
// 實(shí)際上內(nèi)部還是promise方式
// 受限于node環(huán)境支持與否
const timeout = time => {
    return new Promise(resolve => {
        setTimeout(resolve, time)
    })
}
exports.async = async () => {
    await timeout(1000)
    console.log('async task');
}

// 4. 通過流的形式
exports.stream = () => {
    const readStream = fs.createReadStream('package.json')
    const writeStream = fs.createWriteStream('temp.txt')
    readStream.pipe(writeStream)
    // readStream中有個(gè)end事件監(jiān)聽文件讀取,當(dāng)文件讀取結(jié)束就會(huì)觸發(fā)end事件
    return readStream
}

構(gòu)建過程核心工作原理

讀取流 -> 轉(zhuǎn)換流 -> 寫入流

const fs = require('fs')
const { Transform } = require('stream')

exports.default = () => {
  // 文件讀取流
  const readStream = fs.createReadStream('normalize.css')

  // 文件寫入流
  const writeStream = fs.createWriteStream('normalize.min.css')

  // 文件轉(zhuǎn)換流
  const transformStream = new Transform({
    // 核心轉(zhuǎn)換過程
    transform: (chunk, encoding, callback) => {
      // chunk:讀取流中讀取到的內(nèi)容(Buffer)
      const input = chunk.toString()
      const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')
      callback(null, output)
    }
  })

  return readStream
    .pipe(transformStream) // 轉(zhuǎn)換
    .pipe(writeStream) // 寫入
}

文件操作API

const { src, dest } = require('gulp')
const cleanCSS = require('gulp-clean-css')
const rename = require('gulp-rename')

exports.default = () => {
  // 創(chuàng)建構(gòu)建任務(wù)的流程:
  // 1. 通過Gulp中的src方法創(chuàng)建讀取流
  // 2. 借助插件提供的轉(zhuǎn)換流實(shí)現(xiàn)文件加工
  // 3. 通過Gulp中的dest方法創(chuàng)建寫入流
  return src('src/*.css')
    .pipe(cleanCSS())
    .pipe(rename({ extname: '.min.css' }))
    .pipe(dest('dist'))
}

Gulp自動(dòng)化構(gòu)建案例

樣式編譯
const { src, dest } = require('gulp')
const sass = require('gulp-sass');

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目錄為基準(zhǔn)拷貝文件結(jié)構(gòu)
    .pipe(sass({ outputStyle: 'expanded' }))  // 以展開的形式生成css代碼
    .pipe(dest('dist'))
}

module.exports = {
  style
}
腳本編譯
const { src, dest } = require('gulp')
const babel = require('gulp-babel')

const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))  // presets: 最新的所有特性的整體打包
    .pipe(dest('dist'))
}

module.exports = {
  script
}
頁面模版編譯
const { src, dest } = require('gulp')
const swig = require('gulp-swig')

const data = {
  // ...
}

const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))  // 頁面模版轉(zhuǎn)換插件
    .pipe(dest('dist'))
}

module.exports = {
  page
}
const { src, dest, parallel } = require('gulp')
const sass = require('gulp-sass')
const babel = require('gulp-babel')
const swig = require('gulp-swig')

const data = {
  // ...
}

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目錄為基準(zhǔn)拷貝文件結(jié)構(gòu)
    .pipe(sass({ outputStyle: 'expanded' }))  // 以展開的形式生成css代碼
    .pipe(dest('dist'))
}


const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(babel({ presets: ['@babel/preset-env'] }))  // presets: 最新的所有特性的整體打包
    .pipe(dest('dist'))
}

const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(swig({ data }))  // 頁面模版轉(zhuǎn)換插件
    .pipe(dest('dist'))
}

// 組合任務(wù)
const compile = parallel(style, script, page)

module.exports = {
  compile
}
圖片和字體文件轉(zhuǎn)換
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(imagemin())  // 圖片壓縮插件,刪除一些無用的二進(jìn)制信息
    .pipe(dest('dist'))
}

const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(imagemin())
    .pipe(dest('dist'))
}
其他文件及文件清除
const { src, dest, parallel, series } = require('gulp')

const del = require('del')

const clean = () => {
  return del(['dist'])
}

// ...

// public目錄下的文件直接拷貝
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

// 組合任務(wù)
const compile = parallel(style, script, page, image, font)

const build = series(clean, parallel(compile, extra))  // 先清除dist文件夾,再進(jìn)行編譯

module.exports = {
  compile,
  build
}
自動(dòng)加載插件

使用grunt-load-plugins自動(dòng)加載插件,使用plugins.<name>獲取插件。

開發(fā)服務(wù)器
const browserSync = require('browser-sync')
const bs = browserSync.create()  // 創(chuàng)建一個(gè)服務(wù)器 bs => browser server

const serve = () => {
  // 初始化服務(wù)器
  bs.init({
    notify: false,  // 是否顯示 右上角browser-sync是否連接服務(wù)的提示
    port: 8080, // 端口號(hào)
    // open:false,  // 是否自動(dòng)打開瀏覽器
    files: "dist/**",  // 監(jiān)聽dist目錄下文件,發(fā)生變化后更新網(wǎng)站內(nèi)容
    server: {
      baseDir: 'dist',
      // 優(yōu)先級高于baseDir
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

module.exports = {
  serve
}
監(jiān)視變化以及構(gòu)建優(yōu)化

使用gulp中的watch方法監(jiān)視文件變化,由于圖片壓縮等操作一般是上線之前的操作,如果放到開發(fā)構(gòu)建階段是不必要的,因此可以將其移動(dòng)至build任務(wù)中。

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

const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })  // 以src目錄為基準(zhǔn)拷貝文件結(jié)構(gòu)
    .pipe(plugins.sass({ outputStyle: 'expanded' }))  // 以展開的形式生成css代碼
    .pipe(dest('dist'))
    .pipe(bs.reload({ stream: true }))
}

// ...

const serve = () => {
  // 監(jiān)視文件
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)

  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**'
  ], bs.reload)

  // 初始化服務(wù)器
  bs.init({
    notify: false,  // 是否顯示 右上角browser-sync是否連接服務(wù)的提示
    port: 8080, // 端口號(hào)
    // open:false,  // 是否自動(dòng)打開瀏覽器
    // files: "dist/**",  // 監(jiān)聽dist目錄下文件,發(fā)生變化后更新網(wǎng)站內(nèi)容
    server: {
      baseDir: 'dist',
      // 優(yōu)先級高于baseDir
      routes: {
        '/node_modules': 'node_modules'
      }
    }
  })
}

// 上線之前執(zhí)行的任務(wù)
const build = series(clean, parallel(compile, image, font, extra))  // 先清除dist文件夾,再進(jìn)行編譯

const develop = series(compile, serve)

module.exports = {
  clean,
  build,
  develop
}
useref文件引用處理
// useref插件會(huì)檢測文件中的構(gòu)建注釋,把構(gòu)建注釋中包含的內(nèi)容(引用的資源)最終合并到一個(gè)文件中,最后將注釋全部去掉
const useref = () => {
  return src('dist/*.html', { base: 'dist' })
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    .pipe(dest('dist'))
}
文件壓縮

使用gulp-htmlmingulp-uglify、gulp-clean-css分別對htmlcss、js進(jìn)行壓縮,其中,壓縮html需要進(jìn)一步配置選項(xiàng)。

const useref = () => {
  return src('dist/*.html', { base: 'dist' })
    .pipe(plugins.useref({ searchPath: ['dist', '.'] }))
    // html css js
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true
    })))
    .pipe(dest('release'))
}

封裝自動(dòng)化構(gòu)建工作流

gulpfile.js的內(nèi)容復(fù)制到index.js文件中,然后將模塊使用yarn link鏈接到全局。需要使用該模塊的項(xiàng)目使用yarn add "<name>"進(jìn)行安裝。
基于“約定大于配置”的原則,在項(xiàng)目根目錄下創(chuàng)建一個(gè)配置文件pages.config.js,

包裝Gulp CLI

在bin目錄下創(chuàng)建一個(gè)js文件作為gulp-cli的執(zhí)行入口文件,在該文件中創(chuàng)建標(biāo)識(shí)以及添加運(yùn)行所需參數(shù),并在package.json文件中添加bin字段。

// 聲明注釋
#!/usr/bin/env node  

process.argv.push('--cwd')  // 指定工作目錄
process.argv.push(process.cwd())
process.argv.push('--gulpfile')  // 指定gulpfile路徑
process.argv.push(require.resolve('..'))

require('gulp/bin/gulp')  // 自動(dòng)執(zhí)行g(shù)ulp-cli
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容