简介
自动化构建是前端工程化的一个重要组成部分,将源代码转换为生成代码;这样就可以在开发过程中使用提高效率的语法、规范和标准
比如我们可以通过:
- 使用ECMAScript提高代码效率和质量
- 使用Sass增强css的可编程性
- 利用模板引擎抽象页面中重复的页面
这些在浏览器中是不支持的,所有可以通过自动化构建工具转换这些不被支持的特性
-
示例:通过Sass增强css的可编程性
$body-bg: #000; $body-color: #f00; body { margin: 0 auto; padding: 20px; max-width: 800px; background-color: $body-bg; color: $body-color; }
首先安装sass模块,作为开发依赖
yarn add sass --dev
安装完成可以直接使用node_modules下的sass命令
npx sass
-
将sacc文件转换为css
npx sass scss/main.scss css/style.css
,并且自动添加source-map文件body { margin: 0 auto; padding: 20px; max-width: 800px; background-color: #000; color: #f00; }
可以将命令定义在NPM Scripts中,便于使用,在package.json中添加命令
-
NPM Scripts是实现自动化构建工作流最简单的方式,安装browser-sync模块启动测试服务器运行项目
yarn add browser-sync --dev
"scripts": { "build": "sass scss/main.scss css/style.css", "serve": "browser-sync ." }
yarn serve就可以直接运行我们的项目
我们可以给sass添加--watch参数用来监听文件变化,一旦scss文件发生改变就自动转换为css文件
-
使用npm-run-all模块run-p命令实现多个命令依次执行
yarn add npm-run-all --dev
,同时给browser-sync模块添加--files属性监听项目文件变化,更新浏览器界面,避免手动刷新浏览器"scripts": { "build": "sass scss/main.scss css/style.css --watch", "serve": "browser-sync . --files \"css/*.css\"", "start": "run-p build serve" }
常用自动化构建工具
- Grunt
- Gulp
- FIS
Grunt使用
安装grunt模块
yarn add grunt
添加gruntfile.js文件,作为grunt的入口文件
-
使用
grunt.registerTask
注册任务,通过yarn grunt xxx任务名的方式运行任务// gruntfile.js 入口文件 定义一些需要 Grunt 自动执行的任务 // grunt 对象中提供一些创建任务时会用到的 API module.exports = (grunt) => { grunt.registerTask('foo', 'a sample task', () => { console.log('hello grunt') }) grunt.registerTask('bar', () => { console.log('other task') }) // default 是默认任务名称 第二个参数可以指定此任务的映射任务 通过 grunt 执行时可以省略 // 这里映射的任务会按顺序依次执行,不会同步执行 grunt.registerTask('default', ['foo', 'bar']) // 也可以在任务函数中执行其他任务 grunt.registerTask('run-task', () => { // foo 和 bar 会在当前任务执行完成过后自动依次执行 grunt.task.run('foo', 'bar') console.log('run-task') }) // 由于函数体中需要使用 this,所以这里不能使用箭头函数 grunt.registerTask('async-task', function () { // 异步方式需要使用this.async() 方法创建回调函数 const done = this.async() setTimeout(() => { console.log('async task working~') done() }, 1000) }) }
-
标记任务失败
module.exports = (grunt) => { // 返回false 则意味任务执行失败 grunt.registerTask('bad', () => { console.log('bad working') return false }) grunt.registerTask('foo', 'a sample task', () => { console.log('hello grunt') }) grunt.registerTask('bar', () => { console.log('other task') }) // 如果任务执行失败 则后续不再进行 此时添加--force参数强制执行 grunt.registerTask('default', ['foo', 'bad', 'bar']) // 异步函数中标记当前任务执行失败的方式是为回调函数指定一个 false 的实参 grunt.registerTask('bad-async', function () { const done = this.async() setTimeout(() => { console.log('async task working~') done(false) }, 1000) }) }
-
Grunt配置方法
module.exports = (grunt) => { // grunt.initConfig() 用于为任务添加一些配置选项 grunt.initConfig({ // 键一般对应任务的名称 // 值可以是任意类型的数据 foo: { bar: 'bar', }, }) grunt.registerTask('foo', () => { // 通过grunt.config获取 console.log(grunt.config('foo.bar')) // bar }) }
-
Grunt多目标任务
module.exports = (grunt) => { // 多目标模式,可以让任务根据配置形成多个子任务 grunt.initConfig({ build: { js: 1, css: 2, }, }) grunt.registerMultiTask('build', function () { console.log(`task: build, target: ${this.target}, data: ${this.data}`) }) } grunt.initConfig({ build: { options: { msg: 'task options', }, foo: { options: { msg: 'foo target options', }, }, bar: '456', }, }) grunt.registerMultiTask('build', function () { // options可以获取对应的options 如果子任务有options会被覆盖 console.log(this.options()) })
可以单独运行build:css,也可以一次运行
-
Grunt常用插件
const sass = require('sass') const loadGruntTasks = require('load-grunt-tasks') module.exports = grunt => { grunt.initConfig({ sass: { options: { sourceMap: true, implementation: sass }, main: { files: { 'dist/css/main.css': 'src/scss/main.scss' } } }, babel: { options: { sourceMap: true, presets: ['@babel/preset-env'] }, main: { files: { 'dist/js/app.js': 'src/js/app.js' } } }, watch: { js: { files: ['src/js/*.js'], tasks: ['babel'] }, css: { files: ['src/scss/*.scss'], tasks: ['sass'] } } }) // grunt.loadNpmTasks('grunt-sass') loadGruntTasks(grunt) // 自动加载所有的 grunt 插件中的任务 grunt.registerTask('default', ['sass', 'babel', 'watch']) }
Gulp
-
Gulp基本使用
- yarn init --yes初始化项目
- 安装gulp模块
yarn add gulp --dev
- 创建gulpfile.js文件,gulp的入口文件
// gulp 的任务函数都是异步的 // 可以通过调用回调函数标识任务完成 exports.foo = done => { console.log('foo task working~') done() // 标识任务执行完成 } // default 是默认任务 // 在运行是可以省略任务名参数 exports.default = done => { console.log('default task working~') done() }
-
Gulp组合任务
const { series, parallel } = require('gulp') const task1 = done => { setTimeout(() => { console.log('task1 working~') done() }, 1000) } const task2 = done => { setTimeout(() => { console.log('task2 working~') done() }, 1000) } const task3 = done => { setTimeout(() => { console.log('task3 working~') done() }, 1000) } // 让多个任务按照顺序依次执行 串行任务 部署任务 exports.foo = series(task1, task2, task3) // 让多个任务同时执行 并行任务 编译js、css任务 exports.bar = parallel(task1, task2, task3)
-
Gulp异步任务
const fs = require('fs') exports.callback = done => { console.log('callback task') done() } // 错误优先 一旦出错 就不会在执行后面的任务 exports.callback_error = done => { console.log('callback task') done(new Error('task failed')) } exports.promise = () => { console.log('promise task') return Promise.resolve() } // 失败状态 Gulp认为是失败任务 同样种植后面任务 exports.promise_error = () => { console.log('promise task') return Promise.reject(new Error('task failed')) } const timeout = time => { return new Promise(resolve => { setTimeout(resolve, time) }) } exports.async = async () => { await timeout(1000) console.log('async task') } // 构建系统大都在处理文件 exports.stream = () => { const read = fs.createReadStream('yarn.lock') const write = fs.createWriteStream('a.txt') read.pipe(write) return read } exports.stream1 = done => { const read = fs.createReadStream('yarn.lock') const write = fs.createWriteStream('a.txt') read.pipe(write) read.on('end', () => { done() }) }
-
Gulp构建过程核心工作原理
const fs = require('fs') const { Transform } = require('stream') // 过程 输入 -- 加工 -- 输出 exports.default = () => { // 文件读取流 const readStream = fs.createReadStream('normalize.css') // 文件写入流 // const writeStream = fs.createWriteStream('normalize.bak.css') const writeStream = fs.createWriteStream('normalize.min.css') // 文件转换流 const transformStream = new Transform({ // 核心转换过程 transform: (chunk, encoding, callback) => { const input = chunk.toString() const output = input.replace(/s+/g, '').replace(/\/\*.+?\*\//g, '') // 第一个参数为错误对象 如果没有 返回null callback(null, output) }, }) // 把读取出的文件流导入写入文件流 return readStream .pipe(transformStream) // 转换 .pipe(writeStream) // 写入 }
Gulp的核心工作原理就是输入(读取流) -- 加工(转换流) -- 输出(写入流)
-
Gulp文件操作API
// src方法创建读取流 借助插件提供的转换流实现文件加工 dest方法创建写入流 const { src, dest } = require('gulp') const cleanCSS = require('gulp-clean-css') const rename = require('gulp-rename') exports.default = () => { return src('src/*.css') .pipe(cleanCSS()) // 负责文件加工的转换流 通过插件方式实现 .pipe(rename({ extname: '.min.css' })) // 可以执行多个插件 .pipe(dest('dist')) }
-
Gulp实现自动化工作流
/** * 1.首先yarn add gulp --dev安装gulp * 2.使用src和dest输入流和输出流 此时yarn gulp style 并引入 * 3.使用gulp-sass转换流 转换sass文件 此时 dist文件夹中存在以前未被删除的scss文件 需手动删除 * 4.转换ES6语法 使用插件gulp-babel yarn add gulp-babel @babel/core @babel/preset-env --dev 并引入 * 5.html文件编译 使用gulp-swing yarn add glup-swig --dev 需要定义data完成对EJS语法参数的填充 * 6.因为三个构建动作可并行执行 所以使用parallel提高构建效率 * 7.图片和字体文件转换 yarn add gulp-imagemin --dev 并引入 * 8.至此 src下的文件已经处理完成 需要拷贝public下的文件 * 9.开发体验增强 自动清除dist文件 yarn add del --save 需要先清除再编译 需要串行任务series * 10.自动加载插件 yarn add glup-load-plugins --dev 会自动加载所有插件 所以不用再逐个导入 plugins.xxx使用 * 11.热更新开发服务器 yarn add browser-sync --dev 启动web服务器 * 12.监视文件变化 自动更新浏览器 watch监听文件路径匹配符 并执行对应的任务 * 13.构建过程优化 开发阶段执行的任务 和 不许执行的任务单独分开dev * 14.此时基本构建任务已经完成 但html文件中像node_module/xxx/xx.css 这样的css或js文件并没有拷贝到dist目录 使用useref yarn add gulp-useref --dev 将样式文件和js文件合并到新的文件中供页面使用 * 15.将得到的文件进行压缩处理 yarn add gulp-htmlmin gulp-uglify gulp-clean-css --dev 对不同的文件进行不同处理 需要 gulp-if插件 * 16.保持项目风格 所以需要一个临时文件存储未被useref的文件temp * 17.将需要的任务(clean,build,develop等)放在Npm Scripts中去 并且将temp dist目录添加到git忽略文件中 */ const { src, dest, parallel, series, watch } = require("gulp"); const del = require("del"); const browserSync = require("browser-sync"); const loadPlugins = require("gulp-load-plugins"); const { init } = require("browser-sync"); // 会自动加载所有插件 所以不用再逐个导入 const plugins = loadPlugins(); // 自动创建开发服务器 const bs = browserSync.create(); // html模板文件参数 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 clean = () => { return del(["dist", "temp"]); }; const style = () => { // 需要按照项目的目录输出文件 { base: 'src' } return ( src("src/assets/styles/*.scss", { base: "src" }) .pipe(plugins.sass({ outputStyle: "expanded" })) // 需放入临时目录 .pipe(dest("temp")) // 以流的方式推到浏览器 就不需要监视files: 'dist/**' .pipe(bs.reload({ stream: true })) ); }; // js编译 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 })); }; // html const page = () => { return src("src/*.html", { base: "src" }) .pipe(plugins.swig({ data })) .pipe(dest("temp")) .pipe(bs.reload({ stream: true })); }; // 图片 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 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, port: 2080, // open: false, // 目标文件下的内容发生变化时 自动更新浏览器 // files: 'dist/**', server: { // 先从数组第一个去找 找不到的话依次找下去 baseDir: ["temp", "src", "public"], // 先看routes下的配置 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 compile = parallel(style, script, page); // 上线之前执行的任务 const build = series( clean, parallel(series(compile, useref), image, font, extra) ); const develop = series(compile, serve); module.exports = { clean, build, develop, };
-
封装工作流
如何提取项目当中共同的自动化构建任务(封装自动化构建工作流)在别的项目中直接使用
首先全局引入配置模块,yarn link,然后在需要使用的地方引入yarn link zce-pages
gulpfile.js中直接导出module.exports = require('zce-pages')
创建pages.config.js将不应该在公共模块中出现的文件抽象出来,属于项目使用,不应该添加到公共模块
在模板中获取使用模块的路径const cwd = proces.cwd()
-
创建config默认配置文件,将从pages.config.js文件获取的配置与默认配置整合
let config = {} try { const loadConfig = require(path.join(cwd, '/pages.config.js')) config = Object.assign({}, config, loadConfig) } catch (e) {}
-
需要用到配置的地方,使用config.xx代替
const page = () => { return src("src/*.html", { base: "src" }) .pipe(plugins.swig({ data: config.data })) .pipe(dest("temp")) .pipe(bs.reload({ stream: true })); };
-
抽象路径配置
// 将项目中有用到的目录的地方进行替换 let config = { // default config build: { src: "src", dist: "dist", temp: "temp", public: "public", paths: { styles: "assets/styles/*.scss", scripts: "assets/scripts/*.js", pages: "*.html", images: "assets/images/**", fonts: "assets/fonts/**", }, }, };
-
包装Gulp CLI
包装的目的:可以在使用的项目中不需要gulpfile的同时还能使用
首先删除gulpfile文件,再次运行gulp命令,会提示没有gulpfile文件
可以通过指定gulpfile文件目录设置和当前文件目录使用,
yarn gulp clean --gulpfile .\node_modules\zce-pages\lib\index.js --cwd .
,不过很麻烦可以在zce-pages中提供cli,将gulp-cli包装到zce-pages中
首先在zce-pages项目package.json中添加"bin": "bin/zce-pages.js"
创建zce-pages.js文件,对gulp-cli的调用放到这个文件中,需要添加文件头
#!/usr/bin/env node
node_modeuls/bin/gulp.cmd是gulp的执行文件,主要是这句代码
node "%~dp0\..\gulp\bin\gulp.js" %*
,找到gulp.js文件,执行require('gulp-cli')()
而已,所以在我们的zce-pages.js文件中直接调用gulp.js文件require('gulp/bin/gulp')
即可此时,再次gulp命令即可成功,只是还没有gulpfile文件
No gulpfile found
,此时需要指定gulpfile文件目录-
使用process.argv指定--cwd和--gulpfile文件目录
process.argv.push("--cwd"); process.argv.push(process.cwd()); process.argv.push("--gulpfile"); // 自动找package.json中配置的main目录 process.argv.push(require.resolve("..")); require("gulp/bin/gulp");
此时,将gulp包装到zce-pages模块,其他项目中不安装gulp同时也可以使用gulp-cli命令