Grunt 插件开发例子

插件的目的

最近项目前端用的是yeoman来进行压缩,合并等,使用yeoman的时候,需要静态页里配置构建块,yeoman根据配置好的内容动态生成合并,压缩用的配置文件.

比如下面这段代码 html <!-- build:js({.tmp,source}) js/json.js --> <script src="lib/json/json2.js"></script> <!-- endbuild -->

<!-- build<!-- endbuild -->之间的都是需要合并的文件,合并之后的文件名指定在build后面,这里是js/json.js.

现在我们组内觉的这种方式不是很灵活,因为这个静态页面需要经常更新,开发的时候,经常需要引用js,所以最后出了一个方案,静态页面只需要引用几个入口js文件,项目用到的js可以放到入口内,通过document.write方式引入,最后项目构建的时候,动态的合并,压缩这些js,生成跟入口js同名的js,不过为了处理浏览器端cache,所以会对文件进行md5处理生成唯一编号放到文件名中.像这样的a.123123.js.


插件的实现原理

  • 需要一个任务来找出入口js文件内包含的js,并生成合并,压缩需要的grunt 配置文件
  • 替换rev插件生成的新的文件名字,防止浏览器cache问题

开始实现插件

grunt 提供了一个插件生成模板,可以减少许多创建约定好的文件的时间,可以看这里,grunt插件模板,以下是一个常用的grunt插件的目录结构.

  • 根目录
    • tasks,里面存放插件核心文件
    • node-modules ,放插件依赖的node模块
    • Gruntfile.js, 存放插件运行的一些配置,一般测试用
    • package.json, 存放描述插件相关的json文件

以下是完整的插件代码

下面的代码,包括两个任务,一个用来生成合并,压缩的配置文件;一个用来替换静态页中的js文件名,目前只实现了js文件的处理,css的差不多,读者可自行实现.
'use strict';
var util = require('util');
var fs = require('fs');
var inspect = function (obj) {
  return util.inspect(obj, false, 4, true);
};

module.exports = function(grunt) {

    // 先获取动态js列表
    var srcList = [];

    var _ = grunt.util._;
    var reg = /<script src=\\?\"([^\"\\]+)\\?\">/gi;
    var regReplace = /(<script src=\\?\")([^\"\\]+)(\\?\">)/gi;
    // 获取准备前的配置文件
    var smartisan = grunt.config('buildpjPrepare');

    grunt.registerMultiTask('buildpjPrepare', 'build front-end program', function() {
        // Merge task-specific and/or target-specific options with these defaults.

        var options = this.options();
        // Iterate over all specified file groups.
        // 首先检查文件是否存在
        var src;
        var filepath = options.url;
        if (!grunt.file.exists(filepath)) {
            grunt.log.warn('Source file "' + filepath + '" not found.');
        } else {
            src = grunt.file.read(filepath);
        }

        src.replace(reg, function ($0, $1) {
            // $0: 某一模板, $1: key, $2: value
            srcList.push({
                name: $1,
                content: grunt.file.read(smartisan.app + '/' + $1)
            });
        });


        var concatFiles = [], uglifyFiles = [];

        _.each(srcList, function(v, k){

            console.log(v, k);
            var srcFiles = [];
            v.content.replace(reg, function ($0, $1) {
                // $0: 某一模板, $1: key, $2: value
                srcFiles.push(smartisan.app + '/' + $1);
            });

            // 增加合并配置文件
            concatFiles.push({
                src: srcFiles,
                dest: '.temp/' + v.name
            });


            // 增加压缩配置文件
            uglifyFiles.push({
                src: '.temp/' + v.name,
                dest: smartisan.dist + '/' + v.name
            });

        });

        grunt.config('concat', {
            foo: {
                files: concatFiles
            }
        });

        grunt.config('uglify', {
            foo: {
                files: uglifyFiles
            }
        })

        var cfgNames = ['concat', 'uglify'];

        grunt.log.subhead('Configuration is now:');

        _.each(cfgNames, function(name) {

            grunt.log.subhead('  ' + name + ':').writeln('  ' + inspect(grunt.config(name)));

        });

    });

    grunt.registerMultiTask('buildpj', 'build front-end program', function() {

        var options = this.options();

        // 首先检查文件是否存在
        var src;
        var filepath = options.url;
        if (!grunt.file.exists(filepath)) {
            grunt.log.warn('Source file "' + filepath + '" not found.');
        } else {
            src = grunt.file.read(filepath);
        }

        // 获取所有md5之后的名称与之前的对应关系
        _.each(srcList, function(v, k){
            var arg = v.name.split('/');
            var dir = smartisan.dist + '/' + arg[0];
            var result = fs.readdirSync(dir);
            _.each(result, function(_v, _k){
                var fileNameArgOld = arg[1].split('.');
                var fileNameArgNew = _v.split('.');
                fileNameArgOld.splice(fileNameArgOld.length-1, 1);
                fileNameArgOld.push(fileNameArgNew[fileNameArgNew.length-2]);
                fileNameArgOld.push(fileNameArgNew[fileNameArgNew.length-1]);
                if(fileNameArgOld.join('.') == _v){
                    // 则找到一个替换后的文件名了
                    srcList[k].newName = arg[0] + '/' + _v;
                }
            });
        });

        // 替换内容中的压缩之后的并md5处理的名称
        src = src.replace(regReplace, function ($0, $1, $2, $3) {
            $2 = _.filter(srcList, function(v, k){
                return v.name == $2;
            })[0].newName;
            return $1 + $2 + $3;
        });

        // 开始写新的文件
        var fd = fs.openSync(filepath, 'w');
        fs.writeSync(fd, src, 0);
        fs.closeSync(fd);

    });

};

附上一个插件配置

// 全局设置
smartisan: {
    app: 'source',
    dist: 'dist'
},
buildpjPrepare: {
    build: {
        options: {
            url: '<%= smartisan.app %>/index.html'
        }
    },
    app: '<%= smartisan.app %>',
    dist: '<%= smartisan.dist %>',
},
buildpj: {
    build: {
        options: {
            url: '<%= smartisan.dist %>/index.html'
        }
    }
}

总结

感觉grunt是一个非常不错的构建工具,可用的模块非常多,也许我现在写的插件,就有别的实现,权当练手用.


上一篇:关于SAP Fiori Smart Template开发的一些实际例子


下一篇:Squid搭建反向代理提高网站访问速度