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í)行build和serve; - 可以在
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插件的使用
- 找到對應(yīng)插件并通過
NPM安裝; - 在gruntfile中通過
loadNpmTasks方法將這些任務(wù)加載進(jìn)來; - 在
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-htmlmin、gulp-uglify、gulp-clean-css分別對html、css、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