requirejs加载文件带上md5版本号的解决方案



最近团队其他项目组用到了requirejs(快过时的东西了,囧),因为项目过于庞大,因此缓存控制、文件版本号管理、增量更新这些问题都是需要解决的,而requirejs目前是无法解决这些的。由于我对前端构建工具有点研究,领导把这任务交给了我。

require.config 提供了urlArgs属性,用来添加版本号信息,但这东西是对所有文件生效,意味着只能全量更新,实在坑爹。而r.js只能用来打包合并文件,代码量还巨大,瞬间失去研究的兴趣。搜了一圈,国内外都没有谁提出过解决方案,没办法只能自己来了。真正动手处理这问题的时候才发现这问题确实麻烦。

因为r.js的局限性,而且超级难用,果断舍弃了,用的是自己最熟悉的gulp。最初的方案是通过 require.config 来处理, require.config 的 map 属性可以添加文件版号信息,类似这样:

require.config({ map:{ "*":{ "xxx/yyy/zzz" : "app/xxx/yyy/zzz.js?v=r3r3s2s" }
    }
});

可以在读取文件后生成一个版本号信息映射再写回到config文件。搞完后感觉很ok,加载的脚本都能带md5戳了,以为版本号问题就此解决了。可是按依赖关系把文件合并后就悲剧了,程序都跑不起来了。走投无路的时候决定去翻一翻requirejs的源码,突然灵光一闪,想到了被弃用的urlArgs,坑爹的urlArgs。

在requirejs源码里搜索urlArgs,只有一处地方用到,源码如下:

return config.urlArgs ? url + ((url.indexOf('?') === -1 ? '?' : '&') +
                                     config.urlArgs) : url;

当时就想,如果 config.urlArgs 是一个 function 会怎样,自己动手改了一下,代码如下:

   if (!config.urlArgs) {
    return url;
   }

   if (typeof config.urlArgs === 'string') {
    return url + ((url.indexOf('?') === -1 ? '?' : '&') + config.urlArgs);
   }

   if (isFunction(config.urlArgs)) {
    var urlArgs;
    try {
     urlArgs = config.urlArgs.call(config, moduleName, url);
    } catch (e) {
     urlArgs = "";
    }

    return url + ((url.indexOf('?') === -1 ? '?' : '&') + urlArgs);
   }

config.urlArgs 对比原先的字符串,变成了一个方法,每次生成文件url的时候调用一下,在这个方法里返回文件对应的md5串(其他版本号信息也可以)。再通过gulp将文件版本号映射写到 require.config 里,问题就迎刃而解啦。

困扰一个星期的问题终于搞定了,人也轻松多了。后面果断给requirejs作者提了pull request,希望能被合并吧。

上面提到的只是requirejs的部分,还有很多工作需要gulp来完成,不多说,直接贴代码了。

var fs = require('fs');
var path = require('path');
var crypto = require('crypto');

var gulp = require('gulp');
var gutil = require('gulp-util');
var uglify = require('gulp-uglify');
var plumber = require('gulp-plumber');
var concat = require('gulp-concat');
var rename = require('gulp-rename');

var Q = require('q');
var through = require('through2');
var _ = require('underscore');

var opts = null;	//配置文件中require.config()的参数,会通过正则匹配出来,用来算文件路径

var diskBase;	   //当前业务模块的根目录

var confFile;	   //配置文件的路径+文件名

var aliaMap = {};   //别名配置

var depMap = {};	//依赖版本

var verMap = {};	//版本号配置 ,key:路径别名,value:文件md5值

var aliaPaths = []; //所有的别名路径集合,用来计算文件对应的别名路径

var conf = (function() {

 return {
  config:{
   fileset:["../config.js"],
   dest:"../dist"
  },
  js: {
   fileset: ["../**/*.js","!../build/","!../build/**/*.*","!../dist/","!../dist/**/*.*"],
   dest: "../dist"
  },
  html:{
   fileset:["../**/*.html","!../build/","!../build/**/*.*","!../dist/","!../dist/**/*.*"],
   dest:"../dist"
  }
 };

})();

var sepReg = new RegExp("\\" + path.sep, "g");

function tryEval(obj) {
 var json;
 try {
  json = eval('(' + obj + ')');
 } catch (err) {

 }
 return json;
}

//统一文件名的路径分割符,
function formatFileName(name) {
 return name.replace(/[\\\/]+/g, path.sep).replace(/^(\w)/, function (str,cd) {
  return cd.toUpperCase();
 });
}

//过滤掉依赖表里的关键字
function filterDepMap(depMap) {
 depMap = depMap.filter(function (dep) {
  return ["require", "exports", "module"].indexOf(dep) === -1;
 });

 return depMap.map(function(dep) {
  return dep.replace(/\.js$/,'');
 });
}

//根据文件名计算对应别名路径
function getAliaPath(file) {
 file = file.replace(sepReg, "/");

 //paths配置转成数组,按路径级数排序
 if (aliaPaths.length === 0) {
  for (var alia in opts.paths) {
   aliaPaths.push({
    alia:alia,
    path:opts.paths[alia]
   });
  }
  aliaPaths.sort(function(path1,path2) {
   return path2.path.split(/[\\\/]/).length - path1.path.split(/[\\\/]/).length;  
  });
 }

 //优先匹配路径级数多的
 for (var i = 0; i < aliaPaths.length; i++) {
  var aliaPath = aliaPaths[i];
  var fullPath = path.join(opts.baseUrl, aliaPath.path); 
  var pathReg = new RegExp('^.*?' + fullPath.replace(/\\/g, '/') + '(.*?)\.js$'); 
  var matches = file.match(pathReg);
  if (matches) {
   return aliaPath.alia + matches[1];
  }
 }

 //todo?
 return file.replace(diskBase.replace(sepReg, "/"), '').replace(/\.js$/, '');
}

//根据文件内容计算md5串
function getDataMd5(data) {
 return crypto.createHash('md5').update(data).digest('base64');
}

//根据文件名计算发布包对应该文件路径
function getDestFile(file) {
 try {
  var fileDirs = file.split(path.sep);
  fileDirs.shift();
  return formatFileName(_.unique(path.join(__dirname, conf.js.dest).split(path.sep).concat(fileDirs)).join(path.sep));
 } catch(e) {
  return null;
 }
}

//根据发布包文件计算原文件名路径
function getSrcFile(file) {
 file = formatFileName(file);
 var destPath = formatFileName(path.join(__dirname, conf.js.dest));
 var restPath = file.replace(/[\\\/]+/g, '/').replace(destPath.replace(/[\\\/]+/g, '/'), '').replace(/[\\\/]+/g,path.sep);
 //todo
 var fileName = formatFileName(path.join(__dirname, '../', restPath));

 return fileName;
}

gulp.task('makeOpts',function() {
 return gulp.src(conf.config.fileset)
  .pipe(through.obj(function(file,enc,cb) {
   var js = file.contents.toString();
   var name = formatFileName(file.path);
   confFile = name;

   var configMatches = js.match(/require(js)?\.config\s*?\(\s*?({[\s\S]*?})\s*?\)/);
   var options = configMatches[2];
   opts = tryEval(options);

   //todo 有点不靠谱
   var dirs = name.split(path.sep);
   var base = opts.baseUrl.split('/')[1];
   dirs.splice(dirs.indexOf(base))
   diskBase = dirs.join(path.sep);

   var aliaPath = getAliaPath(name);

   //提取依赖,用来合并
   js.replace(/\s*require(js)?\s*\(\s*(\[[^\]\[]*?\])/, function (str, suf,map) {
    var map = tryEval(map);
    depMap[aliaPath] = filterDepMap(map);
   });

   this.push(file);
   cb();
  }))
});

//遍历所有文件,生成依赖关系配置
gulp.task('makeDeps', ['makeOpts'], function () {
 return gulp.src(conf.js.fileset)
  //.pipe(uglify({
  // mangle: {
  // except: ['require', 'requirejs', 'exports']
  // }
  //}))
  .pipe(through.obj(function (file, enc, cb) {
   var js = file.contents.toString();

   var name = formatFileName(file.path);

   var aliaPath = getAliaPath(name);

   aliaMap[aliaPath] = name;

   //todo 在这里把文件内容按规范修改掉?
   //提取依赖,用来合并
   js.replace(/;?\s*define\s*\(([^(]*),?\s*?function\s*\([^\)]*\)/, function (str, map) {

    var depStr = map.replace(/^[^\[]*(\[[^\]\[]*\]).*$/, "$1");

    if (/^\[/.test(depStr)) {
     var arr = tryEval(depStr);
     try {
      depMap[aliaPath] = filterDepMap(arr);
     } catch (e) {
      gutil.log("makeDeps Error: " + e);
     }
    }

   });

   this.push(file);
   cb();
  }))
  .pipe(through.obj(function (file, enc, cb) {
   var name = formatFileName(file.path);
   var aliaPath = getAliaPath(name);
   verMap[aliaPath] = getDataMd5(file.contents);

   this.push(file);
   cb();
  }))
  .pipe(gulp.dest(conf.js.dest));
});
var packPromises = [];

gulp.task('makePacks', ['makeDeps'], function () {

 //递归依赖关系,去重
 function makeDeps(deps) {
  var set = [];

  function make(deps) {
   deps.forEach(function (dep) {	  
    var currDeps = depMap[dep];		 //每个文件对应的依赖
    if (currDeps) {
     make(currDeps);
    }
    set.push(dep);
   });
  }

  make(deps);

  return _.unique(set);
 }

 var defineWithModuleNameReg = /;?\s*define\s*\(\s*["']/;  var defineWithoutModuleNameReg = /(;?\s*define\s*)\(\s*([^,]*),/;  for (var aliaPath in depMap) {   var clearDepMap = makeDeps(depMap[aliaPath]);   var fileMap = clearDepMap.map(function (dep) {    return aliaMap[dep];   });   var destFile = aliaMap[aliaPath];   fileMap.push(destFile);   //修改原文件地址为生成发布包对应文件地址   var destFileMap = fileMap.map(function (file) {    return getDestFile(file);   });   (function (destFileMap) {    var deferred = Q.defer();    var destFile = _.last(destFileMap);    var name = destFile.split(/[\\\/]+/).pop();    var dir = destFile.replace(name, '');    gulp.src(destFileMap)     .pipe(through.obj(function (file, enc, cb) {      var js = file.contents.toString();      var name = getSrcFile(file.path);      var aliaPath = getAliaPath(name);      if (!defineWithModuleNameReg.test(js)){       //添加模块名称       js = js.replace(defineWithoutModuleNameReg, function (str, def, mod) {        return ';define("' + aliaPath + '",' + mod + ',';       });       file.contents = new Buffer(js);      }      this.push(file);      cb();     }))     .pipe(concat(name))     //.pipe(uglify({     // mangle: {     // except: ['require', 'requirejs', 'exports']     // }     //}))     .pipe(through.obj(function (file, enc, cb) {      var name = getSrcFile(file.path);      var aliaPath = getAliaPath(name);      verMap[aliaPath] = getDataMd5(file.contents);      this.push(file);      cb();      deferred.resolve();     }))     .pipe(gulp.dest(dir));    packPromises.push(deferred.promise);   })(destFileMap);  } }); gulp.task('makeConf', ['makePacks'], function () {  Q.all(packPromises)   .done(function () {    gulp.src(getDestFile(confFile))     .pipe(through.obj(function (file, enc, cb) {      var js = file.contents.toString();      js = js.replace(/require(js)?\.config\s*?\(\s*?({[\s\S]*?)}\s*?\)/, "require.config($2" + ',\n"verMap" : ' + JSON.stringify(verMap) + "})");      file.contents = new Buffer(js);      this.push(file);      cb();     }))     .pipe(gulp.dest(conf.config.dest));   }); }); gulp.task('makeTpls',function() {  gulp.src(conf.html.fileset)   .pipe(gulp.dest(conf.html.dest)); }); gulp.task('default', ['makeTpls','makeConf']); 

代码目前只是个雏形,还有很多需要完成的工作,也有很多地方可以优化。

写这篇文章算是记录自己这一个多星期的研究成果吧。

最后附上github地址: https://github.com/hellopao/requirejs

最近团队其他项目组用到了requirejs(快过时的东西了,囧),因为项目过于庞大,因此缓存控制、文件版本号管理、增量更新这些问题都是需要解决的,而requirejs目前是无法解决这些的。由于我对前端构建工具有点研究,领导把这任务交给了我。

require.config 提供了urlArgs属性,用来添加版本号信息,但这东西是对所有文件生效,意味着只能全量更新,实在坑爹。而r.js只能用来打包合并文件,代码量还巨大,瞬间失去研究的兴趣。搜了一圈,国内外都没有谁提出过解决方案,没办法只能自己来了。真正动手处理这问题的时候才发现这问题确实麻烦。

因为r.js的局限性,而且超级难用,果断舍弃了,用的是自己最熟悉的gulp。最初的方案是通过 require.config 来处理, require.config 的 map 属性可以添加文件版号信息,类似这样:

require.config({ map:{ "*":{ "xxx/yyy/zzz" : "app/xxx/yyy/zzz.js?v=r3r3s2s" }
    }
});

可以在读取文件后生成一个版本号信息映射再写回到config文件。搞完后感觉很ok,加载的脚本都能带md5戳了,以为版本号问题就此解决了。可是按依赖关系把文件合并后就悲剧了,程序都跑不起来了。走投无路的时候决定去翻一翻requirejs的源码,突然灵光一闪,想到了被弃用的urlArgs,坑爹的urlArgs。

在requirejs源码里搜索urlArgs,只有一处地方用到,源码如下:

return config.urlArgs ? url + ((url.indexOf('?') === -1 ? '?' : '&') +
                                     config.urlArgs) : url;

当时就想,如果 config.urlArgs 是一个 function 会怎样,自己动手改了一下,代码如下:

   if (!config.urlArgs) {
    return url;
   }

   if (typeof config.urlArgs === 'string') {
    return url + ((url.indexOf('?') === -1 ? '?' : '&') + config.urlArgs);
   }

   if (isFunction(config.urlArgs)) {
    var urlArgs;
    try {
     urlArgs = config.urlArgs.call(config, moduleName, url);
    } catch (e) {
     urlArgs = "";
    }

    return url + ((url.indexOf('?') === -1 ? '?' : '&') + urlArgs);
   }

config.urlArgs 对比原先的字符串,变成了一个方法,每次生成文件url的时候调用一下,在这个方法里返回文件对应的md5串(其他版本号信息也可以)。再通过gulp将文件版本号映射写到 require.config 里,问题就迎刃而解啦。

困扰一个星期的问题终于搞定了,人也轻松多了。后面果断给requirejs作者提了pull request,希望能被合并吧。

上面提到的只是requirejs的部分,还有很多工作需要gulp来完成,不多说,直接贴代码了。

var fs = require('fs');
var path = require('path');
var crypto = require('crypto');

var gulp = require('gulp');
var gutil = require('gulp-util');
var uglify = require('gulp-uglify');
var plumber = require('gulp-plumber');
var concat = require('gulp-concat');
var rename = require('gulp-rename');

var Q = require('q');
var through = require('through2');
var _ = require('underscore');

var opts = null;	//配置文件中require.config()的参数,会通过正则匹配出来,用来算文件路径

var diskBase;	   //当前业务模块的根目录

var confFile;	   //配置文件的路径+文件名

var aliaMap = {};   //别名配置

var depMap = {};	//依赖版本

var verMap = {};	//版本号配置 ,key:路径别名,value:文件md5值

var aliaPaths = []; //所有的别名路径集合,用来计算文件对应的别名路径

var conf = (function() {

 return {
  config:{
   fileset:["../config.js"],
   dest:"../dist"
  },
  js: {
   fileset: ["../**/*.js","!../build/","!../build/**/*.*","!../dist/","!../dist/**/*.*"],
   dest: "../dist"
  },
  html:{
   fileset:["../**/*.html","!../build/","!../build/**/*.*","!../dist/","!../dist/**/*.*"],
   dest:"../dist"
  }
 };

})();

var sepReg = new RegExp("\\" + path.sep, "g");

function tryEval(obj) {
 var json;
 try {
  json = eval('(' + obj + ')');
 } catch (err) {

 }
 return json;
}

//统一文件名的路径分割符,
function formatFileName(name) {
 return name.replace(/[\\\/]+/g, path.sep).replace(/^(\w)/, function (str,cd) {
  return cd.toUpperCase();
 });
}

//过滤掉依赖表里的关键字
function filterDepMap(depMap) {
 depMap = depMap.filter(function (dep) {
  return ["require", "exports", "module"].indexOf(dep) === -1;
 });

 return depMap.map(function(dep) {
  return dep.replace(/\.js$/,'');
 });
}

//根据文件名计算对应别名路径
function getAliaPath(file) {
 file = file.replace(sepReg, "/");

 //paths配置转成数组,按路径级数排序
 if (aliaPaths.length === 0) {
  for (var alia in opts.paths) {
   aliaPaths.push({
    alia:alia,
    path:opts.paths[alia]
   });
  }
  aliaPaths.sort(function(path1,path2) {
   return path2.path.split(/[\\\/]/).length - path1.path.split(/[\\\/]/).length;  
  });
 }

 //优先匹配路径级数多的
 for (var i = 0; i < aliaPaths.length; i++) {
  var aliaPath = aliaPaths[i];
  var fullPath = path.join(opts.baseUrl, aliaPath.path); 
  var pathReg = new RegExp('^.*?' + fullPath.replace(/\\/g, '/') + '(.*?)\.js$'); 
  var matches = file.match(pathReg);
  if (matches) {
   return aliaPath.alia + matches[1];
  }
 }

 //todo?
 return file.replace(diskBase.replace(sepReg, "/"), '').replace(/\.js$/, '');
}

//根据文件内容计算md5串
function getDataMd5(data) {
 return crypto.createHash('md5').update(data).digest('base64');
}

//根据文件名计算发布包对应该文件路径
function getDestFile(file) {
 try {
  var fileDirs = file.split(path.sep);
  fileDirs.shift();
  return formatFileName(_.unique(path.join(__dirname, conf.js.dest).split(path.sep).concat(fileDirs)).join(path.sep));
 } catch(e) {
  return null;
 }
}

//根据发布包文件计算原文件名路径
function getSrcFile(file) {
 file = formatFileName(file);
 var destPath = formatFileName(path.join(__dirname, conf.js.dest));
 var restPath = file.replace(/[\\\/]+/g, '/').replace(destPath.replace(/[\\\/]+/g, '/'), '').replace(/[\\\/]+/g,path.sep);
 //todo
 var fileName = formatFileName(path.join(__dirname, '../', restPath));

 return fileName;
}

gulp.task('makeOpts',function() {
 return gulp.src(conf.config.fileset)
  .pipe(through.obj(function(file,enc,cb) {
   var js = file.contents.toString();
   var name = formatFileName(file.path);
   confFile = name;

   var configMatches = js.match(/require(js)?\.config\s*?\(\s*?({[\s\S]*?})\s*?\)/);
   var options = configMatches[2];
   opts = tryEval(options);

   //todo 有点不靠谱
   var dirs = name.split(path.sep);
   var base = opts.baseUrl.split('/')[1];
   dirs.splice(dirs.indexOf(base))
   diskBase = dirs.join(path.sep);

   var aliaPath = getAliaPath(name);

   //提取依赖,用来合并
   js.replace(/\s*require(js)?\s*\(\s*(\[[^\]\[]*?\])/, function (str, suf,map) {
    var map = tryEval(map);
    depMap[aliaPath] = filterDepMap(map);
   });

   this.push(file);
   cb();
  }))
});

//遍历所有文件,生成依赖关系配置
gulp.task('makeDeps', ['makeOpts'], function () {
 return gulp.src(conf.js.fileset)
  //.pipe(uglify({
  // mangle: {
  // except: ['require', 'requirejs', 'exports']
  // }
  //}))
  .pipe(through.obj(function (file, enc, cb) {
   var js = file.contents.toString();

   var name = formatFileName(file.path);

   var aliaPath = getAliaPath(name);

   aliaMap[aliaPath] = name;

   //todo 在这里把文件内容按规范修改掉?
   //提取依赖,用来合并
   js.replace(/;?\s*define\s*\(([^(]*),?\s*?function\s*\([^\)]*\)/, function (str, map) {

    var depStr = map.replace(/^[^\[]*(\[[^\]\[]*\]).*$/, "$1");

    if (/^\[/.test(depStr)) {
     var arr = tryEval(depStr);
     try {
      depMap[aliaPath] = filterDepMap(arr);
     } catch (e) {
      gutil.log("makeDeps Error: " + e);
     }
    }

   });

   this.push(file);
   cb();
  }))
  .pipe(through.obj(function (file, enc, cb) {
   var name = formatFileName(file.path);
   var aliaPath = getAliaPath(name);
   verMap[aliaPath] = getDataMd5(file.contents);

   this.push(file);
   cb();
  }))
  .pipe(gulp.dest(conf.js.dest));
});
var packPromises = [];

gulp.task('makePacks', ['makeDeps'], function () {

 //递归依赖关系,去重
 function makeDeps(deps) {
  var set = [];

  function make(deps) {
   deps.forEach(function (dep) {	  
    var currDeps = depMap[dep];		 //每个文件对应的依赖
    if (currDeps) {
     make(currDeps);
    }
    set.push(dep);
   });
  }

  make(deps);

  return _.unique(set);
 }

 var defineWithModuleNameReg = /;?\s*define\s*\(\s*["']/;  var defineWithoutModuleNameReg = /(;?\s*define\s*)\(\s*([^,]*),/;  for (var aliaPath in depMap) {   var clearDepMap = makeDeps(depMap[aliaPath]);   var fileMap = clearDepMap.map(function (dep) {    return aliaMap[dep];   });   var destFile = aliaMap[aliaPath];   fileMap.push(destFile);   //修改原文件地址为生成发布包对应文件地址   var destFileMap = fileMap.map(function (file) {    return getDestFile(file);   });   (function (destFileMap) {    var deferred = Q.defer();    var destFile = _.last(destFileMap);    var name = destFile.split(/[\\\/]+/).pop();    var dir = destFile.replace(name, '');    gulp.src(destFileMap)     .pipe(through.obj(function (file, enc, cb) {      var js = file.contents.toString();      var name = getSrcFile(file.path);      var aliaPath = getAliaPath(name);      if (!defineWithModuleNameReg.test(js)){       //添加模块名称       js = js.replace(defineWithoutModuleNameReg, function (str, def, mod) {        return ';define("' + aliaPath + '",' + mod + ',';       });       file.contents = new Buffer(js);      }      this.push(file);      cb();     }))     .pipe(concat(name))     //.pipe(uglify({     // mangle: {     // except: ['require', 'requirejs', 'exports']     // }     //}))     .pipe(through.obj(function (file, enc, cb) {      var name = getSrcFile(file.path);      var aliaPath = getAliaPath(name);      verMap[aliaPath] = getDataMd5(file.contents);      this.push(file);      cb();      deferred.resolve();     }))     .pipe(gulp.dest(dir));    packPromises.push(deferred.promise);   })(destFileMap);  } }); gulp.task('makeConf', ['makePacks'], function () {  Q.all(packPromises)   .done(function () {    gulp.src(getDestFile(confFile))     .pipe(through.obj(function (file, enc, cb) {      var js = file.contents.toString();      js = js.replace(/require(js)?\.config\s*?\(\s*?({[\s\S]*?)}\s*?\)/, "require.config($2" + ',\n"verMap" : ' + JSON.stringify(verMap) + "})");      file.contents = new Buffer(js);      this.push(file);      cb();     }))     .pipe(gulp.dest(conf.config.dest));   }); }); gulp.task('makeTpls',function() {  gulp.src(conf.html.fileset)   .pipe(gulp.dest(conf.html.dest)); }); gulp.task('default', ['makeTpls','makeConf']); 

代码目前只是个雏形,还有很多需要完成的工作,也有很多地方可以优化。

写这篇文章算是记录自己这一个多星期的研究成果吧。

最后附上github地址: https://github.com/hellopao/requirejs

你可能感兴趣的:(requirejs加载文件带上md5版本号的解决方案)