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%) -----------
"
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 };

代码下载

Qbuild.js 源码+示例代码

写在最后

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

原文地址:https://www.cnblogs.com/devin87/p/Qbuild-doc.html