一切重复性的工作都应该被自动化。
在前端工程化中,我们常常希望使用更有的模块进行项目开发,比如我们可能使用模板引擎进行页面渲染,使用sass
来构建css
,使用最新的ECMAScript
语法或者ts
进行业务逻辑开发。而以上的模块或者技能都只是一项选择,最终我们都需要转化为最基本的前端三件套:html
+ css
+ js
。这其中的转换过程根据不同工具的使用而不同,但无疑都是重复性的工作,类似这些开发过程中的重复的工作,都应该使用自动化的思想为我们解决,我们只需要关注代码的业务逻辑和最终的准换结果,而中间的处理过程,都应该交给自动化构建工具。
除了基本的格式转换,自动化构建工具一般还具有压缩、合并、文件整理、启动服务、格式化代码等功能。
本文将会从三款前端自动构建工具grunt 、gulp 、fis 的使用中感受自动化构建的特点和优势。
Grunt的使用 grunt 是比较早的一款前端自动构建工具,插件也比较丰富,目前的使用人数仍然很多,但已被后起之秀gulp 赶超,相比之下,gulp
在使用上更简洁和高效,不过这些都因人而异,grunt
同样是一款优秀的构建工具,我们有必要一探究竟。
快速入门 首先我们初始化一个目录,并新建package.json
文件:
1 2 mkdir guruntDemo npm init -y
安装grunt
模块:
grunt
工具可以看作是任务流的操作逻辑 ,比如html模板解析、sass转为css等自动任务都看作是一项一项的任务,我们在grunt
中注册这些任务,并支持自由组合,然后使用npm run
就能启动这些自动任务。
为了正常启动grunt
任务,默认的入口配置文件为:gruntfile.js
或gruntfile.coffe
。(可以使用大写Gruntfile.js
)。
gruntfile
文件需要默认导出一个参数为grunt
的函数,我们使用grunt.registerTask
方法就可以进行任务的注册:
1 2 3 4 5 6 module .exports = (grunt ) => { grunt.registerTask('html' , '解析html模板' , () => { console .log('渲染html...' ); }); }
执行任务:(如果没有全局安装grunt-cli
,我们需要手动修改package.json
文件的scripts
字段,让其支持npm run
命令)
这样我们就能看到注册的html
任务的执行结果。
使用default
作为默认任务参数时,可以直接使用npm run grunt
进行默认任务执行。
registerTask
函数除了指定函数,一般我们都直接指定任务名即可,这个任务名就是我们已经注册的任务,或者是使用插件注册好的任务:
1 2 3 4 5 6 7 8 9 10 11 module .exports = (grunt ) => { grunt.registerTask('html' , () => { console .log('渲染html...' ); }); grunt.registerTask('sass' , () => { console .log('sass文件转化...' ); }); grunt.registerTask('default' , ['html' , 'sass' ]); }
如果你创建了一个异步任务 ,那么你需要使用this.async()
作为异步结束标识,否则任务将不会执行异步代码。
1 2 3 4 5 6 7 8 grunt.registerTask('asyncTask' , function ( ) { const done = this .async(); setTimeout (function ( ) { console .log('This is a Async Task.' ); done(); }, 1000 ) });
任务失败标记 如果任务内部某些代码已经(或即将)崩溃,可能导致Grunt强行中止。我们可以手动标记该任务为失败任务 ,在任务队列中,这会终止后续任务队列的执行,如果遇到错误任务时需要强制执行需要加上--force
命令。
标记失败任务的方法很简单,我们只需要让任务函数返回false
即可,或者使用失败标记api
:grunt.fail.warn
或者grunt.fail.fatal
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module .exports = (grunt ) => { grunt.registerTask('badTask' , () => { console .log('doing....' ) return false ; }); grunt.registerTask('badTask2' , () => { console .log('doing....' ) grunt.fail.warn('something wrong!' ) console .log('keep going use --force' ) }); grunt.registerTask('badTask3' , () => { console .log('doing....' ) grunt.fail.fatal('something wrong!' ) console .log('can not keep going use --force' ) }); }
注:使用grunt.warn
和grunt.fatal
是一样的效果。
如果是异步函数,也是一样的用法,如果不使用官方api,在done
的参数中传入false
即可。
Grunt的配置config grunt.config
可以从 Gruntfile
中获取针对当前项目的配置数据 。
初始化配置 使用grunt.config.init
或grunt.initConfig
进行配置初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 module .exports = (grunt ) => { grunt.initConfig({ html: { src: 'src/html/*.html' , out: 'dist/html' } }) grunt.registerTask('html' , ()=> { console .log('渲染路径:' , grunt.config('html.src' )) console .log('输出路径:' , grunt.config('html.out' )) }) }
配置的获取和设置 使用grunt,config.get(prop)
可以获取配置的数据。
使用grunt,config.set(prop, value)
可以获取配置的数据。
当然,你可以使用grunt.config(prop [, value])
可以进行设置或者获取操作。
值得注意的是,prop
参数可以传入html.src
这种以.
分割的字符串,这样,需要数据的时候将会逐级查找。
更多配置相关的api
可参考官方说明 。
多目标任务(复合任务) 这里的多目标任务和上面说到的任务队列有所区别。复合任务 是指在不指定目标(target)时,将依次执行其所包含的所有已命名的子属性(sub-properties) (也就是 目标) 。大多数的contrib任务,包括 jshint task 、concat task 和 uglify task 都是复合任务。
多目标任务需要配合配置grunt.config
来使用,就比如我们上面定义的html
任务:
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = (grunt ) => { grunt.initConfig({ html: { src: 'src/html/*.html' , out: 'dist/html' } }) grunt.registerMultiTask('html' , function ( ) { grunt.log.writeln(this .target + ': ' + this .data); }) }
执行yarn grunt html
就可以看到下面的效果:
1 2 3 4 5 6 7 8 9 $ grunt html Running "html:src" (html) task src: src/html/*.html Running "html:out" (html) task out: dist/html Done. Done in 0.42s.
配置项html
中的src
和out
就是多目标任务html
的目标项,将会逐一执行注册的多目标任务函数。
使用this.target
可以获取到当前目前的键,使用this.data
可以获取到当前目标对象配置项的值。
值得注意的一点是,options
是一个特别的配置键,一般用于存储一些通用选项,而不会当做目标指向,我们可以使用this.options
拿到这些数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 module .exports = (grunt ) => { grunt.initConfig({ html: { options: { foo: 'foo' , bar: 'bar' , }, src: { options: { foo: 'src_foo' , }, file: 'src/html/*.html' , }, out: 'dist/html' } }) grunt.registerMultiTask('html' , function ( ) { console .log(this .options()) grunt.log.writeln(this .target + ': ' + this .data); }) }
执行结果:
1 2 3 4 5 6 7 8 9 10 11 $ grunt html Running "html:src" (html) task { foo: 'src_foo', bar: 'bar' } src: [object Object] Running "html:out" (html) task { foo: 'foo', bar: 'bar' } out: dist/html Done. Done in 0.44s.
插件的使用 大多数使用我们都是使用插件执行自动化任务工作的,而很少手动使用registerTask
注册任务。
在grunt
中使用插件,一般有三个步骤:
首先是安装 这个插件模块到本地
在配置数据中更具插件说明配置 相关配置项
使用loadNpmTasks
注册插件提供的任务。
比如我们使用一款文件清理插件:grunt-contrib-clean :
首先,安装:
1 npm i grunt-contrib-clean --dev
然后,配置相关插件配置项,并注册插件任务:
1 2 3 4 5 6 7 module .exports = (grunt ) => { grunt.initConfig({ clean: ['temp/**' , 'dist/html/**' ] }) grunt.loadNpmTasks('grunt-contrib-clean' ); }
点击这里访问插件官方提供的插件列表 。
常用的一些插件 当使用的插件增多时,我们需要多次使用grunt.loadNpmTasks
来注册插件任务,我们可以借助这个插件来自动注册配置项里的插件 。
安装完成后:
1 2 3 4 5 6 7 8 9 10 const loadGruntTasks = require ('load-grunt-tasks' );module .exports = function (grunt ) { grunt.initConfig({ clean: { temp: 'temp/' } }) loadGruntTasks(grunt); }
grunt-sass
需要node-sass
的支持,所以安装时需要多安装一个模块:
1 npm install --save-dev node-sass grunt-sass
使用案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const loadGruntTasks = require ('load-grunt-tasks' );const sass = require ('sass' );module .exports = function (grunt ) { grunt.initConfig({ sass: { options: { implementation: sass, sourceMap: true }, dist: { files: { 'dist/css/main.css' : 'src/sass/main.scss' , 'dist/css/footer.css' : 'src/sass/footer.scss' } } } }) loadGruntTasks(grunt); }
安装:
1 $ yarn add --dev grunt-babel @babel/core @babel/preset-env
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const loadGruntTasks = require ('load-grunt-tasks' );module .exports = function (grunt ) { grunt.initConfig({ babel: { options: { sourceMap: true , presets: ['@babel/preset-env' ] }, dist: { files: { 'dist/js/main.js' : 'src/js/main.js' } } } }) loadGruntTasks(grunt); }
安装:
1 npm install grunt-contrib-watch --save-dev
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const loadGruntTasks = require ('load-grunt-tasks' );const sass = require ('sass' );module .exports = function (grunt ) { grunt.initConfig({ clean: ['dist' ], sass: { options: { implementation: sass, sourceMap: true }, dist: { files: { 'dist/css/main.css' : 'src/sass/main.scss' , 'dist/css/footer.css' : 'src/sass/footer.scss' } } }, babel: { options: { sourceMap: true , presets: ['@babel/preset-env' ] }, dist: { files: { 'dist/js/main.js' : 'src/js/main.js' } } }, watch: { css: { files: ['src/sass/**.scss' ], tasks: ['clean' , 'sass' ], }, scripts: { files: ['src/js/**.js' ], tasks: ['clean' , 'babel' ], options: { spawn: false , }, }, }, }) loadGruntTasks(grunt); }
安装:
1 npm install grunt-contrib-connect --save-dev
结合watch
的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const loadGruntTasks = require ('load-grunt-tasks' );const sass = require ('sass' );module .exports = function (grunt ) { grunt.initConfig({ clean: { css: 'dist/css' , js: 'dist/js' }, sass: { options: { implementation: sass, sourceMap: true }, dist: { files: { 'dist/css/main.css' : 'src/sass/main.scss' , 'dist/css/footer.css' : 'src/sass/footer.scss' } } }, babel: { options: { sourceMap: true , presets: ['@babel/preset-env' ] }, dist: { files: { 'dist/js/main.js' : 'src/js/main.js' } } }, watch: { options: { livereload: true , }, css: { files: ['src/sass/**.scss' ], tasks: ['clean:css' , 'sass' ], }, scripts: { files: ['src/js/**.js' ], tasks: ['clean:js' , 'babel' ], options: { spawn: false , }, }, }, connect: { server: { options: { port: 8000 , open: true , base: 'dist' , index: 'index.html' , livereload: true , } } } }) loadGruntTasks(grunt); grunt.registerTask('default' , ['connect' , 'watch' ]); }
Gulp的使用
gulp 将开发流程中让人痛苦或耗时的任务自动化,从而减少你所浪费的时间、创造更大价值。
使用之下发现,gulp
相较于grunt
来说更灵活易用,grunt是通过注册一个一个的任务来完成自动化操作的,而gulp则是基于流(stream)操作 来实现的。
快速入门 首先安装的gulp
模块:
1 2 npm init -y npm install gulp --dev
创建gulp的入口文件:gulpfile.js
:
1 2 3 4 5 6 7 function aTask ( ) { console .log('this is a task...' ) } module .exports = { aTask }
与grunt
不同的是,我们只需要导出模块函数,这个函数就会被注册为一个任务,我们试着执行一下:
可以看到打印结果:
1 2 3 4 5 6 7 8 9 yarn run v1.22.5 $ gulp aTask [08:26:21] Using gulpfile D:\Test\gulpDemo\gulpfile.js [08:26:21] Starting 'aTask'... this is a task... [08:26:21] The following tasks did not complete: aTask [08:26:21] Did you forget to signal async completion? error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
我们发现,任务正常执行了,但是有报错提示Did you forget to signal async completion?
。这是因为,gulp中的任务都必须是异步的,因此,我们需要返回异步结束标记来标记任务正常结束,我们可以使用任务函数的参数done
来处理,也可以直接返回Promise.resolve
来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 function aTask (done ) { console .log('this is a task...' ) done() } function aTask2 ( ) { console .log('this is a task...' ) return Promise .resolve() } module .exports = { aTask, sTask2 }
这样就能正常执行了。
早期版本注册一个任务有点类似grunt
,我们可以使用require('gulp').task(taskName, taskFun)
来实现,但目前我们只需要直接当做普通函数导出模块即可自动完成注册。
组合任务和异步任务 在grunt
中通过grunt.registerTask
或grunt.registerMultiTask
的api,使用任务列表的模式 进行任务组合,但有一个问题就是异步函数会阻断往往会暂停任务列表的执行,而在gulp
中就显得更加灵活。
在gulp
中,有两种任务组合的方式:队列(同步)执行任务(series)和异步执行任务(parallel)。
队列执行就是,安装排列的任务顺序依次执行,上一个执行结束后再执行下一个任务。而异步执行任务就是,多个任务异步执行,不会相互阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const {series, parallel} = require ('gulp' )function task1 (done ) { console .log('task1 runing...' ) done() } function task2 (done ) { console .log('task2 runing...' ) done() } function task1_async (done ) { setTimeout (function ( ) { console .log('task1_async runing...' ) done() }, 1000 ) } const task_list = series(task1, task1_async, task2) const async_task_list = parallel(task1, task1_async, task2) module .exports = { task_list, async_task_list }
我们知道,gulp中的任务都被定义为异步任务,如过想要任务执行失败错误,可以使用:done(new Error())
或者return Promise.reject()
的形式。
需要注意的是,一旦某个任务执行失败,不管是用series
还是parallel
来组合任务,都会导致任务队列终止运行。
核心工作原理:文件流模式(The Streaming Building System) 正如grunt
的使用过程一样,不管是我们需要编译模板文件,编译sass文件,还是别的什么文件,都是在对文件做处理 ,比如*.scss
输出为*.css
文件。
gulp
的核心工作原理也是这个逻辑:文件流构建模式:**输入流 → 转换流(加工)→ 输出流
**。
输入流就是读取文件内容,输出流就是写入文件,这些操作都可以使用fs
模块实现,而转换流就是内容加工的过程,我们可以借助一个第三方模块:stream
中的Transform
类来实现。
下面我们使用文件流模式来实现压缩css
的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 const fs = require ('fs' )const {Transform} = require ('stream' )const minCss = (done ) => { const read = fs.createReadStream('main.css' ); const transform = new Transform({ transform: function (chunk, encoding, callback ) { const input = chunk.toString(); const output = input .replace(/\s+\{/g , '{' ) .replace(/\{\s+/g , '{' ) .replace(/\s+\}/g , '}' ) .replace(/\}\s+/g , '}' ) .replace(/;\}/g , '}' ) .replace(/\s+:/g , ':' ) .replace(/:\s+/g , ':' ) .replace(/\/\*.+?\*\//g , '' ); callback(null , output); } }) const write = fs.createWriteStream('main.min.css' ) read .pipe(transform) .pipe(write) return read; }; module .exports = { minCss, }
这也是gulp
最核心的文件操作过程。
gulp
中提供了文件流操作的api,这样我们就不需要通过fs
模块来实现了,而gulp
中提供的插件相当于转换流插件,这样,我们就能通过流模式就像简洁高效地开发啦。
gulp
提供的读取流使用src
对象,写入流使用dest
对象,而转换流则是相应的插件来实现。
比如上面我们实现css文件压缩任务,用gulp中的api和插件来实现则是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 const { src, dest } = require ('gulp' )const cleanCss = require ('gulp-clean-css' )const minCss = () => { return src('*.css' ) .pipe(cleanCss()) .pipe(dest('mincss' )) } module .exports = { minCss }
之后的案例基本都是这个不变的模式,区别在于插件的选择而已,所以使用起来十分方便。
使用grunt
的感觉就是在找插件,写配置文件,其实使用gulp
的感觉也是如此,找到适合的插件,写配置,传入pipe
管道中作为转换流调用,虽然在使用上都是在找插件的过程,但gulp
采用的这种流模式更清晰,更高效,也更灵活,我们可以在管道流中自由使用自己想要加工的效果插件。
样式编译 这里以sass
文件为例,编译为css
文件,我们需要使用gulp-sass 这个插件。
在开始之前,我们需要准备好一些测试文件,比如在项目下创建这些样式文件:
1 2 3 4 5 6 └───src ├───assets ├───styles └───_icons.scss └───_variables.scss └───main.scss
下面是最基本的使用:
1 2 3 4 5 6 7 8 9 10 11 12 const {src, dest} = require ('gulp' )const transform_sass = require ('gulp-sass' )const style = () => { return src('src/assets/styles/*.scss' ) .pipe(transform_sass()) .pipe(dest('dist' )) } module .exports = { style }
执行编译命令后,我们发现,在dist
目录下多了一个文件:
我们发现,_icons.scss
和_variables.scss
未编译,这是因为一般来说,我们都把_
开头的样式文件作为其他样式文件的依赖,而不会单独存在,所以没必要编译出来,sass
会自动过滤这些文件。
有时候我们希望保留编译前的目录结构。这需要我们修改一些配置项来改变:
1 2 3 4 5 const style = () => { return src('src/assets/styles/*.scss' , { base : 'src' }) .pipe(transform_sass({ outputStyle : 'expanded' })) .pipe(dest('dist' )) }
脚本编译 有时候我们希望使用最新的js
语法,就需要使用babel
进行编译。或者说我们是用ts
等语言进行开发的,也需要进行编译。
安装gulp-babel :
1 2 3 4 5 # Babel 7 $ npm install --save-dev gulp-babel @babel/core @babel/preset-env # Babel 6 $ npm install --save-dev gulp-babel@7 babel-core babel-preset-env
用法:
1 2 3 4 5 6 7 const _babel = require ('gulp-babel' )const script = () => { return src('src/assets/scripts/*.js' , { base : 'src' }) .pipe(_babel({ presets : ['@babel/preset-env' ] })) .pipe(dest('dist' )) }
如果你使用了ts
进行开发,你需要安装gulp-typescript 模块:
1 yarn add gulp-typescript typescript --dev
用法:
1 2 3 4 5 6 7 const _ts = require ('gulp-typescript' )const script_ts = () => { return src('src/assets/scripts/*.ts' , { base : 'src' }) .pipe(_ts()) .pipe(dest('dist' )) }
模板引擎编译 模板引擎有多种,需要更具你自己使用的模板语法来选择对应的语法。我们使用到的插件是gulp-swig 。
安装:
1 yarn add gulp-swig --dev
用法:
1 2 3 4 5 6 7 const pageData = {}; const page = () => { return src('src/**/*.html' , { base : 'src' }) .pipe(_swig({ data : pageDate })) .pipe(dest('dist' )) }
结合上面的样式编译和脚本编译,就可以组合为一个编译任务:
1 2 3 const { parallel } = require ('gulp' )const compile = parallel(style, script, page)
图片字体转换 对于图片、字体等文件,一般来说不需要转译,只需要压缩等操作即可,我们使用gulp-imagemin 模块。
安装:
1 yarn add gulp-imagemin --dev
使用:
1 2 3 4 5 6 7 8 9 10 11 12 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' )) }
其他文件的处理 比如文件的拷贝、清除等。
这里清除文件可以使用gulp
提供的gulp-clean
,由于清除文件不需要输出文件,这里也可以使用第三方模块,不走文件流模式,这里我们选择del
这个模块。
1 2 3 4 5 6 7 const clean = () => { return _del(['dist' ]) } const extra = () => { return src('public/**' , { base : 'public' }) .pipe(dest('dist' )) }
结合之前写好的编译任务,组合为最终的构建任务build
:
1 2 const compile = parallel(style, script, page, image, font) const build = series(clean, parallel(compile, extra))
到这里,自动构建的基本已经完成,如果有其他需要,可自行选择插件来自动完成你的任务。
自动加载插件 随着我们使用的gulp
插件越来越多,我们每次使用都需要手动导入插件,这样其实很麻烦,在grunt
的使用中,我们使用load-grunt-tasks
为我们自动注册配置里的任务项,同样的,gulp
中也有一个自动加载插件的插件:gulp-load-plugins 。这是非官方插件,目前在gulp插件列表中无法搜索到,这个插件会自动加载你安装的gulp
插件,这样我们拿到这个包含所有插件的对象,就能直接使用了。
整合上文使用到的所有任务:(需要注意的是命名问题,比如gulp-babel
可以使用plugins.babel
获取,而gulp-clean-css
这样的插件需要使用plugins.cleanCss
来获取)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 const {src, dest, parallel, series} = require ('gulp' )const _sass = require ('gulp-sass' )const loadPlugins = require ('gulp-load-plugins' )const _del = require ('del' )const plugins = loadPlugins(); const pageDate = {}; const style = () => { return src('src/assets/styles/*.scss' , { base : 'src' }) .pipe(_sass({ outputStyle : 'expanded' })) .pipe(dest('dist' )) } const script = () => { return src('src/assets/scripts/*.js' , { base : 'src' }) .pipe(plugins.babel({ presets : ['@babel/preset-env' ] })) .pipe(dest('dist' )) } const script_ts = () => { return src('src/assets/scripts/*.ts' , { base : 'src' }) .pipe(plugins.typescript({ noImplicitAny: true , })) .pipe(dest('dist' )) } const page = () => { return src('src/**/*.html' , { base : 'src' }) .pipe(plugins.swig({ data : pageDate })) .pipe(dest('dist' )) } 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 clean = () => { return _del(['dist' ]) } const extra = () => { return src('public/**' , { base : 'public' }) .pipe(dest('dist' )) } const compile = parallel(style, script, page, image, font)const build = series(clean, parallel(compile, extra))module .exports = { clean, compile, build, }
启动服务器 node服务器插件有很多,这里我们可以使用gulp
提供的gulp-live-server ,也可以自由选择。因为不需要使用文件流模式。
使用:
1 2 3 4 5 const server = (done ) => { const live_server = plugins.liveServer.static('dist' , 8080 ); live_server.start(); done(); };
比如也换成使用比较多的:browser-sync 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const server = (done ) => { const browserServer = browserSync.create(); browserServer.init({ notify: false , files: 'dist/**' , port: 3000 , server: { baseDir: "dist" , routes: { "/node_modules" : 'node_modules' } } }); };
文件变化监听 gulp
模块提供了文件监听的函数watch
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const server = (done ) => { const bs = browserSync.create(); 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); bs.init({ notify: false , files: 'dist/**' , open: false , port: 3000 , server: { baseDir: ['dist' , 'src' , 'public' ], routes: { "/node_modules" : 'node_modules' } } }); };
这样,我们启动服务器时就能开启监听,实时编译。
这里有个细节问题,其实监听需要编译文件就可以了,服务器无需再监听dist
文件,我们可以利用bs.reload
重启服务器的方法来改进,类似这样:
1 watch('src/assets/styles/*.scss' , series(style, bs.reload));
文件引用的处理 在html代码中,可能引用了一些node_modules
目录下的css或者js,类似这样:
1 2 3 4 5 <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.css" > <script src="/node_modules/jquery/dist/jquery.js" ></script> <script src="/node_modules/popper.js/dist/umd/popper.js" ></script> <script src="/node_modules/bootstrap/dist/js/bootstrap.js" ></script>
当我们启动服务器时,如果没有配置路由,是无法访问到这个目录下的文件的。
但是,当我们打包上线的时候,node_modules
是不存在的,这样同样无法访问到这些文件,一些办法是我们手动整理这些需要的引用文件,放到要打包的目录下,这样的效率是比较低的,实际上,我们可以使用gulp
提供的一个插件来自动处理:gulp-useref 。
通过一定规则的html注释,这个插件能将注释包裹的文件引用下的资源自动打包到指定的目录下,这样,就避免了引文文件不存在而找不到文件的问题。
html写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <link rel ="stylesheet" href ="/node_modules/bootstrap/dist/css/bootstrap.css" > <link rel ="stylesheet" href ="assets/styles/main.css" > <script src ="/node_modules/jquery/dist/jquery.js" > </script > <script src ="/node_modules/popper.js/dist/umd/popper.js" > </script > <script src ="/node_modules/bootstrap/dist/js/bootstrap.js" > </script > <script src ="assets/scripts/main.js" > </script >
用法:
1 2 3 4 5 const useref = () => { return src('dist/*html' , { base : 'dist' }) .pipe(plugins.useref({ searchPath : ['dist' , '.' ] })) .pipe(dest('release' )) }
注意:上面的示例中我们把最终的文件放到了release
目录下,这样因为如果文件读取流和写入流在同一个文件很可能导致写入失败,而且重写html
文件之后,之前的特殊标记都消失了,需要重新编译才能使用useref
。
但是,使用release
文件之后,其他很多页面静态编译资源都已经存在了dist
目录,所以,当我们使用了useref
,就需要使用两个目录进行文件整理:dist目录
:一般为最终打包上线的目录,temp
目录:缓存目录,开发阶段调试的目录。
首先,useref构建之后的文件是需要上线的,一般我们需要进行压缩代码的操作,但是引文文件包含:css、js还有html,不同的文件类型需要不同的压缩插件,所以我们需要借助gulp-if 进行流判断操作:
1 2 3 4 5 6 7 8 9 10 11 12 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' )) }
下面是整个文件的目录重新整理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 const {src, dest, parallel, series, watch} = require ('gulp' )const _sass = require ('gulp-sass' )const loadPlugins = require ('gulp-load-plugins' )const _del = require ('del' )const browserSync = require ('browser-sync' )const plugins = loadPlugins(); const pageDate = {}; const srcPath = 'src' ; const tempPath = 'temp' ; const distPath = 'dist' ; const clean = () => { return _del([tempPath, distPath]) } const style = () => { return src('src/assets/styles/*.scss' , { base : srcPath }) .pipe(_sass({ outputStyle : 'expanded' })) .pipe(dest(tempPath)) } const script = () => { return src('src/assets/scripts/*.js' , { base : srcPath }) .pipe(plugins.babel({ presets : ['@babel/preset-env' ] })) .pipe(dest(tempPath)) } const script_ts = () => { return src('src/assets/scripts/*.ts' , { base : srcPath }) .pipe(plugins.typescript({ noImplicitAny: true , })) .pipe(dest(tempPath)) } const page = () => { return src('src/**/*.html' , { base : srcPath }) .pipe(plugins.swig({ data : pageDate, cache : false })) .pipe(dest(tempPath)) } const image = () => { return src('src/assets/images/**' , { base : srcPath }) .pipe(plugins.imagemin()) .pipe(dest(distPath)) } const font = () => { return src('src/assets/fonts/**' , { base : srcPath }) .pipe(plugins.imagemin()) .pipe(dest(distPath)) } const extra = () => { return src('public/**' , { base : 'public' }) .pipe(dest(distPath)) } const server = (done ) => { const bs = browserSync.create(); watch('src/assets/styles/*.scss' , series(style, bs.reload)); watch('src/assets/scripts/*.js' , series(script, bs.reload)); watch('src/**/*.html' , series(page, bs.reload)); watch([ 'src/assets/images/**' , 'src/assets/fonts/**' , 'public/**' ], bs.reload); bs.init({ notify: false , open: false , port: 3000 , server: { baseDir: [tempPath, srcPath, 'public' ], routes: { "/node_modules" : 'node_modules' } } }); } const useref = () => { return src('temp/*html' , { base : tempPath }) .pipe(plugins.useref({ searchPath : [tempPath, '.' ] })) .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(distPath)) } const compile = parallel(style, script, page)const develop = series(clean, compile, server)const build = series(clean, parallel(compile, image, font, extra), useref)module .exports = { clean, develop, build, }
为了方便测试,我们一般还需要在package.json
的添加scripts
信息:
1 2 3 4 5 "scripts": { "clean": "gulp clean", "develop": "gulp develop", "build": "gulp build" }
如果需要上传到git,需要忽略自动构建文件夹:dist
和temp
。
封装自动构建工作流脚手架 上面的自动化是比较常用的,我们可以封装为自己的脚手架,便于下次直接使用,我们可以创建一个基于yeoman
之类的脚手架,也可以直接把整个文件放到git
远程仓库。脚手架的好处是支持cli用户个性化设置,本质上也只是文件的复制工作,为了这个模块更通用,我们需要把gulpfile.js
文件进行路径、配置的提取,便于使用者修改配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 const {src, dest, parallel, series, watch} = require ('gulp' )const _del = require ('del' ) const { join } = require ('path' )const loadPlugins = require ('gulp-load-plugins' )const browserSync = require ('browser-sync' ) const plugins = loadPlugins(); const default_userConfig = { build: { srcPath: 'src' , tempPath: 'temp' , distPath: 'dist' , publicPath: 'public' , srcPaths: { styles: 'assets/styles/*.scss' , scripts: 'assets/scripts/*.js' , script_ts: 'assets/scripts/*.ts' , pages: '*.html' , images: 'assets/images/**' , fonts: 'assets/fonts/**' , public: '**' , }, }, pageDate: {}, }; const userConfig = Object .assign({}, default_userConfig, require ('./userConfig' ));const pathBuild = userConfig.build;const srcPaths = pathBuild.srcPaths;const style = () => { return src(srcPaths.styles, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.sass({ outputStyle : 'expanded' })) .pipe(dest(pathBuild.tempPath)) } const script = () => { return src(srcPaths.scripts, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.babel({ presets : ['@babel/preset-env' ] })) .pipe(dest(pathBuild.tempPath)) } const script_ts = () => { return src(srcPaths.script_ts, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.typescript( { noImplicitAny : true })) .pipe(dest(pathBuild.tempPath)) } const page = () => { return src(srcPaths.pages, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.swig({ data : userConfig.pageDate, cache : false })) .pipe(dest(pathBuild.tempPath)) } const image = () => { return src(srcPaths.images, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.imagemin()) .pipe(dest(pathBuild.distPath)) } const font = () => { return src(srcPaths.fonts, { base : pathBuild.srcPath, cwd : pathBuild.srcPath}) .pipe(plugins.imagemin()) .pipe(dest(pathBuild.distPath)) } const extra = () => { return src(srcPaths.public, { base : pathBuild.publicPath, cwd : pathBuild.publicPath}) .pipe(dest(pathBuild.distPath)) } const server = () => { const bs = browserSync.create(); const opts = { cwd : pathBuild.srcPath }; watch(srcPaths.styles, opts, series(style, bs.reload)); watch(srcPaths.scripts, opts, series(script, bs.reload)); watch(srcPaths.script_ts, opts, series(script_ts, bs.reload)); watch(srcPaths.pages, opts, series(page, bs.reload)); watch([ srcPaths.pages, srcPaths.fonts, srcPaths.public, ], opts , bs.reload); bs.init({ notify: false , open: true , port: 3000 , server: { baseDir: [pathBuild.tempPath, pathBuild.srcPath, pathBuild.publicPath], routes: { "/node_modules" : 'node_modules' , } } }); } const clean = () => { return _del([pathBuild.tempPath, pathBuild.distPath]) } const useref = () => { return src(join(pathBuild.tempPath, srcPaths.pages), { base : pathBuild.tempPath}) .pipe(plugins.useref({ searchPath : [pathBuild.tempPath, '.' ] })) .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(pathBuild.distPath)) } const compile = parallel(style, script, script_ts, page)const develop = series(clean, compile, server)const build = series(clean, parallel(compile, image, font, extra), useref)module .exports = { clean, dev: develop, build, }
文件中引用的userConfig
就是为了用户能更好地自定义文件目录,我们可以通过使用脚手架时初始化文件目录,以及这个配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 module .exports = { build: { srcPath: 'src' , tempPath: '.temp' , distPath: 'dist' , publicPath: 'public' , srcPaths: { styles: 'assets/styles/*.scss' , scripts: 'assets/scripts/*.js' , script_ts: 'assets/scripts/*.ts' , pages: '*.html' , images: 'assets/images/**' , fonts: 'assets/fonts/**' , public: '**' , }, }, pageDate: {}, };
如果你使用的是yeoman
搭建的脚手架,可以参考下面的处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const generators = require ('yeoman-generator' );const path = require ('path' )const fs = require ('fs' )module .exports = class extends generators { prompting(){ return this .prompt([{ type: 'input' , name: 'name' , message : 'Your project name' , default : this .appname }]).then(answers => { this .answers = answers; }); } writing() { const tempDir = path.join(__dirname, 'templates' ); const destDir = process.cwd(); const ejsData = { title: this .answers['name' ], success: true }; fs.readdir(tempDir, (err, files ) => { if (err) throw err; files.forEach(temp => { const input = path.join(tempDir, temp); const output = path.join(destDir, temp); this .fs.copyTpl(input, output, ejsData); }) }); } }
Fis的使用 百度团队开发的一款自动化构建工具,有兴趣的可参考:http://fis.baidu.com/ 。