用JavaScript实现JavaScript文件的版本管理和加载

受《大公司怎样开发和部署前端代码?》这篇文章的启发,结合自己的项目实践,创建了一套JavaScript文件的版本管理和加载的机制,虽然比较粗糙,但是解决了不少实际的问题。

使用到的主要工具:

  1. Node.js

  2. NPM

  3. Grunt和相关插件(grunt-hashmap,grunt-contrib-uglify,自定义的插件)

  4. LABjs

功能点:

  1. 利用grunt插件hashmap根据JavaScript的文件的内容生成hash码,可做为JavaScript文件名的一部分,有效防止在更改JavaScript文件内容后浏览器缓存的问题。

  2. JSP页面中不再直接引用JavaScript文件,所有的JavaScript文件通过JSON格式的映射文件,由LAB.js根据映射关系负责加载,这样每次修改JavaScript文件后,再次发布时无需修改JSP页面。

  3. 开发环境和生成环境的切换。

  4. 对JavaScript文件压缩


前端的项目结构:

用JavaScript实现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项目,而且代码比较粗糙,只是写出来给有用的朋友一些帮助。


发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注