这是我们部门前端同学cobish的学习笔记,笔者编辑了一下并分享给大家。
在使用 Grunt 之前,项目静态文件几乎没进行压缩合并便直接放到线上,部分文件手动复制粘贴到某压缩网站进行压缩。没压缩合并的文件显然耗资源,手动压缩的文件后期不易维护,每修改一次便要重复复制粘贴,很不方便。Grunt 的加入帮忙解决了以上问题,让开发人员更加专注于开发。这里有一篇「Grunt教程——安装Grunt」很好地教会我们如何搭建 Grunt 环境。
「官网」的入门文档写得很详细,建议阅读并动手一遍。 网上有人会纠结该用 Grunt 还是 glup。个人认为,其实无论是 Grunt 还是 glup 都是构建工具,基本的功能都差不多,与其浪费时间纠结该使用哪个,还不如先开始选择一个使用,等过段时间熟悉后再考虑是否接触另一个,最后再比较出哪个更适合自己岂不更好。
合并压缩静态资源
我开始使用 Grunt 的时候只是用来对 css,js 文件进行合并压缩,使用到的插件分别如下:
代码语言:txt复制"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-cssmin": "^0.14.0",
"grunt-contrib-uglify": "^0.10.0",
"grunt-contrib-watch": "^0.6.1"
}
我先通过 watch 监控静态文件,一旦文件有改动并保存,便用 concat 把 css 或 js 目录下的文件进行了合并,再用 cssmin 或 uglify 把刚刚合并的文件压缩,最后用 clean 把合并但未压缩的文件删除掉。部分代码(以 js 为例)如下:
代码语言:txt复制// 文件合并
concat: {
js: {
files: {
'dest/js/index.js': ['src/js/index/*.js']
}
}
},
// 压缩js代码
uglify: {
build: {
expand: true,
cwd: 'dest/js',
src: ['**/*.js', '!*.min.js'],
dest: 'dest/js',
ext: '.min.js'
}
},
// 删除多余的文件
clean: {
js: ['dest/js/*js', '!dest/js/*.min.js']
},
// 文件监控
watch: {
js: {
files: 'src/js/**/*.js',
tasks: ['concat:js', 'uglify', 'clean:js']
}
}
Source Map
后来发现 cssmin 和 uglify 其实已包含了合并的功能,于是乎把 concat 和 clean 给移除掉,因为它们功能重复了。代码如下:
代码语言:txt复制// 合并压缩js代码
uglify: {
build: {
files: {
'dest/js/index.min.js': ['src/js/index/*.js']
}
}
}
是的,用了 cssmin 和 uglify 后在浏览器的调试工具下便无法定位到源代码处,这是有办法解决的。办法就是使用 source map,chrome 和 firefox 的调试工具都支持,具体详情请移步「JavaScript Source Map 详解」。
grunt-newer
使用了 cssmin 和 uglify 之初项目还不算大的时候,你也许已经发现了一个现象。那就是我们每次一修改保存文件的时候,watch 插件便会立马调用 cssmin 和 uglify。而它们在配置里是对所有的 css 和 js 文件进行操作,虽然只对其中一个文件修改,但是目录下的所有文件都会大动干戈地进行合并压缩。配置高的电脑还行,配置低的电脑就悲剧了,至少我试过每次一保存文件都要等待个两三秒钟后合并压缩完成才能去刷新浏览器。一旦静态文件多起来,那这等待的时候只会增多不会减少。后来我找到了「grunt-newer」这个插件来缓解燃眉之急。newer 只会对改动的文件进行操作,这样至少不会每次保存都对全部文件进行操作。它的使用方法很简单:
代码语言:txt复制// 监控
watch: {
js: {
files: 'src/js/**/*.js',
tasks: ['newer:uglify']
}
}
引入资源
以上便是我目前用于项目的阶段,而此时我做进行开发的项目中主要用了类似于 thinkPHP 的框架,于是添加 css 或 js 外部文件是在 php 代码里添加,如下:
代码语言:txt复制<?php
$this->addMoreCss('dest/index.min.css');
$this->addMoreJs('dest/index.min.js');
?>
这样虽然开发使用到的文件跟上线的文件一致,但也有一些弊端,比如每次改动保存静态文件便会去执行合并压缩代码,我们每天都在时时刻刻地用 ctrl s,这是没有必要的。我们应该只在准备发版上线的时候才去合并压缩。但这时如果在开发时使用原始文件则会是这样:
代码语言:txt复制<?php
$this->addMoreCss('src/index/test1.css');
$this->addMoreCss('src/index/test2.css');
$this->addMoreCss('src/index/test3.css');
$this->addMoreJs('src/index/test1.js');
$this->addMoreJs('src/index/test2.js');
?>
上面一段代码在上线时是需要注释掉的,那在修复时又要重新打开这份代码,注释掉上面上线使用的代码。如果涉及到多个页面的修改,那得手动打开很多份类似这样的代码,而在修复完成后又得重新重复地进行注释和打开上线代码。万一有哪一段代码没看见忘了就不好了。
接下来
所以接下来我打算在 Grunt 中使用「grunt-contrib-sass」和「grunt-contrib-requirejs」,这样在 php 函数都只需要引入一个入口文件,然后 sass 通过 import,requirejs 通过 require 便可去加载它们需要的文件。具体结果得等我实践后才知道,但我相信如果 ok 的话我便可以移除 cssmin 和 uglify 两个插件,因为 Sass 和 requirejs 也有合并压缩的功能。
代码语言:txt复制<?php
// 开发
$this->addMoreCss('src/main.css');
$this->addMoreJs('src/main.js');
// 上线
$this->addMoreCss('dest/main.css');
$this->addMoreJs('dest/main.js');
?>
添加版本号
为了上线之后用户能使用到最新的静态资源,大部分人会使用添加时间戳来清掉缓存,类似于下面这样的代码。读过张云龙的「大公司里怎样开发和部署前端代码」,意识这种方法有几个弊端。一则是每次修改一下时间戳全部的静态资源都会重新被下载一次,没有修改过的文件又重新下载一遍明显是一种浪费。二则是这种方法是一种覆盖式发布,无论先部署页面还是先部署静态资源,期间都可能有用户访问到页面,都有可能造成了页面显示错乱问题,所以需要一种非覆盖式的发布方法来避免这种情况。
代码语言:txt复制<!-- css -->
<link rel="stylesheet" type="text/css" href="index.css?t=20160121" />
<!-- js -->
<script type="text/javascript" src="index.js?t=20160121"></script>
总结上诉理论,此刻我们需要一种非覆盖式发布的方法,而此时这种方法就是将静态资源的内容hash后修改其文件名,做到文件名不同从而起到类似于时间戳的作用。如以下静态资源hash后的文件名发生的变化:
代码语言:txt复制css/index.css -> css/index.aa59f6ab.css
img/demo.png -> img/demo.aa59f6ac.png
接下要怎么实现以上方法呢?要用的工具是 Grunt,使用到的插件如下:
代码语言:txt复制"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-clean": "^1.0.0",
"grunt-contrib-copy": "^1.0.0",
"grunt-filerev": "^2.3.1",
"grunt-usemin": "^3.1.1"
}
这里暂时不涉及到 js 文件,处理 js 文件跟处理 css 文件类似。使用了「grunt-filerev」便可以很轻松地生成 hash 戳后的静态文件。
代码语言:txt复制// 静态文件hash
filerev: {
img: {
src: 'src/img/**/*.png',
dest: 'dest/img/'
},
css: {
src: 'src/css/**/*.css',
dest: 'dest/css/'
}
}
静态文件生成后便可以使用「grunt-usemin」对使用到这些静态文件的文件里进行文件名替换,改成hash后的静态文件名。
代码语言:txt复制// 替换
usemin: {
options: {
assetsDirs: [
'dest',
'dest/img',
'dest/css'
]
},
css: 'dest/css/**/*.css',
html: 'dest/html/**/*.html'
}
以下的步骤都会避免修改到源文件。具体步骤则是先将图片 hash 后放置于 dest 目录(发布目录)。然后将 css 代码都复制到一个tmp目录(临时目录),替换里面变更的图片名字,再将 css 文件 hash 后放置于 dest 目录。接着将 html 代码复制到 dest 目录,替换里面引用到的图片和 css 文件名。最后将 tmp 目录删除。具体代码实现如下:
代码语言:txt复制// 步骤一:对图片进行处理
grunt.registerTask('img', [
'filerev:img'
]);
// 步骤二:对css进行处理
grunt.registerTask('css', [
'copy:css',
'usemin:css',
'filerev:css'
]);
// 步骤三:对html进行处理
grunt.registerTask('html', [
'copy:html',
'usemin:html',
'clean:tmp'
]);
未解决的问题:如上代码,我把它分成了三份分别按步骤运行,但是放在一个任务里却会遇到问题,比如css里的图片名称没有被替换等。如哪位朋友有解决办法,不妨传授我一下,感激!
基于 Grunt 的前端构建
继续对 Grunt 进行探索研究,例子参考「grunt-project」。这一次不再使用 php 进行 include 静态文件,而是在 html 里面进行 include。然后主要将 Grunt 用于两个大的方向,一个是用于开发期间,一个用于上线前期打包。使用到的插件可能有些更换。具体目录如下,src 目录用于开发与维护,dist 目录是打包后的项目,用于上线:
代码语言:txt复制├─ dist/
├─ css/
├─ images/
├─ js/
└─ view/
└─ src/
├─ css/
├─ images/
├─ js/
├─ sass/
└─ view/
在开发期间,使用到的 Grunt 插件如下,watch 插件用了监听文件,一旦文件被修改,可以让它触发浏览器自动刷新:
代码语言:txt复制"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-jshint": "^0.12.0",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-watch": "^0.6.1"
}
图片不需要压缩,css 使用 sass 编译,js 使用了 requirejs,并使用 jshint 进行检错。其中 sass 编译好后会在同一目录下生成对应的 css 目录与文件。jshint 的具体配置参考「例子」。
代码语言:txt复制sass: {
dev: {
options: {
style: 'expanded'
},
files: [{
expand: true,
cwd: 'src/sass/',
src: ['**/*.scss'],
dest: 'src/css/',
ext: '.css'
}]
}
},
jshint: {
options: {
curly: true,
newcap: true,
eqeqeq: true
// ...
},
files: {
src: ['Gruntfile.js', 'src/**/*.js']
}
}
在开发结束后,接下来就是让项目上线了,于是就有了打包项目的过程。看过张云龙博客里讲的「大公司里怎样开发和部署前端代码?」,于是便有了非覆盖式发布和静态文件hash,用到了「grunt-filerev」和「grunt-usemin」这两个插件。网上有很多教程都是图片、css、js 文件同一时间进行 hash,但我觉得这样不妥,毕竟 css(js)代码里引用到了图片,得先图片进行 hash 后替换了 css(js)里引用的路径,然后再对 css(js)进行hash才能保证哪些文件是修改过的。
打包分四个步骤。按顺序分别是图片的打包、css 文件的打包、js 文件的打包、html 文件的打包。使用到的插件如下:
代码语言:txt复制"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-copy": "^0.8.2",
"grunt-contrib-cssmin": "^0.14.0",
"grunt-contrib-htmlmin": "^0.6.0",
"grunt-contrib-imagemin": "^1.0.0",
"grunt-contrib-jshint": "^0.12.0",
"grunt-contrib-requirejs": "^0.4.4",
"grunt-contrib-sass": "^0.9.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-css-sprite": "^0.2.2",
"grunt-filerev": "^2.3.1",
"grunt-include-replace": "^3.2.0",
"grunt-newer": "^1.1.1",
"grunt-replace": "^0.11.0",
"grunt-usemin": "^3.1.1",
"load-grunt-tasks": "^3.3.0",
"time-grunt": "^1.2.1"
}
首先得将 dist 目录给删除掉,因为是非覆盖式部署,所以删掉一些过期用不到的静态文件。第一个步骤是图片打包,将需要合并的图片合并了(并修改对应的 css 文件)放置于临时目录(tmp),不需要合并的图片则复制粘贴到临时目录(tmp)。然后对临时目录里的图片进行压缩,最后 hash 后放置于 dist 生产环境目录。
代码语言:txt复制// 步骤一:对图片进行打包
grunt.registerTask('img', [
'clean:dist',
'sprite',
'copy:images',
'imagemin',
'filerev:img'
]);
第二个步骤是 css 文件的打包,先用 sass 将 css 压缩到临时目录(tmp)中,接着用 usemin 替换掉里面的已经 hash 的图片资源,最后将 css 文件进行 hash 后放置于 dist 生产环境目录。
代码语言:txt复制// 步骤二:对css进行打包
grunt.registerTask('css', [
'sass:dist',
'usemin:css',
'filerev:css'
]);
第三个步骤是 js 文件的打包,用的是 requirejs 插件将 js 文件合并压缩到临时目录(tmp),然后替换掉文件里的图片资源路径,最后 hash 到生产环境目录(dist),并把不需要 hash 的第三方库复制到 dist 生产环境目录。
代码语言:txt复制// 步骤三:对js进行打包
grunt.registerTask('js', [
'requirejs',
'usemin:js',
'filerev:js',
'copy:js'
]);
第四个步骤则是 html 文件的打包,先用 grunt-replace 把里面的 php include 替换成特定的模式放置于临时目录(tmp),然后再用 grunt-include-replace 把 html 依赖的 html 片段复制粘贴到一个 html 中,紧接着替换到 html 中的已 hash 的静态文件(包括css,js,image),最后将 html 压缩至 dist 目录下。
代码语言:txt复制// 步骤四:对html进行打包
grunt.registerTask('html', [
'replace:before',
'includereplace',
'usemin:html',
'replace:after',
'htmlmin',
'clean:tmp'
]);
如果你想问我为什么上面的四个步骤不直接写成一个 task 呢,这是我一直解不开的问题。我试过写成一个 task,后果则是文件里的图片资源路径没能够替换成功,可能是在一个 task 内 usemin 插件无法执行多次,于是我就分类写成四个了。 最后总结一下,以上的方式的好处就在于开发时期不需要去合并压缩文件,方便调试。而生产环境则是尽可能去合并压缩,减少用户的请求时间。