你的位置:首页 > Java教程

[Java教程]grunt配置太复杂?使用Qbuild进行文件合并、压缩、格式化等处理


上次简单介绍了下Qbuild的特点和配置,其实实现一个自动化工具并不复杂,往简单里说,无非就是筛选文件和处理文件。但Qbuild的源码也并不少,还是做了不少工作的。

1. 引入了插件机制。在Qbuild中称作模块,分为任务处理模块(如合并、压缩等处理)和文本处理模块(如内容添加和替换等处理),一个任务处理模块可以有多个文本处理模块。任务和文本处理模块均可以按指定的顺序执行,可以指定要执行的模块。每个任务的配置可以继承或覆盖全局配置,既保证了简洁,也保证了灵活。

2. 文件筛选支持通配符(*和**)和正则表达式,支持排除规则。支持基于文件夹定位。支持文件变动检测,跳过未更新的文件,大大提升处理效率。

3. 模块路径和文件夹路径支持绝对路径,支持基于配置文件所在路径(以./开头),支持基于自定义的根目录(以/开头,全局root配置),支持基于程序所在路径( 以|开头)。

4. 支持简单的参数引用和函数调用。eg:以下f为文件对象,仅列出部分属性  f: {dir,dest,fullname,filename:"test.js",name:"test",ext:".js",stat:{size:165346}}
    %Q.formatSize(f.stat.size)%  => Q.formatSize(165346) => 161.47KB
    %f.filename.toUpperCase().replace('.','$&parsed.')%  => TEST.parsed.JS

5. 提供简单易用的api,以简化插件编写。

 

下面分别介绍每个功能的使用。

文件合并

配置文件位于 build-demo/test 目录,下同。t-error.js 实际并不存在,此为演示异常情况。

 1 module.exports = { 2   root: "../", 3  4   concat: { 5     title: "文件合并", 6  7     dir: "demo/js/src", 8     output: "release/js-concat", 9 10     list: [11       {12         dir: "a",13         src: ["t1.js", "t2.js", "t3.js"],14         dest: "a.js",15         prefix: "//----------- APPEND TEST (%f.filename%) -----------\n"16       },17       {18         dir: "b",19         src: ["t1.js", "t2.js", "t-error.js"],20         dest: "b.js"21       },22       {23         //不从父级继承,以/开头直接基于root定义的目录24         dir: "/release/js-concat",25         src: ["a.js", "b.js"],26         dest: "ab.js"27       }28     ]29   }30 };

js压缩

调用命令行来执行js压缩。error.js 演示js代码异常的情况。现在压缩工具一般都带语法检测,可以方便的定位错误信息。

 1 module.exports = { 2   dir: "../demo", 3   output: "../release", 4  5   cmd: { 6     title: "压缩js", 7     //cmd: "java -jar D:\\tools\\compiler.jar --js=%f.fullname% --js_output_file=%f.dest%", 8     cmd: "uglifyjs %f.fullname% -o %f.dest% -c -m", 9 10     match: "js/*.js",11     exclude: "js/error.js",12 13     before: "//build:%NOW%\n"14   }15 };

文件格式化

任务模块(format.js)并不直接执行html和css的格式化,而是调用文本处理模块(replace.js)来执行一些常规替换。

 1 module.exports = { 2   dir: "../demo", 3   output: "../release", 4  5   format: [ 6     { 7       title: "格式化html文件", 8  9       match: "*.html",10       exclude: "**.old.html",11 12       replace: [13         //移除html注释14         [/(<!--(?!\[if\s)([^~]|~)*?-->)/gi, ""],15         //移除无效的空格或换行16         [/(<div[^>]*>)[\s\r\n]+(<\/div>)/gi, "$1$2"],17         //移除多余的换行18         [/(\r?\n)(\r?\n)+/g, "$1"],19         //移除首尾空格20         [/^\s+|\s+$/, ""]21       ]22     },23     {24       title: "格式化css文件",25 26       match: "css/*.css",27 28       replace: [29         //移除css注释30         [/\/\*([^~]|~)*?\*\//g, ""],31         //移除多余的换行32         [/(\r?\n)(\r?\n)+/g, "$1"],33         //移除首尾空格34         [/^\s+|\s+$/, ""]35       ]36     }37   ]38 };

文件同步(复制)

 1 module.exports = { 2   dir: "../demo", 3   output: "../release", 4  5   copy: [ 6     { 7       title: "同步js数据", 8       match: "js/data/**.js" 9     },10     {11       title: "同步图片",12       match: "images/**"13     }14   ]15 };

插件(模块)编写

1. 了解文件对象。每个任务流程可以有多个任务对象(如上文的文件格式化和复制),除文件合并较特殊(姑且称之为list模式,传入的对象均有src属性,可以传入多个文件路径,但不支持通配符和正则表达式),其它都一样(暂称为match模式,支持通配符和正则表达式)。list模式下,每个对象是一个文件对象;match模式下每个文件是一个文件对象。下面是它们的属性。

   1> match模式

 1 { 2   dir,     //文件所在目录 3   destname,  //默认文件保存路径 4   dest,    //文件实际保存路径 5   fullname,  //文件完整路径 6   relname,   //相对于 config.dir 的路径 7   filename,  //文件名(带扩展名) 8   name,    //文件名(不带扩展名) 9   ext,     //文件扩展名10   stat,    //文件状态(最后访问时间、修改时间、文件大小等) {atime,mtime,size}11   12   skip,    //是否跳过文件13   14   //仅当启用重命名时15   rename,   //新文件名称(带扩展名)16   last_dest  //文件上次构建时的保存路径17 };

   2> list模式

 1 { 2   dir,     //文件所在目录(for src) 3   destname,  //文件保存路径 4   dest,    //同destname 5   fullname,  //同destname 6   filename,  //文件名(带扩展名) 7   name,    //文件名(不带扩展名) 8   ext,     //文件扩展名 9   src,     //文件路径列表10   11   skip,    //是否跳过文件12   13   //仅对concat.js生效14   join,    //文件连接字符串15   prefix    //要在合并文件头部添加的内容(concat.js内部支持,不同于文本模块append.js)16 };

2. 提供的api,已注册到全局变量,支持直接调用。

 1 global.Qbuild = { 2   ROOT,    //配置文件所在目录,与config.root不同 3   ROOT_EXEC,  //文件执行路径,即build.js所在路径 4  5   config,   //配置对象 6    7   HOT,     //红色输出,用于print和log,下同 8   GREEN,    //绿色输出 9   YELLOW,   //黄色输出10   PINK,    //粉红色输出11 12   print:function (msg,color), //输出控制台信息,不换行,可指定输出颜色13   log:function (msg,color),  //输出控制台信息并换行,可指定输出颜色14   error:function (msg),    //输出错误信息,默认黄色15 16   //注册模块17   //type:String|Array|Object18   //   String:模块类型 eg: register("concat",fn|object)19   //   Array: 模块数组 eg: register([module,module],bind)20   //   Object:模块对象 eg: register({type:module},bind)21   //module:模块方法或对象,当为function时相当于 { exec:fn } ,若type为模块数组或对象,则同bind22   //bind:文本模块绑定对象(文本模块只在此对象上生效),可以传入一个空对象以注册一个全局文本模块23   register: function (type, module, bind),24 25   //创建路径筛选正则表达式,将默认匹配路径的结束位置26   //pattern: 匹配规则,点、斜杠等会被转义,**表示所有字符,*表示斜杠之外的字符 eg: demo/**.html27   //isdir: 是否目录,若为true,将匹配路径的起始位置28   getPathRegex: function (pattern, isdir),29 30   //获取匹配的文件,默认基于config.root31   //pattern:匹配规则,支持数组 eg:["js/**.js","m/js/**.js"]32   //ops:可指定扫描目录、输出目录、排除规则、扫描时是否跳过输出目录 eg:{ dir:"demo/",output:"release/",exclude:"**.old.js",skipOutput:true }33   getFiles: function (pattern, ops) ,34   //获取相对路径,默认相对于config.dir35   getRelname: function (fullname, rel_dir),36   //获取不带扩展名的名称37   getNameWithoutExt: function (name),38   //设置文件变更 => map_dest[f.destname.toLowerCase()]={src: f.fullname, dest: f.dest}39   setChangedFile: function (f),40   //获取输出路径映射,返回 { map: map_dest, last: map_last_dest }41   getDestMap: function (),42   //确保文件夹存在43   mkdir: function (dir),44   //读取文件内容(f[read_key] => f.text),read_key 默认为fullname45   readFile: function (f, callback, read_key),46   //保存文件(f.text => f.dest)47   saveFile: function (f, callback),48 49   //简单文本解析,支持属性或函数的连续调用,支持简单参数传递,若参数含小括号,需用@包裹 eg:%Q.formatSize@(f.stat.size,{join:'()'})@%50   //不支持函数嵌套 eg:path.normalize(path.dirname(f.dest))51   //eg:parse_text("%f.name.trim().drop@({a:'1,2',b:'(1+2)'})@.toUpperCase()% | %Q.formatSize(f.stat.size).split('M').join(' M')%", { dest: "aa/b.js", name: "b.js", size: 666, stat: { size: 19366544 } }) => B.JS | 18.47 MB52   //eg:parse_text("%path.dirname(f.dest)%", { dest: "aa/b.js"}); => aa53   parseText: function (text, f),54 55   //执行命令行调用56   shell: function (cmd, callback),57 58   //运行文本处理模块59   runTextModules: function (f, task),60 61   //设置检测函数,检查文件是否需要更新62   setCheck: function (task, check),63 64   //自定义存储操作,文件默认为build.store.json65   store: {66     init: function (callback),  //读取json数据并解析67     get: function (key),68     set: function (key, value),69     save: function (callback)  //保存json数据到文件70   }71 };

3. 任务处理模块格式

 1 module.exports = { 2   //模块类型,即任务属性名称,可以为数组 3   type:"concat", 4    5   //可选,任务初始化时触发 6   //task:任务对象 => config[module.type] 7   init: function (task), 8    9   //可选,文件预处理函数10   check: function (f, task),11   12   //可选,任务处理完毕触发(仅对exec有效)13   after: function (task),14   15   //文件处理函数(针对单个文件)16   exec: function (f, task, callback),17   18   //文件处理函数(针对所有文件),exec和process任选其一,process主要针对特殊情况19   //task.files :文件对象列表,match模式20   //task.list :文件对象列表,list模式21   process:function (task, callback)22 };

关于 exec 和 process,可以参看部分源码实现

//转交给 module.process 处理if (module.process) return fire(module.process, module, task, callback);//针对单一的文件或任务处理//Q.Queue为自定义队列对象,详见 build/lib/Q.jsvar queue = new Q.Queue({  tasks: task.files || task.list,  //注入参数索引(exec回调函数所在位置)  injectIndex: 1,  exec: function (f, ok) {    //在检查文件是否需要更新后进行文件处理    after_check(f, function () {      fire(module.exec, module, f, task, ok);    });  },  complete: function () {    log();    log("处理完毕!", GREEN);    log();    fire(module.after, module, task);    fire(callback);  }});

4. 文本处理模块格式

 1 module.exports = { 2   //模块类型,即任务属性名称,可以为数组 3   type:["before","after"], 4    5   //文本处理函数,通过操作f.text实现内容更新 eg:f.text=f.text+"OK!"; 6   //f:  文件对象 7   //data: 在配置中指定的参数 => task[type] 8   //task: 任务对象 9   //type: 文本模块触发时的类型10   process:function (f, data, task, type)11 };

 

模块示例

1. 任务处理模块

 1 /* 2 * copy.js 文件同步模块 3 * author:devin87@qq.com 4 * update:2015/07/10 16:23 5 */ 6 var log = Qbuild.log, 7   print = Qbuild.print, 8   mkdir = Qbuild.mkdir, 9 10   formatSize = Q.formatSize;11 12 module.exports = {13   type: ["copy", "copy0", "copy1"],14 15   init: function (task) {16     //不预加载文件内容,不重命名文件17     task.preload = task.rename = false;18   },19 20   exec: function (f, task, callback) {21     if (f.skip) {22       log("跳过:" + f.relname);23       return Q.fire(callback);24     }25 26     print("复制:" + f.relname, Qbuild.HOT);27     print(" " + formatSize(f.stat.size));28 29     //确保输出文件夹存在30     mkdir(path.dirname(f.dest));31 32     var rs = fs.createReadStream(f.fullname), //创建读取流33       ws = fs.createWriteStream(f.dest);   //创建写入流34 35     //通过管道来传输流36     rs.pipe(ws);37 38     rs.on("end", function () {39       print("  √\n", Qbuild.GREEN);40       callback();41     });42 43     rs.on("error", function () {44       print("  ×\n", Qbuild.YELLOW);45     });46   }47 };

 1 /* 2 * format.js 文件格式化模块 3 * author:devin87@qq.com 4 * update:2015/07/10 16:23 5 */ 6 var log = Qbuild.log, 7   print = Qbuild.print; 8  9 module.exports = {10   type: ["format", "format0", "format1"],11 12   exec: function (f, task, callback) {13     if (f.skip) {14       log("跳过:" + f.relname);15       return Q.fire(callback);16     }17 18     //log("处理:" + f.relname, Qbuild.HOT);19 20     print("处理:" + f.relname, Qbuild.HOT);21     if (f.rename) print(" => " + f.rename);22     print("\n");23 24     Qbuild.readFile(f, function () {25       Qbuild.runTextModules(f, task);26       Qbuild.saveFile(f, callback);27     });28   }29 };

2. 文本处理模块

 1 /* 2 * replace.js 文本模块:内容替换 3 * author:devin87@qq.com 4 * update:2015/07/10 16:23 5 */ 6 module.exports = { 7   type: "replace", 8  9   process: function (f, data, task, type) {10     if (!data) return;11 12     var text = f.text || "";13 14     Q.makeArray(data).forEach(function (item) {15       var pattern = item[0],16         replacement = item[1],17         flags = item[2];18 19       if (!pattern || typeof replacement != "string") return;20 21       var regex = new RegExp(pattern, flags);22       text = text.replace(regex, replacement);23     });24 25     f.text = text;26   }27 };

代码下载

Qbuild.js 源码+示例代码

写在最后

如果本文或本项目对您有帮助的话,请不吝点个赞。欢迎交流!