上次简单介绍了下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,以简化插件编写。
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%) ----------- " 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% " 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 [/(<!--(?![ifs)([^~]|~)*?-->)/gi, ""], 15 //移除无效的空格或换行 16 [/(<div[^>]*>)[s ]+(</div>)/gi, "$1$2"], 17 //移除多余的换行 18 [/( ? )( ? )+/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 [/( ? )( ? )+/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|Object 18 // 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为模块数组或对象,则同bind 22 //bind:文本模块绑定对象(文本模块只在此对象上生效),可以传入一个空对象以注册一个全局文本模块 23 register: function (type, module, bind), 24 25 //创建路径筛选正则表达式,将默认匹配路径的结束位置 26 //pattern: 匹配规则,点、斜杠等会被转义,**表示所有字符,*表示斜杠之外的字符 eg: demo/**.html 27 //isdir: 是否目录,若为true,将匹配路径的起始位置 28 getPathRegex: function (pattern, isdir), 29 30 //获取匹配的文件,默认基于config.root 31 //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.dir 35 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 默认为fullname 45 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 MB 52 //eg:parse_text("%path.dirname(f.dest)%", { dest: "aa/b.js"}); => aa 53 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.json 65 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.js var 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 //可选,任务初始化时触发 6 //data: 在配置中指定的参数 => task[type] 7 //task: 任务对象 8 init: function (data, task), 9 10 //文本处理函数,通过操作f.text实现内容更新 eg:f.text=f.text+"OK!"; 11 //f: 文件对象 12 //type: 文本模块触发时的类型,比如在内容前后追加文本 f.text=type=="before"?data+f.text:f.text+data; 13 process:function (f, data, task, type) 14 };
5. 模块注册(程序会默认导入指定目录的模块,一般无需手动注册)
1 module.exports = { 2 //注册任务处理模块,基于根目录,默认导入./module/*.js 3 register: "./module/*.js", 4 5 //另一种注册方式 6 //注意:此种方式注册的type会覆盖模块默认定义的type (module.type) 7 /*register: { 8 concat: "./module/concat.js", 9 format: "./module/format.js", 10 cmd: "./module/cmd.js", 11 copy: "./module/copy.js", 12 13 //若处理程序相同,可重用已注册的模块 14 formatCss:"format" 15 },*/ 16 17 //要启动的任务,按顺序执行,不支持*,默认运行所有 18 //run: ["concat", "cmd", "formatCss", "format", "copy"], 19 20 //注册文本处理模块,基于根目录,默认导入./module/text/*.js 21 //对所有任务均生效(如果模块调用了文本处理) 22 registerText: "./module/text/*.js", 23 24 //另一种注册方式,同register 25 //registerText: {}, 26 27 //要执行的文本处理模块(按顺序执行),*表示其它模块,默认运行所有 28 //runText: ["replace", "before", "after", "*"], 29 30 //定义任务对象(相当于传给任务模块的参数),名称要和模块定义的type一致 31 //可以为数组 eg:[{},{}] 32 formatCss: { 33 //此处可以注册仅对本任务生效的文本处理模块 34 registerText: "./module/text/custom/test.js", 35 36 //指定运行的文本处理模块及顺序 37 //runText:[], 38 39 //传给文本处理模块的参数,和文本模块type对应 40 //是否需要参数,取决于文本处理模块 41 before: "", 42 after: "", 43 44 match:"" 45 } 46 };
模块示例
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(" √ ", Qbuild.GREEN); 40 callback(); 41 }); 42 43 rs.on("error", function () { 44 print(" × ", 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(" "); 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 };
代码下载
写在最后
如果本文或本项目对您有帮助的话,请不吝点个赞。欢迎交流!