受《大公司怎樣開發和部署前端代碼?》這篇文章的啓發,結合本身的項目實踐,建立了一套JavaScript文件的版本管理和加載的機制,雖然比較粗糙,可是解決了很多實際的問題。
javascript
使用到的主要工具:css
Node.jshtml
NPM前端
Grunt和相關插件(grunt-hashmap,grunt-contrib-uglify,自定義的插件)java
LABjs
jquery
功能點:ios
利用grunt插件hashmap根據JavaScript的文件的內容生成hash碼,可作爲JavaScript文件名的一部分,有效防止在更改JavaScript文件內容後瀏覽器緩存的問題。json
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項目,並且代碼比較粗糙,只是寫出來給有用的朋友一些幫助。