首页 > Js/Jquery

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

发表于2015-07-15 15:54:52| --次阅读| 来源webkfa| 作者js,javascript

摘要:上次简单介绍了下Qbuild的特点和配置,其实实现一个自动化工具并不复杂,往简单里说,无非就是筛选文件和处理文件。但Qbuild的源码也并不少,还是做了不少工作的。1. 引入了插件机制。在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 = {
    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压缩

调用命令行来执行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;
    }
};

代码下载

Qbuild.js 源码+示例代码

写在最后

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

相关文章

猜你喜欢

学到老在线代码浏览器 关闭浏览
友情链接: hao123 360导航 搜狗网址导航 114啦网址导航 博客大全
Copyright © 1999-2014, WEBKFA.COM, All Rights Reserved  京ICP备14034497号-1