• 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 源码+示例代码

    写在最后

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

  • 相关阅读:
    SAP BDC批量导入数据(转)
    如何快速从BSEG读取数据(转)
    刷新ALV定位到当前记录行
    一些常用的系统变量(SYST)
    javascript实例
    selenium webDriver属性
    获取豆瓣电影数据
    新浪微博超话题签到demo
    java读取XML文件的四种方法总结(必看篇)
    StringReader分析
  • 原文地址:https://www.cnblogs.com/devin87/p/Qbuild-doc.html
Copyright © 2020-2023  润新知