最近团队其他项目组用到了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