用JavaScript實現JavaScript文件的版本管理和加載
受《大公司怎樣開發和部署前端代碼?》這篇文章的啟發,結合自己的項目實踐,創建了一套JavaScript文件的版本管理和加載的機制,雖然比較粗糙,但是解決了不少實際的問題。
使用到的主要工具:
-
Node.js
-
NPM
-
Grunt和相關插件(grunt-hashmap,grunt-contrib-uglify,自定義的插件)
-
LABjs
功能點:
-
利用grunt插件hashmap根據JavaScript的文件的內容生成hash碼,可做為JavaScript文件名的一部分,有效防止在更改JavaScript文件內容后瀏覽器緩存的問題。
-
JSP頁面中不再直接引用JavaScript文件,所有的JavaScript文件通過JSON格式的映射文件,由LAB.js根據映射關系負責加載,這樣每次修改JavaScript文件后,再次發布時無需修改JSP頁面。
-
開發環境和生成環境的切換。
-
對JavaScript文件壓縮
前端的項目結構:
后端的主要文件:static_file.jsp
<script type="text/javascript" src="${staticroot}/static/js/lib/LAB.min.js"></script>
<script type="text/javascript">
(function() {
$LAB
.script("${staticroot}/static/js/lib/jquery.min.js")
.wait()
.script("${staticroot}/static/js/config/JspJsMapping.js")
.script("${staticroot}/static/js/config/VersionMapping.js").wait()
.script("${staticroot}/static/js/config/AppConfig.js")
.wait(function() {
AppConfig.getScripts('<%=request.getServletPath()%>', '${staticroot}');
})
})();
</script> 項目中的JSP文件中如果存在JavaScript文件,則只需引用static_file.jsp文件即可,其余的工作都交給static_file.jsp文件中的JavaScript代碼即可。
通過觀察static_file.jsp文件可以發現,首先要引用LAB.min.js類庫,這是加載JavaScript文件的核心。另外由于項目中每個JSP文件都有對Jquery的引用,所以在此處Jquery文件也有LABjs默認加載。
另外,JspJsMapping.js、VersionMapping.js和AppConfig.js便是整個前端的配置文件,在某個JSP中加載哪些JavaScript文件,加載哪個版本的JavaScript文件,使用生成環境還是測試環境都由這三個配置文件進行管理。
下面分別介紹這三個配置文件:
JspJsMapping.js文件由JspJsMapping.tpl模板文件使用grunt自定義插件生成的,內容如下(部分):
/**
* Created by wanglu on 2014/12/16.
* 生成jsp中使用到的js文件的映射信息(模板)
*/
;(function(window) {
window.JspJsMapping = {
"public1": {
"scripts":[
"lib/spin.min.js",
"lib/iosOverlay.js",
"lib/fastclick.js",
"Utils.js"
]
},
"/WEB-INF/views/acc/about.jsp": {
"scripts":[
"include:public1"
]
},
"/WEB-INF/views/acc/applylist.jsp": {
"scripts":[
"lib/jquery.mobile.custom.min.js",
"lib/AjaxUtil.js",
"lib/iscroll.js",
"lib/iscrollAssist.js",
"PageController.js",
"lib/SessionStorageUtil.js",
"lib/LocalStorageUtil.js",
"include:public1",
{"name": "applylist.js", "wait": true}
]
},
"/WEB-INF/views/acc/favbranches.jsp": {
"scripts":[
"lib/jquery.mobile.custom.min.js",
"lib/AjaxUtil.js",
"lib/iscroll.js",
"lib/iscrollAssist.js",
"PageController.js",
"lib/SessionStorageUtil.js",
"lib/LocalStorageUtil.js",
"include:public1",
{"name": "favbranches.js", "wait": true}
]
},
"/WEB-INF/views/acc/favjobs.jsp": {
"scripts":[
"lib/jquery.mobile.custom.min.js",
"lib/AjaxUtil.js",
"lib/iscroll.js",
"lib/iscrollAssist.js",
"PageController.js",
"lib/SessionStorageUtil.js",
"lib/LocalStorageUtil.js",
"include:public1",
{"name": "favjobs.js", "wait": true}
]
},
"/WEB-INF/views/acc/index.jsp": {
"scripts":[
]
}
}
})(window); 這個文件配置的JSP頁面與JavaScript的引用關系。
VersionMapping.js文件由Versionmapping.tpl模板文件使用grunt自定義插件生成,內容如下(部分):
/**
* Created by wanglu on 2014/12/16.
* 生成版本映射信息(模板)
*/
;(function(window) {
window.VersionMapping = {
version: '20150403174521',
mappings: {
'CityHelper.js': '5e4cda',
'DictionaryCache.js': '96ecdf',
'FoodHelper.js': 'ec65b2',
'FunctionHelper.js': '350065',
'Gruntfile.js': 'f916ad',
'PageController.js': 'b5ed9d',
'SocialHelper.js': '821373',
'Utils.js': 'cb4ade',
'WorkFuncHelper.js': '3013b8',
'app.js': 'aecc0b',
'apply.js': 'baa38d',
'applylist.js': '78be19',
'branch.js': '388c5e',
'branchjobs.js': '1fe1ec',
'branchlist.js': '86d21a',
'favbranches.js': 'd2331a',
'favjobs.js': '4970e4',
'iscroll_kt.js': '0dc411'
}
}
})(window); 這個文件配置的JavaScript文件和其當前的hashcode的映射關系,比如/WEB-INF/views/acc/favjobs.jsp文件引用了PageController.js文件,那么最終在JSP文件中將加載PageController_b5ed9d.js文件,對于一些公用的JavaScript文件,可將其配置如下的格式:
"public1": {
"scripts":[
"lib/spin.min.js",
"lib/iosOverlay.js",
"lib/fastclick.js",
"Utils.js"
]
} 然后在JavaScript文件的使用的地方引用public1,改變為如下樣式:
"/WEB-INF/views/acc/applylist.jsp": {
"scripts":[
"lib/jquery.mobile.custom.min.js",
"lib/AjaxUtil.js",
"lib/iscroll.js",
"lib/iscrollAssist.js",
"PageController.js",
"lib/SessionStorageUtil.js",
"lib/LocalStorageUtil.js",
"include:public1",
{"name": "applylist.js", "wait": true}
]
}
AppConfig.js文件是加載功能的實現,在此文件中使用LABjs,通過JspJsMapping.js和VersionMapping.js兩個配置文件為JSP頁面加載所需要的JavaScript的文件。內容如下(全部):
;(function($, window){
window.AppConfig = {
// mode: 'debug',
min: true,
log: true,
baseUrl: '/static/js/build/',
debugUrl: '/static/js/',
getScripts: function(key, baseUrl) {
var scripts = JspJsMapping[key]['scripts'] || [];
var labJsScript = this.getLABScript(baseUrl, scripts);
if (labJsScript !== '$LAB') {
try {
window.eval(labJsScript + ';');
} catch(e) {
console && console.error(e);
}
}
},
getLABScript: function(baseUrl, scripts, labJsScript) {
labJsScript = labJsScript || '$LAB';
for (var sc; scripts && (scripts instanceof Array) && (sc = scripts.shift()); ) {
if (typeof sc === 'string') {
if(sc.indexOf('include:') === 0) {
var key = sc.substring(sc.indexOf(':') + 1);
labJsScript = this.getLABScript(baseUrl, JspJsMapping[key]['scripts'] || [], labJsScript);
}
else {
var url = AppConfig.getFileName(sc);
labJsScript += '.script("'+ baseUrl + url + '")';
this.log && console && console.log('loadding : ' + baseUrl + url);
}
}
else if (typeof sc === 'object') {
var url = AppConfig.getFileName(sc['name']);
labJsScript += '.script("'+ baseUrl + url + '")';
if (sc['wait']) {
labJsScript += '.wait()';
url = 'wait ' + baseUrl + url;
}
this.log && console && console.log('loadding : ' + url);
}
}
return labJsScript;
},
getFileName : function(fileName) {
if (!fileName) {
return '';
}
if (this.mode === 'debug') {
return ( AppConfig.debugUrl || AppConfig.baseUrl) + fileName;
}
else {
return (AppConfig.baseUrl || '')
+ (this.min ? 'min/' : '')
+ fileName.substring(0, fileName.lastIndexOf('.'))
+ '_' + VersionMapping.mappings[fileName] + '.js';
}
}
};
return window.AppConfig;
})($, window); 此文件中的代碼最后生成是類似于$LAB.script(...).script(..)格式的代碼字符串,然后用window.eval方式執行,實現JavaScript的代碼加載。
在此文件中還可以配置是否開啟debug(debug屬性)模式,是否加載壓縮過(min屬性)的JavaScript文件等。由于在瀏覽器調試JavaScript的時候,壓縮過的JavaScript的文件無法閱讀,所以才使用min屬性控制是否加載壓縮過的JavaScript文件。
未壓縮的文件放在build文件夾中,壓縮過的文件放在build/min文件夾中。
另附 Gruntfile.js全部代碼:
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
//banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
build: {
expand: true,
cwd: 'build/',
src: ['lib/*.js', '*.js'],
dest: 'build/min'
}
},
hashmap: {
options: {
// These are default options
output: '#{= dest}/hash.json',
etag: null, // See below([#](#option-etag))
algorithm: 'md5', // the algorithm to create the hash
rename: '#{= dirname}/#{= basename}_#{= hash}#{= extname}', // save the original file as what
keep: true, // should we keep the original file or not
merge: false, // merge hash results into existing `hash.json` file or override it.
hashlen: 6 // length for hashsum digest
},
js: {
// Target-specific file lists and/or options go here.
options: {
output: 'config/versions/version_' + grunt.template.date(new Date(), 'yyyymmddHHMMss') + '.json'
},
files: {
src: ['lib/*.js', '*.js']
}
}
},
copy: {
main: {
files: [
{
cwd: 'src/', //此設置很有用,如果設置設置src:src/*.js,則會連src文件夾一同復制
src: ['lib/*.js', '*.js'],
dest: 'build/',
filter: 'isFile',
expand: true
}
]
}
},
clean: {
build: {
src: ["src/lib/*.js", "src/*.js", 'build/lib/*.js', 'build/*.js']
}
},
//壓縮css
cssmin: {
build: {
files: {
'../css/all.014.min.css': [ '../css/*.css', '!../css/*.min.css']
}
}
}
});
// 加載任務的插件。
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-hashmap');
grunt.loadNpmTasks('grunt-file-hash');
grunt.loadNpmTasks('grunt-cachebuster');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-clean');
// 默認被執行的任務列表。
grunt.registerTask('default', ['cssmin']);
grunt.registerTask('build', ['hashmap', 'copy', 'uglify', 'clean']);
grunt.registerTask('version','publish JavaScript', function(file){
var version = grunt.file.readJSON('config/versions/' + file);
var json = '{\n';
var maps = [];
for (var p in version) {
grunt.log.write(p + ' : ' + version[p] + '\n');
maps.push('\t\t\t\'' + p + '\'' + ': ' + '\'' + version[p] + '\'');
}
json += maps.join(',\n');
json += '\n\t\t}';
grunt.log.write('successed!!! 【' + maps.length + '】 files done.\n');
var config = grunt.file.read('config/template/VersionMapping.tpl')
.replace('{{mappings}}', json)
.replace('{{version}}', file.substring(file.indexOf('_') + 1, file.lastIndexOf('.')));
grunt.file.write('config/VersionMapping.js', config, {encoding: 'utf-8'});
});
/* 將config/jsp2js/文件夾下的配置信息合并到config/JspJsMapping.js */
grunt.registerTask('mapping','generate mapping info', function(){
var mappings = [];
var baseTab = '\t';
grunt.file.recurse('config/jsp2js/', function(abspath, rootdir, subdir, filename) {
grunt.log.write('reading ' + abspath + '\n');
var json = grunt.file.readJSON(abspath);
for (var p in json.mapping) {
if (json[p]) {
grunt.log.write('In ' + abspath + ' file,' + p + ' has exists!\n');
continue;
}
var str = '';
var mapping = json.mapping[p];
var scripts = mapping['scripts'];
str += baseTab + '\t' + '"' + p + '": {\n';
str += baseTab + '\t\t' + '"scripts":[\n';
for (var i = 0; scripts && i < scripts.length; i++) {
var script = scripts[i];
if (typeof script === 'string') {
str += ( baseTab + '\t\t\t' + '"' + script + '"'
+ (i !== scripts.length - 1 ? ',' : '') + '\n');
}
else if (typeof script === 'object') {
str += ( baseTab + '\t\t\t{"name": ' + '"' + script['name']
+ '", "wait": ' + (script['wait'] ? 'true' : 'false') + "}"
+ (i !== scripts.length - 1 ? ',' : '') + '\n');
}
}
str += (baseTab + '\t\t' + ']\n');
str += (baseTab + '\t' + '}');
mappings.push(str);
}
});
var mappingsStr = '{\n' + mappings.join(',\n') + '\n' + baseTab + '}\n';
grunt.log.write('result: ' + mappingsStr + '\n');
var config = grunt.file.read('config/template/JspJsMapping.tpl')
.replace('{{mapping}}', mappingsStr);
grunt.file.write('config/JspJsMapping.js', config, {encoding: 'utf-8'});
});
}; 另附 package.json全部代碼:
{
"name": "weilai",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "wanglu",
"license": "ISC",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-cachebuster": "^0.1.5",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-cssmin": "*",
"grunt-contrib-sass": "*",
"grunt-contrib-uglify": "*",
"grunt-contrib-watch": "*",
"grunt-cssc": "*",
"grunt-file-hash": "^0.1.5",
"grunt-hashmap": "^0.1.5",
"grunt-htmlhint": "^0.4.1",
"matchdep": "*"
}
} 整個機制就是這樣,目前寫的還不是太詳細,后續將繼續完善。另外,本方法不是對所有的項目都適用,比如由模塊化開發的JavaScript項目,而且代碼比較粗糙,只是寫出來給有用的朋友一些幫助。
來自:http://my.oschina.net/zero2hero/blog/401293