插件的目的
最近项目前端用的是yeoman
来进行压缩,合并等,使用yeoman
的时候,需要静态页里配置构建块,yeoman
根据配置好的内容动态生成合并,压缩用的配置文件.
比如下面这段代码 html <!-- build:js({.tmp,source}) js/json.js --> <script src="lib/json/json2.js"></script> <!-- endbuild -->
<!-- build
与<!-- endbuild -->
之间的都是需要合并的文件,合并之后的文件名指定在build
后面,这里是js/json.js
.
现在我们组内觉的这种方式不是很灵活,因为这个静态页面需要经常更新,开发的时候,经常需要引用js,所以最后出了一个方案,静态页面只需要引用几个入口js文件,项目用到的js可以放到入口内,通过document.write
方式引入,最后项目构建的时候,动态的合并,压缩这些js,生成跟入口js同名的js,不过为了处理浏览器端cache,所以会对文件进行md5处理生成唯一编号放到文件名中.像这样的a.123123.js
.
插件的实现原理
- 需要一个任务来找出入口js文件内包含的js,并生成合并,压缩需要的grunt 配置文件
- 替换
rev
插件生成的新的文件名字,防止浏览器cache问题
开始实现插件
grunt 提供了一个插件生成模板,可以减少许多创建约定好的文件的时间,可以看这里,grunt插件模板,以下是一个常用的grunt插件的目录结构.
- 根目录
- tasks,里面存放插件核心文件
- node-modules ,放插件依赖的node模块
- Gruntfile.js, 存放插件运行的一些配置,一般测试用
- package.json, 存放描述插件相关的json文件
以下是完整的插件代码
下面的代码,包括两个任务,一个用来生成合并,压缩的配置文件;一个用来替换静态页中的js文件名,目前只实现了js文件的处理,css的差不多,读者可自行实现.
'use strict';
var util = require('util');
var fs = require('fs');
var inspect = function (obj) {
return util.inspect(obj, false, 4, true);
};
module.exports = function(grunt) {
// 先获取动态js列表
var srcList = [];
var _ = grunt.util._;
var reg = /<script src=\\?\"([^\"\\]+)\\?\">/gi;
var regReplace = /(<script src=\\?\")([^\"\\]+)(\\?\">)/gi;
// 获取准备前的配置文件
var smartisan = grunt.config('buildpjPrepare');
grunt.registerMultiTask('buildpjPrepare', 'build front-end program', function() {
// Merge task-specific and/or target-specific options with these defaults.
var options = this.options();
// Iterate over all specified file groups.
// 首先检查文件是否存在
var src;
var filepath = options.url;
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
} else {
src = grunt.file.read(filepath);
}
src.replace(reg, function ($0, $1) {
// $0: 某一模板, $1: key, $2: value
srcList.push({
name: $1,
content: grunt.file.read(smartisan.app + '/' + $1)
});
});
var concatFiles = [], uglifyFiles = [];
_.each(srcList, function(v, k){
console.log(v, k);
var srcFiles = [];
v.content.replace(reg, function ($0, $1) {
// $0: 某一模板, $1: key, $2: value
srcFiles.push(smartisan.app + '/' + $1);
});
// 增加合并配置文件
concatFiles.push({
src: srcFiles,
dest: '.temp/' + v.name
});
// 增加压缩配置文件
uglifyFiles.push({
src: '.temp/' + v.name,
dest: smartisan.dist + '/' + v.name
});
});
grunt.config('concat', {
foo: {
files: concatFiles
}
});
grunt.config('uglify', {
foo: {
files: uglifyFiles
}
})
var cfgNames = ['concat', 'uglify'];
grunt.log.subhead('Configuration is now:');
_.each(cfgNames, function(name) {
grunt.log.subhead(' ' + name + ':').writeln(' ' + inspect(grunt.config(name)));
});
});
grunt.registerMultiTask('buildpj', 'build front-end program', function() {
var options = this.options();
// 首先检查文件是否存在
var src;
var filepath = options.url;
if (!grunt.file.exists(filepath)) {
grunt.log.warn('Source file "' + filepath + '" not found.');
} else {
src = grunt.file.read(filepath);
}
// 获取所有md5之后的名称与之前的对应关系
_.each(srcList, function(v, k){
var arg = v.name.split('/');
var dir = smartisan.dist + '/' + arg[0];
var result = fs.readdirSync(dir);
_.each(result, function(_v, _k){
var fileNameArgOld = arg[1].split('.');
var fileNameArgNew = _v.split('.');
fileNameArgOld.splice(fileNameArgOld.length-1, 1);
fileNameArgOld.push(fileNameArgNew[fileNameArgNew.length-2]);
fileNameArgOld.push(fileNameArgNew[fileNameArgNew.length-1]);
if(fileNameArgOld.join('.') == _v){
// 则找到一个替换后的文件名了
srcList[k].newName = arg[0] + '/' + _v;
}
});
});
// 替换内容中的压缩之后的并md5处理的名称
src = src.replace(regReplace, function ($0, $1, $2, $3) {
$2 = _.filter(srcList, function(v, k){
return v.name == $2;
})[0].newName;
return $1 + $2 + $3;
});
// 开始写新的文件
var fd = fs.openSync(filepath, 'w');
fs.writeSync(fd, src, 0);
fs.closeSync(fd);
});
};
附上一个插件配置
// 全局设置
smartisan: {
app: 'source',
dist: 'dist'
},
buildpjPrepare: {
build: {
options: {
url: '<%= smartisan.app %>/index.html'
}
},
app: '<%= smartisan.app %>',
dist: '<%= smartisan.dist %>',
},
buildpj: {
build: {
options: {
url: '<%= smartisan.dist %>/index.html'
}
}
}
总结
感觉grunt是一个非常不错的构建工具,可用的模块非常多,也许我现在写的插件,就有别的实现,权当练手用.