前言:博主2020.3正式进军前端,目标高级前端工程师,经验尚浅,文章内容如如有误,欢迎指正。css
自动化构建 并非一个句子,有必要在行文前先说明一下它在本文中的宾语是前端工程。html
随着前端需求和项目的日益复杂,出于提升开发效率、用户体验以及其它工程上的须要,咱们一般会借助不少更高阶的语法或者工具来帮助咱们更快更好的开发、部署一个前端工程。前端
那么如何理解构建以及自动化构建呢?我的观点,简单务实点说,构建就是指项目从源代码到一个能按需运行的工程(开发环境、生产环境)所须要作的全部事情。一般来讲,项目屡次构建的每次构建过程都包含不少任务而且基本一致,按照任何简单机械的重复劳动都应该让机器去完成的思想,咱们应该自动化去完成工程的构建,提升构建效率。node
那么如何实现自动化构建呢?我的理解,前端工程构建其实就是一个任务流,完成了这个任务流中的全部任务即完成了构建。说到任务以及任务流,咱们有必要先好好认识一下它们。webpack
对比于JavaScript的函数,我的对任务是这么分类的:nginx
同步任务和异步任务无须解释,这里说说并行任务和串行任务。任务并行能够用于缩短多个任务的执行时间。由于node是单线程执行的,我认为它并不能缩短多个同步任务并行的执行时间,可是构建过程当中的任务一般都会涉及到IO流,因此大部分任务都是异步任务,IO任务并行能够避免IO阻塞线程执行,进而缩短多个任务的执行时间。git
而任务串行能够保证任务之间的有序执行,好比在开发环境下,咱们确定要先执行编译任务,而后才能执行启动开发服务器的任务。es6
理解了构建过程当中的任务以后,下面再列举一些在平常开发当中,咱们常见到的构建任务。github
任务名 | 任务职责 |
---|---|
Eslint检查 | 统一代码风格 |
babel转换 | ES6 -> ES5 |
typescript转换 | Ts -> Js |
sass转换 | sass -> css |
less转换 | less -> css |
html、css、js压缩 | .html -> .min.html、.css->.min.css、.js->.min.js |
web server | 开发服务器 |
js、css兼容 | 兼容不一样浏览器 |
... | ... |
除了上述表格中列举的任务以外,在不一样的项目中还会有不一样的构建任务,这里再也不一一赘述。上面说到自动化构建其实就是一个任务流,再理解和认识了常见任务以后,咱们再来理一理什么是前端的构建任务流。web
构建是为工程服务的,而工程又是为用户服务的。对应于开发环境和生产环境,前端工程能够分为开发环境工程和生产环境工程,其中开发环境工程为开发者服务,生产环境工程为用户服务。
知足工程使用者的需求是咱们构建工程的终极目的,因此有必要投其所好,根据工程的使用者不一样,完成他所须要的的一连串任务,也就是任务流。这时能够根据构建后工程的目标使用者来划分,把任务流分为开发环境构建任务流和生产环境构建任务流两种。
开发环境构建任务流构建后的工程是为开发者服务的。开发者须要开发调试代码,因此开发环境任务流构建的工程须要实现如下功能:
功能项 | 包含任务 |
---|---|
语法检查 | Eslint |
语法转换 | ES6 -> ES五、Sass -> less、Ts->Js等等 |
模拟生产环境 | web开发服务器:devServer |
易于调试 | sourceMap |
... | ... |
开发者须要不断修改代码查看效果,因此除了知足功能以外,还须要加快构建速度而且自动刷新,以保证良好的使用体验。
优化方式 | 实现方案 |
---|---|
加快构建速度 | devServer热模块替换 |
自动刷新 | devServer 监听源代码 |
... | ... |
关于web开发服务器devServer
使用web开发服务器能够模拟像使用nginx、tomcat等服务器软件同样的线上环境,它在功能以及配置上都与nginx以及tomcat相似, 最简单的配置就是指明资源路径baseUrl以及服务启动ip和端口port便可。在开发环境启动本地服务时,配置代理能够在符合同源策略的状况下解决跨域问题。
开发服务器除了能够模拟线上环境以外,更增强大的一点是它能够监听源代码,实现热部署和自动刷新功能!
生产环境构建任务流构建后的工程是为用户服务的。与开发环境相比,它也须要语法检查以及编译功能,但不须要考虑修改以及调试代码的问题,它关注的是浏览器兼容以及运行速度等问题。
功能项 | 包含任务 |
---|---|
语法检查 | Eslint |
语法转换 | ES6 -> ES五、Sass -> less、Ts->Js等等 |
语法兼容 | 不一样浏览器的js、css语法兼容 |
下载速度 | 资源压缩与合并 |
... | ... |
生产环境的优化除了资源的下载速度以外,还能够从不少方面入手,下面是其中的一些方面以及实现方案。
优化方面 | 实现方式 |
---|---|
下载优化 | treeshaking、代码分割、懒加载 |
运行优化 | 代码上优化性能 |
离线访问 | pwa技术 |
... | ... |
终于把任务以及任务流浅显粗陋的讲完了,接下来咱们先是使用npm scripts来实现简单项目的自动化构建,然后学习一下Gulp工具如何实现复杂项目的自动化构建。
任务流由任务组成,任务由脚本实现。在定义好任务脚本或者安装好任务cli模块以后,咱们只需在package.json的scripts选项中配置一条script,就能够方便地调用任务脚本或者任务模块。对于任务流的npm script定义,咱们能够借助一些能够帮助任务组合的库,这样就能够实现多个任务之间的并行和串行。
这里不得不提一下node_modules/.bin文件夹,咱们在项目中安装的cli模块都会有一个cmd文件出如今这里。当咱们在项目中须要调用这些cli模块时,只需yarn/npx cli模块名的方式就能够很方便的调用这些cli模块。
好的,经过上面的分析以后,咱们接下来展开讲述一下npm scripts如何实现任务以及任务流的构建。
对于单任务的构建,只需配置一条简单的script便可,如如下sass和ES6转换的script示例(package.json):
"scripts": {"sass": "sass scss/main.scss css/style.css","es6": "babel es6 --out-dir es5", }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","sass": "^1.29.0" }复制代码
配置以上scripts以后,咱们就可使用如下命令执行任务:
# sass转换yarn sass# es6转换yarn es6复制代码
这里提一提在执行上述命令以后到最后调用sass和es6编译工具的调用过程:
yarn sass -> sass scss/main.scss css/style.css -> node_modules/.bin/sass.cmd -> node_modules/sass/sass.js -> ...code
yarn es6 -> babel es6 --out-dir es5 -> node_modules/.bin/babel.cmd -> node_modules/@babel\cli\bin\babel.js -> node_modules/@babel\cli\lib\index.js -> ...code
对于任务流的构建,除了准备基本任务以外,咱们还须要考虑这些任务之间是否有序,若是有序咱们借助任务串行实现,若是无序咱们经过任务并行加快构建速度。一般咱们会借助npm-run-all 这个库来实现任务的并行和串行,如如下经过任务并行实现sass转换以及ES6转换的简单示例(package.json):
"scripts": {"sass": "sass scss/main.scss css/style.css","es6": "babel es6 --out-dir es5","build": "run-p sass es6" }, "devDependencies": {"npm-run-all": "^4.1.5","@babel/cli": "^7.12.8","@babel/core": "^7.12.9","sass": "^1.29.0" }复制代码
配置以上scripts以后,咱们就可使用如下命令执行任务:
yarn build复制代码
执行yarn build以后,就能够借助npm-run-all库的nun-p.cmd实现sass和es6任务的并行。对于任务的串行,则经过npm-run-all库的nun-s.cmd实现。
好的,在经过以上两个示例理解了npm script实现构建任务以及任务流以后,咱们接下来经过npm script实现一个简单前端项目的开发环境自动化构建和生产环境的自动化构建。
这个项目很简单,它只包含一个html文件,一个使用了ES6语法js文件以及一个使用了sass语法的样式文件,接下来咱们就用npm script来实现这个简单项目的自动化构建(也即开发环境构建任务流和生产环境构建任务流)。事实上,简单项目的自动化构建就是npm script实现自动化构建的使用场景。
经过上面咱们对开发环境构建任务流的认识,咱们先理一理在这个项目中,开发环境任务流至少应该包含哪些任务:
对于sass和ES6修改源代码后的实时转换,咱们能够经过加上一个watch参数实现。而对于全部这些须要监听变化的文件,咱们则统一放入temp文件夹下(角色比如如nginx和Tomcat的应用存放目录),然后让web开发服务器监听这个temp文件夹下全部文件的变化,一旦变化即重启并刷新浏览器。
好的通过上面任务分析以后,咱们可能会把package.json的scripts以及devDependencies写成以下样子:
"scripts": {"sassDev": "sass scss/main.scss temp/css/style.css --watch","babelDev": "babel es6/script.js --out-dir temp/es5/script.js --watch","copyHtmlDev": "copyfiles index.html temp","serve": "browser-sync temp --files \"temp\"","start": "run-p sassDev babelDev copyHtmlDev serve" }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","browser-sync": "^2.26.13","copyfiles": "^2.4.1","npm-run-all": "^4.1.5","sass": "^1.29.0" }复制代码
经过上面咱们对生产环境构建任务流的认识,咱们先理一理在这个项目中,生产环境任务流应该包含哪些任务:
"scripts": {"sass": "sass scss/main.scss dist/css/style.css","babel": "babel es6 --out-dir dist/es5","copyHtml": "copyfiles index.html dist","build": "run-p sass babel copyHtml" }, "devDependencies": {"@babel/cli": "^7.12.8","@babel/core": "^7.12.9","browser-sync": "^2.26.13","copyfiles": "^2.4.1","npm-run-all": "^4.1.5","sass": "^1.29.0" }复制代码
上述代码实现不全,按道理说,在生产环境下,至少须要作代码的兼容以及压缩。这时咱们就须要找到对应的工具库或者本身实现,另外对于压缩而言至少须要在编译以后完成,因此须要注意多个任务间的关系。思路很简单,博主偷个懒当前就不花时间去实践了,须要时再实现就行。
在介绍Gulp以前,咱们有必要再重申一点。在项目以及构建需求不复杂时,npm scripts就能够知足咱们的构建需求了,无需借助其它工具。
Gulp是一个基于流的自动化构建工具,相比较于Grunt,它的构建速度更快,任务编写也更加简单灵活(Grunt博主没用过也不感兴趣)。要使用Gulp,首先须要在项目根目录下建立一个Gulp入口文件gulpfile.js,而后在这个入口文件中经过暴露函数的方式注册任务。
对于一个工具,其它的很少比比,咱们接下来看看它是怎么实现前端项目的自动化构建的。
对于新版本的Gulp来讲,全部任务都是异步任务,因此任务须要告诉Gulp何时执行结束。如下是gulp异步任务实现的几种方式(关注它们是如何通知Gulp异步任务结束的)。
// 方式1:调用done方法主动通知任务结束exports.foo = done => { console.log('foo task working~') done() // 标识任务执行完成}// 方式2:返回Promise,经过它的resolve/reject方法通知任务结束const timeout = time => { return new Promise(resolve => {setTimeout(resolve, time) }) }// 方式3:返回读取流对象,流完即自动通知任务结束exports.stream = () => { const read = fs.createReadStream('yarn.lock') const write = fs.createWriteStream('a.txt') read.pipe(write) return read }// 更多方式复制代码
并行任务和串行任务能够经过gulp提供的series(串行), parallel(并行)实现。
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) }exports.bar = parallel(task1, task2); // 并行任务barexports.foo = series(task1, task2); // 串行任务foo复制代码
Gulp生态中有不少成熟的gulp任务插件,使用它们能够很好地提升效率,如如下示例:
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插件,那么咱们就须要自定义任务,以下示例:
const fs = require('fs')const { Transform } = require('stream')exports.default = () => { const readStream = fs.createReadStream('normalize.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, ''); callback(null, output); } }) return readStream .pipe(transformStream) // 转换.pipe(writeStream) // 写入}复制代码
gulp只是一个帮助咱们实现自动化构建的工具,思想在上文中已经探讨了不少,并且对比于gulp,我的对后面的webpack更感兴趣,这个示例我就很少作叙述了。
好吧,都是借口,如今已经深夜了,用心地写了那么长,我坦白我熬不住了。对了,若是承认文章内容,点赞收藏鼓励一下吧,每周一篇,后面会有个人更多学习记录哦。
下例是通过我的整理过的,课程中老师使用gulp对开发环境和生产环境自动化构建的示例实现:
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 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() }// css编译 src => tempconst style = () => { return src('src/assets/styles/*.scss', { base: 'src'}) .pipe(plugins.sass({ outputStyle: 'expanded'})) .pipe(dest('temp')) .pipe(bs.reload({ stream: true})) }// js编译 src => tempconst 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模板解析 src => tempconst page = () => { return src('src/*.html', { base: 'src'}) .pipe(plugins.swig({ data, defaults: {cache: false } })) // 防止模板缓存致使页面不能及时更新.pipe(dest('temp')) .pipe(bs.reload({ stream: true})) }// 串行编译、模板解析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,port: 2080,// open: false,// files: 'dist/**',server: { baseDir: ['temp', 'src', 'public'], routes: {'/node_modules': 'node_modules' } } }) }// 开发环境构建流:编译 + 启动开发服务器 src => tempconst develop = series(compile, serve)// 生产环境下清空文件夹const clean = () => { return del(['dist', 'temp']) }// 生产环境js、css、html压缩后构建 temp => distconst useref = () => { return src('temp/*.html', { base: 'temp'}) .pipe(plugins.useref({ searchPath: ['temp', '.'] }))// html js css.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')) }// 生产环境图片压缩后构建 src => distconst image = () => { return src('src/assets/images/**', { base: 'src'}) .pipe(plugins.imagemin()) .pipe(dest('dist')) }// 生产环境字体压缩后构建 src => distconst font = () => { return src('src/assets/fonts/**', { base: 'src'}) .pipe(plugins.imagemin()) .pipe(dest('dist')) }// 生产环境静态资源构建const extra = () => { return src('public/**', { base: 'public'}) .pipe(dest('dist')) }// 上线以前执行的任务 src => (temp =>) => dist const build = series( clean, parallel( series(compile, useref), image, font, extra ) )module.exports = { clean, build, develop }复制代码
本文结束,观众老爷您慢走,欢迎下次光临。