上次简单介绍了下Qbuild的特点和配置,其实实现一个自动化工具并不复杂,往简单里说,无非就是筛选文件和处理文件。但Qbuild的源码也并不少,还是做了不少工作的。
下面分别介绍每个功能的使用。
配置文件位于 build-demo/test 目录,下同。t-error.js 实际并不存在,此为演示异常情况。
1 module.exports = { root: "../", concat: { title: "文件合并", dir: "demo/js/src", output: "release/js-concat", list: [ { dir: "a", src: ["t1.js", "t2.js", "t3.js"], dest: "a.js", prefix: "//----------- APPEND TEST (%f.filename%) -----------\n" }, { dir: "b", src: ["t1.js", "t2.js", "t-error.js"], dest: "b.js" }, { //不从父级继承,以/开头直接基于root定义的目录 dir: "/release/js-concat", src: ["a.js", "b.js"], dest: "ab.js" } ] } };
调用命令行来执行js压缩。error.js 演示js代码异常的情况。现在压缩工具一般都带语法检测,可以方便的定位错误信息。
1 module.exports = { dir: "../demo", output: "../release", cmd: { title: "压缩js", //cmd: "java -jar D:\\tools\\compiler.jar --js=%f.fullname% --js_output_file=%f.dest%", cmd: "uglifyjs %f.fullname% -o %f.dest% -c -m", match: "js/*.js", exclude: "js/error.js", before: "//build:%NOW%\n" } };
任务模块(format.js)并不直接执行html和css的格式化,而是调用文本处理模块(replace.js)来执行一些常规替换。
1 module.exports = { dir: "../demo", output: "../release", format: [ { title: "格式化html文件", match: "*.html", exclude: "**.old.html", replace: [ //移除html注释 [/(<!--(?!\[if\s)([^~]|~)*?-->)/gi, ""], //移除无效的空格或换行 [/(<div[^>]*>)[\s\r\n]+(<\/div>)/gi, "$1$2"], //移除多余的换行 [/(\r?\n)(\r?\n)+/g, "$1"], //移除首尾空格 [/^\s+|\s+$/, ""] ] }, { title: "格式化css文件", match: "css/*.css", replace: [ //移除css注释 [/\/\*([^~]|~)*?\*\//g, ""], //移除多余的换行 [/(\r?\n)(\r?\n)+/g, "$1"], //移除首尾空格 [/^\s+|\s+$/, ""] ] } ] };
1 module.exports = { dir: "../demo", output: "../release", copy: [ { title: "同步js数据", match: "js/data/**.js" }, { title: "同步图片", match: "images/**" } ] };
1. 了解文件对象。每个任务流程可以有多个任务对象(如上文的文件格式化和复制),除文件合并较特殊(姑且称之为list模式,传入的对象均有src属性,可以传入多个文件路径,但不支持通配符和正则表达式),其它都一样(暂称为match模式,支持通配符和正则表达式)。list模式下,每个对象是一个文件对象;match模式下每个文件是一个文件对象。下面是它们的属性。
1> match模式
1 { dir, //文件所在目录 destname, //默认文件保存路径 dest, //文件实际保存路径 fullname, //文件完整路径 relname, //相对于 config.dir 的路径 filename, //文件名(带扩展名) name, //文件名(不带扩展名) ext, //文件扩展名 stat, //文件状态(最后访问时间、修改时间、文件大小等) {atime,mtime,size} skip, //是否跳过文件 //仅当启用重命名时 rename, //新文件名称(带扩展名) last_dest //文件上次构建时的保存路径 };
2> list模式
1 { dir, //文件所在目录(for src) destname, //文件保存路径 dest, //同destname fullname, //同destname filename, //文件名(带扩展名) name, //文件名(不带扩展名) ext, //文件扩展名 src, //文件路径列表 skip, //是否跳过文件 //仅对concat.js生效 join, //文件连接字符串 prefix //要在合并文件头部添加的内容(concat.js内部支持,不同于文本模块append.js) };
2. 提供的api,已注册到全局变量,支持直接调用。
1 global.Qbuild = { ROOT, //配置文件所在目录,与config.root不同 ROOT_EXEC, //文件执行路径,即build.js所在路径 config, //配置对象 HOT, //红色输出,用于print和log,下同 GREEN, //绿色输出 YELLOW, //黄色输出 PINK, //粉红色输出 print:function (msg,color), //输出控制台信息,不换行,可指定输出颜色 log:function (msg,color), //输出控制台信息并换行,可指定输出颜色 error:function (msg), //输出错误信息,默认黄色 //注册模块 //type:String|Array|Object // String:模块类型 eg: register("concat",fn|object) // Array: 模块数组 eg: register([module,module],bind) // Object:模块对象 eg: register({type:module},bind) //module:模块方法或对象,当为function时相当于 { exec:fn } ,若type为模块数组或对象,则同bind //bind:文本模块绑定对象(文本模块只在此对象上生效),可以传入一个空对象以注册一个全局文本模块 register: function (type, module, bind), //创建路径筛选正则表达式,将默认匹配路径的结束位置 //pattern: 匹配规则,点、斜杠等会被转义,**表示所有字符,*表示斜杠之外的字符 eg: demo/**.html //isdir: 是否目录,若为true,将匹配路径的起始位置 getPathRegex: function (pattern, isdir), //获取匹配的文件,默认基于config.root //pattern:匹配规则,支持数组 eg:["js/**.js","m/js/**.js"] //ops:可指定扫描目录、输出目录、排除规则、扫描时是否跳过输出目录 eg:{ dir:"demo/",output:"release/",exclude:"**.old.js",skipOutput:true } getFiles: function (pattern, ops) , //获取相对路径,默认相对于config.dir getRelname: function (fullname, rel_dir), //获取不带扩展名的名称 getNameWithoutExt: function (name), //设置文件变更 => map_dest[f.destname.toLowerCase()]={src: f.fullname, dest: f.dest} setChangedFile: function (f), //获取输出路径映射,返回 { map: map_dest, last: map_last_dest } getDestMap: function (), //确保文件夹存在 mkdir: function (dir), //读取文件内容(f[read_key] => f.text),read_key 默认为fullname readFile: function (f, callback, read_key), //保存文件(f.text => f.dest) saveFile: function (f, callback), //简单文本解析,支持属性或函数的连续调用,支持简单参数传递,若参数含小括号,需用@包裹 eg:%Q.formatSize@(f.stat.size,{join:'()'})@% //不支持函数嵌套 eg:path.normalize(path.dirname(f.dest)) //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 //eg:parse_text("%path.dirname(f.dest)%", { dest: "aa/b.js"}); => aa parseText: function (text, f), //执行命令行调用 shell: function (cmd, callback), //运行文本处理模块 runTextModules: function (f, task), //设置检测函数,检查文件是否需要更新 setCheck: function (task, check), //自定义存储操作,文件默认为build.store.json store: { init: function (callback), //读取json数据并解析 get: function (key), set: function (key, value), save: function (callback) //保存json数据到文件 } };
3. 任务处理模块格式
1 module.exports = { //模块类型,即任务属性名称,可以为数组 type:"concat", //可选,任务初始化时触发 //task:任务对象 => config[module.type] init: function (task), //可选,文件预处理函数 check: function (f, task), //可选,任务处理完毕触发(仅对exec有效) after: function (task), //文件处理函数(针对单个文件) exec: function (f, task, callback), //文件处理函数(针对所有文件),exec和process任选其一,process主要针对特殊情况 //task.files :文件对象列表,match模式 //task.list :文件对象列表,list模式 process:function (task, callback) };
关于 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 = { //模块类型,即任务属性名称,可以为数组 type:["before","after"], //可选,任务初始化时触发 //data: 在配置中指定的参数 => task[type] //task: 任务对象 init: function (data, task), //文本处理函数,通过操作f.text实现内容更新 eg:f.text=f.text+"OK!"; //f: 文件对象 //type: 文本模块触发时的类型,比如在内容前后追加文本 f.text=type=="before"?data+f.text:f.text+data; process:function (f, data, task, type) };
5. 模块注册(程序会默认导入指定目录的模块,一般无需手动注册)
1 module.exports = { //注册任务处理模块,基于根目录,默认导入./module/*.js register: "./module/*.js", //另一种注册方式 //注意:此种方式注册的type会覆盖模块默认定义的type (module.type) /*register: { concat: "./module/concat.js", format: "./module/format.js", cmd: "./module/cmd.js", copy: "./module/copy.js", //若处理程序相同,可重用已注册的模块 formatCss:"format" },*/ //要启动的任务,按顺序执行,不支持*,默认运行所有 //run: ["concat", "cmd", "formatCss", "format", "copy"], //注册文本处理模块,基于根目录,默认导入./module/text/*.js //对所有任务均生效(如果模块调用了文本处理) registerText: "./module/text/*.js", //另一种注册方式,同register //registerText: {}, //要执行的文本处理模块(按顺序执行),*表示其它模块,默认运行所有 //runText: ["replace", "before", "after", "*"], //定义任务对象(相当于传给任务模块的参数),名称要和模块定义的type一致 //可以为数组 eg:[{},{}] formatCss: { //此处可以注册仅对本任务生效的文本处理模块 registerText: "./module/text/custom/test.js", //指定运行的文本处理模块及顺序 //runText:[], //传给文本处理模块的参数,和文本模块type对应 //是否需要参数,取决于文本处理模块 before: "", after: "", match:"" } };
1. 任务处理模块
1 /* * copy.js 文件同步模块 * author:devin87@qq.com * update:2015/07/10 16:23 */ var log = Qbuild.log, print = Qbuild.print, mkdir = Qbuild.mkdir, formatSize = Q.formatSize; module.exports = { type: ["copy", "copy0", "copy1"], init: function (task) { //不预加载文件内容,不重命名文件 task.preload = task.rename = false; }, exec: function (f, task, callback) { if (f.skip) { log("跳过:" + f.relname); return Q.fire(callback); } print("复制:" + f.relname, Qbuild.HOT); print(" " + formatSize(f.stat.size)); //确保输出文件夹存在 mkdir(path.dirname(f.dest)); var rs = fs.createReadStream(f.fullname), //创建读取流 ws = fs.createWriteStream(f.dest); //创建写入流 //通过管道来传输流 rs.pipe(ws); rs.on("end", function () { print(" √\n", Qbuild.GREEN); callback(); }); rs.on("error", function () { print(" ×\n", Qbuild.YELLOW); }); } };
1 /* * format.js 文件格式化模块 * author:devin87@qq.com * update:2015/07/10 16:23 */ var log = Qbuild.log, print = Qbuild.print; module.exports = { type: ["format", "format0", "format1"], exec: function (f, task, callback) { if (f.skip) { log("跳过:" + f.relname); return Q.fire(callback); } //log("处理:" + f.relname, Qbuild.HOT); print("处理:" + f.relname, Qbuild.HOT); if (f.rename) print(" => " + f.rename); print("\n"); Qbuild.readFile(f, function () { Qbuild.runTextModules(f, task); Qbuild.saveFile(f, callback); }); } };
2. 文本处理模块
1 /* * replace.js 文本模块:内容替换 * author:devin87@qq.com * update:2015/07/10 16:23 */ module.exports = { type: "replace", process: function (f, data, task, type) { if (!data) return; var text = f.text || ""; Q.makeArray(data).forEach(function (item) { var pattern = item[0], replacement = item[1], flags = item[2]; if (!pattern || typeof replacement != "string") return; var regex = new RegExp(pattern, flags); text = text.replace(regex, replacement); }); f.text = text; } };
如果本文或本项目对您有帮助的话,请不吝点个赞。欢迎交流!