之所以想写有关前端自动化工具的文章出于以下几个原因:
- 自动化构建工具对于前端开发的重要性:高效、减少重复性操作、各种强大插件的支撑。
- 构建工具的上手使用有一定的成本,其中也有不少坑踩,前端在掌握html/js/css三剑客的同时,还需要了解node.js、npm包管理器、构建工具的配置、语法糖以及插件的使用,也要学会当构建工具的使用日趋复杂庞大的时候如何优雅有效的组织代码,减少在使用工具的时候出现bug的概率。
- 工作中遇到一些grunt相关的常用实例与奇技淫巧可以拿来品玩、解读,有助于更快速上手并定制一套强大的自动化工作方式。
- 同类的构建工具例如gulp、webpack(
严格意义上它应该是模块管理工具,但它依旧可以做一些构建的工作
),甚至是扬言可以摈弃grunt与gulp的npm scripts,它们各有各的可取之处,刷新了我对构建工具的认识。而在我看来,与其争论个孰好孰坏,还不如用上一个自己觉得顺手的、更贴合项目需求的工具库。
自动化构建工具 --- grunt
先说下在没有诞生这些工具之前写前端代码的一些痛点:
- “css写得好费劲啊,那些可复用的样式能不能存在一个变量或函数里直接调用啊”
- “样式里还要记得写上兼容不同浏览器的前缀,ctrl+C/V手好累”
- “更改代码后每次都要按F5来刷新浏览器,如果要进行多台设备的调试,每台设备都要手动刷新下,想想都觉得心累~”
- “代码写完后要借用工具手动合并、压缩最后还要自己再拷贝到产品目录下,每次发布都要进行着重复的操作...”
- “太好了,代码合并后现在页面只有一个script标签了,大幅度减少了请求数,但是却引入了其他页面才会使用到的代码,能不能拆分到它们各自需要的page view里啊...”
- etc...
痛点实在太多,不胜枚举,小点的项目这么手动折腾下无伤大雅,但是到了大中型的程度依旧这么徒手操作,实在不敢想象。为了让前端的工作不那么枯燥,各路好汉纷纷支招,在node的光环照耀下,js的构建工具应运而生,逐渐成为前端生态下必不可少的一环。自动化的构建工具就是要让你在编写前端代码的时候对反复重复
枯燥无聊
的工作 say no。
About Grunt
前面扯了那么多闲话,赶紧介绍今天的主角吧。Grunt,(说实话第一眼看到这个单词我竟然想到的是魔兽争霸里我兽族的大G~
) 为什么要选择用grunt来作为首选的构建工具呢,首先还是因为个人比较熟悉吧,也是用到的第一个构建框架,其次借用下官方说的推荐缘由:
Grunt生态系统非常庞大,并且一直在增长。由于拥有数量庞大的插件可供选择,因此,你可以利用Grunt自动完成任何事,并且花费最少的代价。如果找不到你所需要的插件,那就自己动手创造一个Grunt插件,然后将其发布到npm上吧。
---- from grunt 官网介绍
是的,截止到目前为止grunt的插件数目已经达到5,500多个,拥有了这些插件就好比拥有了一把瑞士军刀,正所谓工欲善其事必先利其器
,有关grunt的基本安装、配置、注册任务、etc..就不在此多做介绍,详情可以参照官网的快速入门指南,让我们看下插件TOP100里,grunt是如何让我们的武器更加锋利无比的。
Grunt的基本套装
grunt自家利器:(grunt官方维护的插件)
包名称 | 说明 |
---|---|
contrib-watch | 监视文件的变化,可以指定发生变化时执行的任务 |
contrib-clean | 清楚指定目录下的文件 |
contrib-jshint | js语法规范提示,可以将规范写入配置文件,对不符合规范的代码予以提示 |
contrib-copy | 拷贝文件到指定目录 |
contrib-uglify | 压缩指定的js代码 |
contrib-concat | 合并指定的js or css代码 |
contrib-cssmin | 压缩指定的css代码 |
contrib-less | 将less文件编译为css |
contrib-htmlmin | 压缩指定的html代码 |
contrib-imagemin | 压缩指定的图片 |
家常必备神器:(常用的第三方插件,配合官方插件效果更佳)
包名称 | 说明 |
---|---|
postcss | css预处理工具,可以实现less or scss or stylus的css预处理器效果,也可以借助其强大的auto-prefix插件来为css代码自动添加兼容性浏览器厂商前缀 |
babel | ES6语法转为ES5 js转换器 |
sync | 类似contrib-copy,但只是拷贝那些被更改过的文件 |
webpack | 强大的模块管理工具,其极具特色的loader功能可以让你在js代码里引入几乎任何类型文件 |
jsdoc | 通过写遵循约定好的语法格式的注释而自动生成文档的grunt插件 |
sails-linker | 将css or js(一个或多个)文件自动插入到页面的指定位置 |
assets-linker | 类似sails-linker,但其配置语法更为简洁 |
browser-sync | 一个支持在多个设备间同步测试与调试的轻量版http开发服务器 |
time-grunt | 可以直观的看到每个grunt task的耗时,可以有效的优化构建工具 |
grunt-cdn | 指定cdn路径,为css、js资源添加cdn路径 |
load-grunt-configs | 可以将注册好的各个grunt task拆分到单独的文件里,在tasks数目比较大的时候能更方便组织与管理 |
load-grunt-tasks | 自动将各个task载入到grunt.loadNpmTasks中,节省代码量 |
grunt全家桶的运用场景
在此,假定你已经掌握如何安装grunt、配置package.json文件、使用grunt插件以及注册grunt task等一系列基本操作,如果还是不太清楚请猛戳 官方介绍。紧接上面介绍的十几款常用的grunt插件,我想从项目的两种模式(开发与产品)里详细的列出它们的使用场景,但在此之前,有必要从一个基础的项目例子讲起,它的目录架构大体长这样:
├── your project
│ ├── Gruntfile.js
│ ├── package.json
│ ├── grunt
│ │ ├── watch.js
│ │ ├── clean.js
│ │ ├── ...
│ ├── assets
│ │ ├── js
│ │ │ ├── index.js
│ │ │ ├── ...
│ │ ├── less
│ │ │ ├── index.less
│ │ │ ├── ...
│ ├── www
│ │ ├── js
│ │ │ ├── index.js
│ │ │ ├── ...
│ │ ├── css
│ │ │ ├── index.css
│ │ │ ├── ...
│ ├── build
| | ├── min
│ │ | ├── js
│ │ │ | ├── index.js
│ │ │ | ├── ...
│ │ | ├── css
│ │ │ | ├── index.css
│ │ │ | ├── ...
其中想特别说明的是,在官网介绍的 Gruntfile.js 文件中,grunt 个插件的配置以及task的载入都是类似下面的方式书写的:
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
src: 'src/<%= pkg.name %>.js',
dest: 'build/<%= pkg.name %>.min.js'
}
}
});
// 加载包含 "uglify" 任务的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
// 默认被执行的任务列表。
grunt.registerTask('default', ['uglify']);
};
这其中只是引入了一个任务(uglify)的插件。想象一下,如果有几十个插件写入,Gruntfile.js 可就没那么好看咯。为了能够单独拆分每个插件到不同文件,分开管理,这里就需要引入 load-grunt-configs
与 load-grunt-tasks
插件,它们分别实现grunt任务拆分到单独文件与自动加载包含对应的grunt任务。在它们的帮助下代码量将极大的减少,并且极大的提高grunt各任务的可维护性。若对比官方的写法,现在的代码可以是类似这般的优雅:
module.exports = fucntion(grunt){
var options = {
config : {
src: "grunt/*.js"
}
};
var configs = require('load-grunt-configs')(grunt, options);
grunt.initConfig(configs);
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
}
将各自的grunt任务写到单独的js文件里,以 watch task 为例,像这样:
module.exports.tasks = {
watch: {
js: {
files: [
'assets/js/**/*.js',
'routes/**/*.js'
],
tasks: ['copy:dev'],
options: {
livereload: true
}
},
less: {
files: ['assets/styles/**/*.less'],
tasks: ['less:dev', 'postcss'],
options: {
livereload: true
}
},
view: {
files: ['templates/**/*'],
options: {
livereload: true
}
}
}
};
把这些文件都放在 grunt 目录下,再在 load-grunt-configs 的 options 配置里指定好grunt目录位置,就可以轻松实现grunt任务写入单独文件。而通过 load-grunt-tasks,我们只需要一行代码:
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
就可以代替如下 n 行!
grunt.loadNpmTasks('grunt-shell');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-recess');
grunt.loadNpmTasks('grunt-sizediff');
grunt.loadNpmTasks('grunt-svgmin');
grunt.loadNpmTasks('grunt-styl');
grunt.loadNpmTasks('grunt-php');
grunt.loadNpmTasks('grunt-eslint');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-bower-requirejs');
...
而另一个可以在 Gruntfile.js 中配合使用的插件 --- time-grunt,它可以非常直观的输出每个grunt task的耗时以便你可以针对某项task做好构建时间的优化,如下所示:
在开发模式下使用grunt
开发模式下的grunt任务主要包括源码预编译、代码修饰、代码规范检查、代码tag的自动注入等,这些如同为你配备了一把全能的瑞士军刀般的体验完全可以解决之前提到的诸多痛点,结合grunt全家桶,下面一一介绍如何配置好一套适用于开发环境下的自动化流程:
首先回到之前的项目目录,可以看到分别有assets、www、build三个包含了类似文件的目录,
- assets 用于存放项目前端代码的源码
- www 里包含了编译、修饰过的、可供本地调试服务器上的网页直接访问的代码与静态资源
- build 则是包含了产品模式下的所有打包过的代码与资源,用于放在cdn服务下
之所以这么划分是为了让grunt的职责与分工更加明确,也方便两种模式下的轻松切换与管理。
进入正题,下面列出的是一套开发模式下常用到的任务列表:
[
'clean',
'less',
'postcss',
'jshint',
'copy',
'asset-linker',
'browserSync',
'watch'
]
以上任务转换为自然语言就是:
- 首先清空目标目录,确保下次再执行grunt任务时清空上次任务生成的文件,写入一个干净的目录下
- 将less(css预编译语言,此处也可以是scss、stylus)编译成css
- 修饰编译好的css(例如简化后的css、通过auto-prefix添加过兼容性前缀的css)
- 检测js代码的规范性,是否有书写有误,是否足够规范
- 将处理好的代码拷贝到目标目录(例如www、build)
- 自动添加link、script标签到html或模板文件下
- 检测指定目录下的文件,如有任何修改,则自动刷新浏览器,修改效果所见即所得
接下来,我们只需要把这一些列任务注册到grunt dev
这个指令下,每个任务按照排列的先后顺序依次执行:
grunt.registerTask('dev', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev',
'browserSync',
'watch'
]);
PS:大部分grunt任务都是支持多线程的,即每个grunt任务下可以同时运行多个子任务,也可以单独只运行某个子任务,像'clean:dev',就运行了clean下的dev子任务。因此这里可以根据环境来分为dev与build
为了更直观的了解grunt任务的子任务,举个栗子就好啦:
module.exports.tasks = {
clean: {
dev: ['www'],
build: ['build-res']
}
};
注:当我们在terminal输入grunt clean
时,默认会执行clean下的所有子任务:dev与build
在上面的例子里通过registerTask注册过的任务集群,我们只要在终端输入grunt dev
,剩下的事就交给工具自行处理即可
在产品模式下使用grunt
在我看来,产品模式较之开发模式显得更为严谨与精简。开发模式讲究的是开发者可以快速的调试与追踪自己的代码以及代码变更产生的所见即所得的效果,为的是更高效、更便捷的完成功能点的开发与测试。而产品模式则要求原来在开发模式下的代码更少出现错误、更小的体积(文件大小)更适于网络传播,不仅如此,产品模式还需要考虑到每次发布版本的时候,通过加入代码的版本号,来保证版本更新的平滑过渡,而接下来,就一步步来介绍如何让grunt为我们处理好这一切:
先献出一份产品模式下的tasks list:
grunt.registerTask('build', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev',
'cdn',
'concat',
'uglify',
'cssmin',
'asset-linker:linkCssProd',
'asset-linker:linkJsProd',
'clean:build',
'copy:build'
]);
可以发现这份列表基本囊括了开发模式下的任务,为此我们可以把这部分共有的task单独注册到一个叫做compileAssets里:
grunt.registerTask('compileAssets', [
'clean:dev',
'less',
'postcss',
'jshint',
'copy:dev',
'asset-linker:linkCssDev',
'asset-linker:linkJsDev'
]);
grunt.registerTask('dev', [
'compileAssets',
'browserSync',
'watch'
]);
grunt.registerTask('build', [
'compileAssets',
'cdn',
'concat',
'uglify',
'cssmin',
'asset-linker:linkCssProd',
'asset-linker:linkJsProd',
'clean:build',
'copy:build'
]);
添加版本号
众所周知,每个项目中的package.json都有一个version的字段来表明项目的版本号,而我们要做的就是把这个版本号添加到相关的任务中:
相关任务
- cdn
- asset-linker
- copy
关于添加版本号的位置,我们可以把版本号添加到文件的末尾处,例如index.1.0.0.js
,但是仔细想下,发布版本时,为了能保证新旧版本的文件可以同时保留到线上,一定会出现一个文件夹下有好多个带版本号的文件(当你保留的版本号比较多的时候),这样很显然不方便整理,为此最明智的选择是把版本号放到根目录下,例如http://your-web-site/1.0.1/index.js
,如此一来一个版本就是一个目录,既美观又方便版本管理,想删掉其中一个版本,只要把整个目录除去掉即可。
gulp 与 npm scripts
本来这篇文章只想介绍grunt的内容,但既然大家都是自动化构建工具,也就不得不把这俩货搬出来聊聊。又因为前一阵子读到一篇《我为何放弃Gulp与Grunt,转投npm scripts》的译文,可谓大开眼界,茅塞顿开,醍醐灌顶,心邻神会,如沐春风,不明觉厉... 既然都写到这了就简单介绍下两者吧
gulp
gulp给我最大的感受就是:
- 配置代码更简洁、更直观
- 基于node.js的streams流工作方式,使其处理任务速度更快
gulp允许你把源文件灌入到管道内,期间可以配置一系列插件对管道内的文件逐一处理,最后输出到目标位置。像是工厂里的流水线一样,gulp直接把上一个流水线任务完成的output作为下一个流水线任务的input,这就意味着相比grunt而言,我们不需要在每个grunt任务里指定这个任务的input与output,这样就节省很多代码,说再啰嗦也敌不过一个赤裸裸的例子摆在你的面前:
Grunt
sass: {
dist: {
options: {
style: 'expanded'
},
files: {
'dist/assets/css/main.css': 'src/styles/main.scss',
}
}
},
autoprefixer: {
dist: {
options: {
browsers: [
'last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'
]
},
src: 'dist/assets/css/main.css',
dest: 'dist/assets/css/main.css'
}
},
grunt.registerTask('styles', ['sass', 'autoprefixer']);
让我们看下同样的配置在Gulp下是怎么实现的:
Gulp
gulp.task('sass', function() {
return sass('src/styles/main.scss', { style: 'expanded' })
.pipe(autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
.pipe(gulp.dest('dist/assets/css'))
});
有木有一种眼前一亮的感觉!这确实会让不少grunt的老玩家会毅然决定跳到gulp圈里。有关Gulp的配置与入门教程,可以参考这篇非常棒的入门文章,以上的代码例子也是引用这篇好文(好学生要注明摘要出处,尊重版权)
--- Getting started with gulp
npm scripts
说实话,看完那篇《我为何放弃Gulp与Grunt,转投npm scripts》,给我最最最形象的感受是,就像听到一个大神说,编辑器我只用Vim,容我拜三下。当然啦,总的来说npm scripts大法很好很强大,也需要一定的成本才能练就,就像文中所说的要使用npm scipts可能还需要学会一些命令行的指令与操作,这更像是高级玩家玩的游戏,一下post出一些文中提到的其强大之处:
npm scripts本身其实是非常强大的。它提供了基于约定的pre与post钩子:
{
name: "npm-scripts-example",
version: "1.0.0",
description: "npm scripts example",
scripts: {
prebuild: "echo I run before the build script",
build: "cross-env NODE_ENV=production webpack",
postbuild: "echo I run after the build script"
}
}
此外,还可以通过在一个脚本中调用另一个脚本来对大的问题进行分解:
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"clean": "rimraf ./dist && mkdir dist",
"prebuild": "npm run clean",
"build": "cross-env NODE_ENV=production webpack"
}
}
如果一个命令很复杂,那还可以调用一个单独的文件:
{
"name": "npm-scripts-example",
"version": "1.0.0",
"description": "npm scripts example",
"scripts": {
"build": "node build.js"
}
}
总结
学会使用恰当的工具来解决问题一定会是一件大快人心的事,也会让工作变得更有趣、更具可玩性。文中提到的三种自动化构建工具基本是前端工程化工作中必不可少的需要掌握的除js、css、html外的工作技巧。grunt有其庞大的插件在背后支持,可以通过大量组合来支撑更为复杂的构建工作。gulp更符合小而美,快而精,less is more的准则,github上获得不少的点赞(比grunt多好多!),算是后起之秀。而npm scripts则脱离了一层不必要的抽象,且不需要像grunt和gulp要依赖与其插件作者的维护,直接通过npm的指令即可完成大部分构建工作,为自动化构建流程提供了一种新的思路,有一种返璞归真的意思。所以,具体真的要选择哪一种作为工作主打的工具,还是那句话,就用你觉得顺手的那个好啦~