自动化构建
一切重复的工作都应该被自动化
自动化构建是前端工程化过程中一个重要的组成部分
自动化构建工作流可以使我们在开发阶段使用一些高效的语法、规范、标准和工具,脱离运行环境兼容性带来的问题,并最终自动转换成能够被运行环境支持的可执行代码
自动化构建初体验
尝试使用sass开发页面样式,并自动编译为css文件
创建项目目录,并使用
yarn init -y
初始化添加index.html页面用于测试
-
yarn add sass --dev
添加sass模块作为开发依赖- 此时可以使用sass编写样式文件*.scss,并使用
yarn sass
命令将scss文件编译为css文件
- 此时可以使用sass编写样式文件*.scss,并使用
-
在package.json文件中,添加script属性,定义npm运行脚本
- 此时可以通过
npm run
或yarn
运行script中定义的命令
- 此时可以通过
yarn add npm-run-all --dev
安装npm-run-all作为开发依赖,可以用来顺序同时运行多个script中定义的命令yarn add browser-sync --dev
安装browser-sync作为开发依赖,可以监控指定目录下的文件改动,并自动刷新浏览器-
上述步骤实现了开发过程中,自动将scss文件编译为css文件,并自动监听文件变化,刷新浏览器实时查看最新页面效果
// package.json { "name": "sample", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "build": "sass scss/main.scss css/main.css --watch", "serve": "browser-sync . --files \"*.html, css/*.css\"", "start": "run-p build serve" }, "devDependencies": { "browser-sync": "^2.26.12", "sass": "^1.26.10", "npm-run-all": "^4.1.5" } }
常用的自动化构建工具
- Grunt
- 插件生态丰富,可以实现任意类型项目的构建
- 工作过程基于临时文件,磁盘读写频繁,构建速度较慢
- Gulp
- 插件生态丰富
- 工作过程基于内存,构建速度较快
- 支持同时进行多个构建任务
- FIS
- 由百度前端团队推出的构建工具
- 捆绑式全家桶
- 适合新手
-
Webpack?- Webpack属于模块打包工具
Grunt
-
基本使用
新建项目目录,并使用
yarn init -y
命令初始化yarn add grunt
添加grunt模块-
添加gruntfile.js文件
- Grunt的入口文件
- 用于定义Grunt自动执行的任务
- 需要导出一个函数
- 函数接收grunt形参,用于提供创建任务时将会用到的的API
-
grunt.registerTask
注册待执行的task- 第一个参数为task的名称,如果名称为
default
,则为默认task,执行yarn grunt
不指定task明时默认执行 default task - 第二个参数如果是回调函数,则表示task执行时要执行的内容
- 第二个参数如果是字符串,则是对该task的描述信息,当执行
yarn grunt --help
时会显示相应任务的描述 - 第二个参数如果是数组,则接收由task名称组成的字符串数组,执行该task将会依次执行数组中指定的task
- 如果task执行的是一个异步任务,需要使用调用
this.async()
返回的函数来标记异步操作执行完成,此时grunt会等待异步操作执行完成
- 第一个参数为task的名称,如果名称为
-
yarn grunt
执行gruntfile.js中定义的task// gruntfile.js module.exports = grunt => { grunt.registerTask('foo', () => { console.log('hello grunt') }) grunt.registerTask('bar', 'description', () => { console.log('hello bar') }) grunt.registerTask('default', ['foo', 'bar']) grunt.registerTask('async-task', function() { const done = this.async(); setTimeout(()=> { console.log('async task done') done() }, 1000) }) }
-
标记任务失败
同步任务通过在一个task的回调函数中
return false
来实现顺序执行多个任务时,当有一个任务被标记失败,后续任务将不再继续执行
使用
--force
参数来强制执行所有任务-
异步任务标记失败,需要使用
this.async()
返回的函数,传入false// gruntfile.js module.exports = grunt => { grunt.registerTask('foo', () => { console.log('hello grunt') }) grunt.registerTask('bar', 'description', () => { console.log('hello bar') }) grunt.registerTask('bad', ()=> { console.log('bad task') return false }) grunt.registerTask('default', ['foo', 'bad', 'bar']) grunt.registerTask('async-task', function() { const done = this.async(); setTimeout(()=> { console.log('async task fail') done(false) }, 1000) }) }
-
配置方法
使用
grunt.initConfig({})
方法进行grunt配置参数的初始化-
在task的回调函数中,通过
grunt.config(key)
方法可以获取参数配置对象中的值// gruntfile.js module.exports = grunt => { grunt.initConfig({ hello: { what: 'world' } }) grunt.registerTask('foo', () => { console.log(`hello ${grunt.config('hello.what')}`) }) }
-
多目标任务
多目标模式,可以让任务根据配置形成多个子任务
使用
grunt.registerMultiTask(
来注册多目标任务, -
多目标任务需要通过
grunt.initConfig
来配置相应的子任务- 以task名称作为key,value是配置对象
- 配置对象以子任务名称作为key,在任务运行时可以通过
this.target
获取当前执行的子任务名称 - 配置对象的值为运行时的配置数据,在任务运行时可以通过
this.data
获取当前执行的子任务的配置数据 - 当配置对象的key为options时,代表任务运行时的选项配置,而不代表一个子任务
- 子任务的配置数据中也可以包含options,此时该子任务中配置的options覆盖任务全局的options
// gruntfile.js module.exports = grunt => { grunt.initConfig({ build: { options: { hello: 'world' }, css: { value: 'css' }, js: { options: { hello: 'grunt' }, value: 'js' } }, hello: { what: 'world' } }) grunt.registerMultiTask('build', function() { console.log(this.options()) console.log(this.target) console.log(this.data) }) }
-
插件的使用
安装相应的grunt插件模块,例如
yarn add grunt-contrib-clean
使用
grunt.loadNpmTasks
加载插件,并在grunt.initConfig()
中配置相关的参数(这里grunt-contrib-clean是多目标任务)-
执行插件相关的task
// gruntfile.js module.exports = grunt => { grunt.initConfig({ clean: { temp: 'temp/*.txt' } }) grunt.loadNpmTasks('grunt-contrib-clean') }
-
常用插件及总结
- grunt-sass 用于scss转css
yarn add grunt-sass sass --dev
- grunt-babel 用于js语法转换
yarn add grunt-babel @babel/core @babel/present-env --dev
- grunt-contrib-watch 开发阶段监控文件改动,并自动执行配置指定的task
yarn add grunt-contirb-watch --dev
- load-grunt-tasks 用于自动加载所有grunt插件中的任务
yarn add load-grunt-tasks --dev
- grunt-sass 用于scss转css
Gulp
-
gulp的基本使用
新建项目目录,并使用
yarn init -y
命令初始化yarn add gulp
添加gulp模块-
添加gulpfile.js文件
- gulp的入口文件
- 用于定义gulp自动执行的任务
- gulp 4.0以上的版本通过exports.task的方式导出一个task,exports.default为默认的task
- gulp定义任务为异步任务,需要在任务执行的函数中调用参数传入的方法,去标记任务是否执行完成
-
yarn gulp
执行gulpfile.js中定义的task// gulpfile.js gulp的入口文件 exports.foo = done => { console.log('gulp foo') done() } exports.default = done => { console.log('gulp default') done() } // gulp 4.0以前版本定义task方式 const gulp = require('gulp') gulp.task('bar', done => { console.log('gulp bar') done() })
-
gulp的组合任务
- gulp提供了
series
和parallel
两种方式来组合执行多个任务 - series为串行执行多个任务
- parallel为并行执行多个任务
// gulpfile.js const { series, parallel } = require('gulp') const task1 = done => { setTimeout(() => { console.log('task 1 working') done() }, 1000); } const task2 = done => { setTimeout(() => { console.log('task 2 working') done() }, 1000); } const task3 = done => { setTimeout(() => { console.log('task 3 working') done() }, 1000); } exports.series = series(task1, task2, task3) exports.parallel = parallel(task1, task2, task3)
- gulp提供了
-
gulp的异步任务常用的几种方式
-
回调函数方式
-
gulp异步任务函数接收一个回调函数参数,通过该回调函数,可以标记异步任务是否执行完成,或者发生异常,当发生异常,后续任务将不再继续执行
exports.callback = done => { console.log('callback') done() } exports.callback_error = done => { console.log('callback error') done(new Error('callback error')) }
-
-
Promise
-
gulp异步任务支持使用Promise方式,标记异步任务执行完成,返回一个resolved状态的Promise对象,标记任务失败,返回一个rejected状态的Promise对象
exports.promise = () => { console.log('promise') return Promise.resolve() } exports.promise_error = () => { console.log('promise error') return Promise.reject(new Error('promise error')) }
-
-
async/await
-
async/await是Promise的语法糖,使用node版本8以上时,gulp也支持使用该方式处理异步任务
const timeout = time => { return new Promise((resolve) => { setTimeout(resolve, time) }) } exports.async = async () => { await timeout(1000) console.log('async timeout') }
-
-
stream
通常在自动化构建过程中需要处理大量的文件,stream也是gulp异步任务处理中常用的方式
-
通过返回一个stream对象,当stream的end方法被调用时,异步任务会被标记完成
const fs = require('fs') exports.stream = () => { const readStream = fs.createReadStream('package.json') const writeStream = fs.createWriteStream('temp.txt') readStream.pipe(writeStream) return readStream } // 等同于 exports.stream_callback = done => { const readStream = fs.createReadStream('package.json') const writeStream = fs.createWriteStream('temp.txt') readStream.pipe(writeStream) readStream.on('end', () => { done() }) }
-
-
gulp构建过程核心工作原理
官方对于gulp的定义是the streaming build system
即基于流的构建系统
构建的流程通常是:读取文件 -> 处理读取内容 -> 写入文件
-
对应gulp的工作过程:读取流(read stream) -> 转换流(transform stream) -> 写入流 (write stream)
// 尝试模拟实现css的压缩 const fs = require('fs') const { Transform } = require('stream') exports.transform = () => { const read = fs.createReadStream('style.css') const write = fs.createWriteStream('style.min.css') const transform = new Transform({ transform: (chunk, encoding, callback) => { const input = chunk.toString() const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '') // 去除空格及注释 callback(null, output) } }) return read .pipe(transform) .pipe(write) }
-
gulp文件操作API
-
使用gulp提供的src与dest来实现文件的读取流与写入流
// 使用src批量读取文件,使用dest批量写入文件 // 借助gulp-clean-css插件压缩css // 借助gulp-rename插件重命名 const { src, dest } = require('gulp') const cleanCss = require('gulp-clean-css') const rename = require('gulp-rename') exports.minify = () => { return src('style.css') .pipe(cleanCss()) .pipe(rename({ extname: '.min.css' })) .pipe(dest('dist')) }
-
-
gulp案例
这里通过一个工程样例来展示,在实际开发过程中,对一个前端工程使用gulp进行自动化构建可能会涉及到的各种编译配置
例如样式文件sass的编译,脚本ES6+的转换,HTML文件的转换等等
工程demo见 https://github.com/zce/zce-gulp-demo.git
-
样式编译
const { src, dest } = require('gulp'); const sass = require('gulp-sass'); const style = () => { return src('src/assets/styles/*.scss', { base: 'src' }) .pipe(sass({ outputStyle: 'expanded' })) .pipe(dest('dist/styles')) } module.exports = { style, }
-
脚本编译
// gulpfile.js 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'] })) .pipe(dest('dist')) } module.exports = { script, }
-
页面模板编译
// gulpfile.js const { src, dest } = require('gulp'); const swig = require('gulp-swig'); const data = { menus: [ { name: 'Home', icon: 'aperture', link: 'index.html' }, { name: 'Features', link: 'features.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 page = () => { return src('src/*.html', { base: 'src' }) .pipe(plugins.swig({ data, defaults: { cache: false } })) // 关闭缓存防止修改不实时生效 .pipe(dest('temp')) } module.exports = { page, }
-
图片和字体文件转换
// gulpfile.js const { src, dest} = require('gulp'); const imagemin = require('gulp-imagemin'); 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')) } module.exports = { image, font, }
-
其他文件及文件清除
const del = require('del'); const extra = () => { return src('public/**', { base: 'public' }) .pipe(dest('dist')) } const clean = () => { return del(['dist']) }
-
自动加载插件
const loadPlugins = require('gulp-load-plugins'); const plugins = loadPlugins(); // 使用gulp-load-plugins插件自动加载所有已安装的以gulp-开头的插件,并使用plugins.xxxx访问相应的插件 // 例如使用plugins.sass访问gulp-sass插件
-
开发服务器
const browserSync = require('browser-sync'); const bs = browserSync.create(); const serve = () => { bs.init({ notify: false, files: 'dist/**', server: { baseDir: 'dist', routes: { '/node_modules': './node_modules' } } }) }
-
监视变化以及构建优化
- 使用gulp提供的watch API来监控指定文件,并在文件变化时,执行相应的task
- watch接收两个参数,第一个参数是文件通配符字符串或者通配符字符串数组,表示要监视的文件,第二个参数是文件变化时要执行的task函数
- 在开发阶段,对于图片及字体的压缩以及静态资源的拷贝意义不大,同时会增加构建任务的开销,将这些文件保留在源文件目录(需指定baseDir)并直接通过watch对这些目录进行监视,在文件发生变化时,执行bs.reload刷新页面
// gulpfile.js const { src, dest, series, parallel, watch } = require('gulp'); const bs = browserSync.create(); const serve = () => { watch('src/assets/styles/*.scss', style) watch('src/assets/scripts/*.js', script) watch('src/**/*.html', page) // watch('src/assets/images/**', image) 开发阶段意义不大 // watch('src/assets/fonts/**', font) 开发阶段意义不大 // watch('public/**', extra) 开发阶段意义不大 watch([ 'src/assets/images/**', 'src/assets/fonts/**', 'public/**', ], bs.reload) bs.init({ notify: false, files: 'dist/**', server: { baseDir: ['dist', 'src', 'public'], routes: { '/node_modules': './node_modules' } } }) }
-
useref文件引用处理
- 依赖gulp-useref插件,可以将html中依赖的js、css根据注释提取并生成到指定的文件中,并替换依赖的资源文件路径为新生成的文件路径
const useref = () => { return src('dist/*.html', { base: 'dist' }) .pipe(plugins.useref({ searchPath: ['dist', '.'] })) .pipe(dest('dist')) }
-
文件压缩
- 使用相关的gulp插件对相应类型的文件进行压缩处理
- gulp-uglify 压缩js文件
- gulp-clean-css 压缩css文件
- gulp-htmlmin 压缩html文件
- 为避免文件读写冲突,可以将压缩后的代码放入另外的文件夹,例如release
const useref = () => { return src('dist/*.html', { base: '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, minifyCSS: true, minifyJS: true, }))) .pipe(dest('release')) }
- 使用相关的gulp插件对相应类型的文件进行压缩处理
-
重新规划构建过程
- 对上述构建任务进行重新整理
- style、script、page可以通过parallel组合成并行处理任务compile
const compile = parallel(style, script, page)
,生成的文件由于需要后续进行useref压缩处理,构建过程中临时存放在temp目录,最终压缩后放入dist目录 - useref需要先进行compile生成临时文件,可以使用series组合成串行处理任务,最终组合成构建任务build
const build = series(clean, parallel(series(compile, useref), image, font, extra))
- 去除不必要exports的任务
- 将对应的构建任务加入package.json的script中
- 完整的gulpfile.js如下
// gulpfile.js const { src, dest, series, parallel, watch } = require('gulp'); const loadPlugins = require('gulp-load-plugins'); const browserSync = require('browser-sync'); const del = require('del'); const plugins = loadPlugins(); const bs = browserSync.create(); const data = { menus: [ { name: 'Home', icon: 'aperture', link: 'index.html' }, { name: 'Features', link: 'features.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 style = () => { return src('src/assets/styles/*.scss', { base: 'src' }) .pipe(plugins.sass({ outputStyle: 'expanded' })) .pipe(dest('temp')) } const script = () => { return src('src/assets/scripts/*.js', { base: 'src' }) .pipe(plugins.babel({ presets: ['@babel/preset-env'] })) .pipe(dest('temp')) } const page = () => { return src('src/**/*.html', { base: 'src' }) .pipe(plugins.swig({ data })) .pipe(dest('temp')) } 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 extra = () => { return src('public/**', { base: 'public' }) .pipe(dest('dist')) } const clean = () => { return del(['dist', 'temp']) } const compile = parallel(style, script, page) const serve = () => { watch('src/assets/styles/*.scss', style) watch('src/assets/scripts/*.js', script) watch('src/**/*.html', page) // watch('src/assets/images/**', image) 开发阶段意义不大 // watch('src/assets/fonts/**', font) 开发阶段意义不大 // watch('public/**', extra) 开发阶段意义不大 watch([ 'src/assets/images/**', 'src/assets/fonts/**', 'public/**', ], bs.reload) bs.init({ notify: false, files: 'temp/**', server: { baseDir: ['temp', 'src', 'public'], routes: { '/node_modules': './node_modules' } } }) } const useref = () => { return src('temp/*.html', { base: 'temp' }) .pipe(plugins.useref({ searchPath: ['temp', '.'] })) .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('dist')) } const build = series(clean, parallel(series(compile, useref), image, font, extra)) const dev = series(compile, serve) module.exports = { clean, build, dev, }
-
-
封装工作流
多个项目中复用gulpfile中的task
-
准备
- 创建项目托管repository(github/gitee等)
- 创建项目目录xxx,并初始化
yarn init -y
- 创建目录结构及文件lib/index.js
-
提取gulpfile
- 将原gulpfile.js中的内容复制到lib/index.js文件中
- 将原package.json中devDependencies中的依赖添加到当前package.json的dependencies中
- yarn安装相应的依赖
-
yarn link
建立软连接 - 进入待构建项目目录,执行
yarn link xxx
,执行相应的命令yarn xxx
,测试当前的封装
-
解决模块中的问题
- 将原gulpfile.js中使用到的data数据,改为读取配置文件xxx.config.js
- 对babel使用的插件模块,使用require引入
-
抽象路径配置
-
将原gulpfile.js中使用到的项目路径,通过config进行配置,并提供默认值,同时替换所有使用位置为config读取
// lib/index.js const { src, dest, series, parallel, watch } = require('gulp'); const loadPlugins = require('gulp-load-plugins'); const browserSync = require('browser-sync'); const del = require('del'); const plugins = loadPlugins(); const bs = browserSync.create(); const cwd = process.cwd() let config = { // default config build: { src: 'src', temp: 'temp', dist: 'dist', public: 'public', paths: { styles: 'assets/styles/*.scss', scripts: 'assets/scripts/*.js', pages: '*.html', images: 'assets/images/**', fonts: 'assets/fonts/**', } } } try { const loaded = require(`${cwd}/pages.config.js`) config = { ...config, ...loaded } } catch(e) {} const style = () => { return src(config.build.paths.styles, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.sass({ outputStyle: 'expanded' })) .pipe(dest(config.build.temp)) } const script = () => { return src(config.build.paths.scripts, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.babel({ presets: [require('@babel/preset-env')] })) .pipe(dest(config.build.temp)) } const page = () => { return src(config.build.paths.pages, { base: config.build.src, cwd: config.build.src }) .pipe(plugins.swig({ data: config.data })) .pipe(dest(config.build.temp)) } 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 extra = () => { 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]) } const compile = parallel(style, script, page) const serve = () => { watch(config.build.paths.styles, { cwd: config.build.src}, style) watch(config.build.paths.scripts, { cwd: config.build.src}, script) watch(config.build.paths.pages, { cwd: config.build.src}, page) // watch('src/assets/images/**', image) 开发阶段意义不大 // watch('src/assets/fonts/**', font) 开发阶段意义不大 // watch('public/**', extra) 开发阶段意义不大 watch([ config.build.paths.images, config.build.paths.fonts, ], { cwd: config.build.src }, bs.reload) watch([ '**', ], { cwd: config.build.public }, bs.reload) bs.init({ notify: false, files: `${config.build.temp}/**`, server: { baseDir: [config.build.temp, 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 }) .pipe(plugins.useref({ searchPath: [config.build.paths.temp, '.'] })) .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(config.build.dist)) } const build = series(clean, parallel(series(compile, useref), image, font, extra)) const dev = series(compile, serve) module.exports = { clean, build, dev, }
-
-
包装Gulp CLI
项目目录下新建bin/xxx.js
package.js中增加bin选项配置对应的文件
bin/xxx.js中,读取process.argv命名行参数数组,并添加gulp执行相关参数
-
require('gulp/bin/gulp')
执行gulp命令#!/usr/bin/env node process.argv = process.argv.concat(['--cwd', process.cwd(), '--gulpfile', require.resolve('..')]) require('gulp/bin/gulp')
-
发布并使用模块
- 执行
yarn publish --registry=https://registry.yarnpkg.com
发布封装的工作流模块到npm - 在待使用的项目中,执行
yarn add xxx --dev
添加相应的依赖 -
xxx build
进行使用
- 执行
-
FIS
由百度前端团队推出的自动化构建工具,内置了很多常用的任务以及devServer,不需要开者自己去配置
- 基本使用
-
yarn add fis3 --dev
安装fis3模块 -
fis3 release
编译,使用-d参数可以指定输出目录fis3 release -d output
- 添加配置文件fis-config.js,通过配置文件可以对fis3编译进行配置
-
- 详细用法
- 参考官方文档http://fis.baidu.com/fis3/docs/beginning/intro.html