版本号的管理
一、面临的问题
出于性能优化的考虑,通常资源服务器会对静态资源的HTTP响应首部添加Expires 或者Cache-Control: max-age设置失效时间,如下图:
这样,在失效时间到达之前,浏览器会使用缓存文件而不用重新发送HTTP请求。这就引起另一个问题:失效时间还未到,但是我们有新功能上线,如何通知浏览器弃用缓存,重新发送HTTP请求获取最新的文件?
二、版本号的用途
当URL有变化时,浏览器总是会重新发送HTTP请求尝试获取最新的文件。利用浏览器的这一特性,我们可以为静态资源的URL添加版本号来更新浏览器缓存。生成版本号,业界有两种常用方案:
方案一、用更新时间当做版本号,如下:
<script type="text/javascript" src="m139.core.pack.js?v=20130825"></script>
方案二、根据文件内容进行 hash 运算得到版本号,如下:
<script type="text/javascript" src="m139.core.pack.js?v=opMnaQdlocvGACU1GtgYoQ%3D%3D"></script>
方案一的优势为实现简单,缺点显而易见:
(1)需要手动管理版本号
(2)无法解决CDN 缓存攻击。什么是CDN缓存攻击?黑客可提前猜测到你的下一个版本号,20130825的下一个版本很可能是20130826,提前访问你的静态资源,让你的CDN缓存旧文件。这样当真正的用户请求资源时,永远返回的都是过期的文件。
由于以上缺陷,139邮箱web2.0采用方案二生成版本号
三、139邮箱版本号的生成与使用
(1)依赖软件
Jdk、Ant、Node 之所以需要Jdk是因为Ant需要Jdk,所以新同学来了首先装软件,配置环境变量。
(2)生成过程
1、Ant的入口脚本build.xml:
<project default="m2012_pack"> <target name="m2012_pack" > <property name="base" location="./" /> <property name="out" location="./target" /> <property name="resource_m2012" location="${out}/images.139cm.com/m2012" /> <!-- 创建文件夹 --> <mkdir dir="${out}"></mkdir> <!-- 执行合并js --> <antcall target="pack_js"></antcall> <!-- 执行合并css --> <subant target=""> <fileset dir="./" includes="concatcss.xml"/> </subant> <!-- 执行提取文件版本号 --> <subant target=""> <fileset dir="./" includes="createFileVersion.xml"/> </subant> <!-- 复制js文件 --> <copydir src="${base}/js/" dest="${resource_m2012}/js/" includes="**/**.js" excludes="" /> <!-- 复制css/image/html/flash文件 --> <copydir src="${base}/css/" dest="${resource_m2012}/css/" includes="**/**.css," excludes="" /> <copydir src="${base}/images/" dest="${resource_m2012}/images/" excludes="" /> <!-- 小工具文件夹包删除 <copydir src="${base}/controlupdate/" dest="${resource_m2012}/controlupdate/" excludes="" /> --> <copydir src="${base}/component/" dest="${resource_m2012}/component/" excludes="" /> <copydir src="${base}/flash/" dest="${resource_m2012}/flash/" includes="**/**" excludes="" /> <!-- 信纸文件夹有图片 --> <copydir src="${base}/html/" dest="${resource_m2012}/html/" excludes="" /> <delete file="${resource_m2012}/html/set/feature_meal_config.js" /> <delete file="${resource_m2012}/js/config.*.js" /> <!-- 复制conf文件 --> <copydir src="${base}/conf/" dest="${resource_m2012}/conf/" excludes="" /> <!-- 压缩js --> <echo>开始压缩脚本</echo> <antcall target="min_js"></antcall> <echo>生成样式引用图片的版本号</echo> <antcall target="replace_image_version"></antcall> </target> <!-- 子任务:合并js --> <target name="pack_js"> <subant target=""> <fileset dir="./js/" includes="**.pack.js.xml"/> </subant> </target> <!-- 子任务:压缩js --> <target name="min_js"> <subant target=""> <fileset dir="./" includes="jsmin.xml"/> </subant> </target> <!-- 子任务:替换css里的图片文件版本号 --> <target name="replace_image_version"> <exec executable="node"> <arg value="${base}/build/lib/replaceCSSImageVersion.js" /> <arg value="--csspath=${resource_m2012}/css" /> </exec> </target> </project>
注意,合并文件任务在提取版本号任务之前,我们是正对合并后的文件内容生成版本号,原因很简单:我们在页面上引入的本来就是合并后的文件。
2、Ant的生成版本号脚本createFileVersion.xml:
<?xml version="1.0"?> <project name="BuildCssProject" default="createFileVersion"> <!-- 执行任务的nodejs程序 --> <property name="path.concatExec" location="./build/lib/createFileVersion.js" /> <property name="thisPath" location="./" /> <target name="createFileVersion"> <echo>正在生成文件版本号</echo> <apply executable="node" failonerror="true"> <!-- 要生成版本号的文件,可根据需求配置 --> <fileset dir="./" includes="js/packs/**/**.js,css/**/**.css" /> <arg path="${path.concatExec}" /> <arg line="--output=${thisPath}/conf/config.10086.cn.js,${thisPath}/conf/config.10086ts.cn.js,${thisPath}/conf/config.10086rd.cn.js" /> </apply> </target> </project>
看脚本我们知道,Ant调用了Node命令执行了JS脚本createFileVersion.js:
/* * 根据文件md5值生成版本号 */ var fs = require("fs"); var crypto = require("crypto"); //用来做MD5 和 Base64 var argvs = { //输入输出文件必须都是utf-8编码 "--output": ""//这个参数指定输出的配置文件,可以是以逗号隔开的多个文件 }; process.argv.forEach(function (item) { for (var p in argvs) { if (item.indexOf(p + "=") == 0) { argvs[p] = item.split("=")[1]; } } }); var filePath = process.argv[process.argv.length - 1]; var md5 = getFileMD5(filePath); md5 = encodeURIComponent(md5); var fileName = getFileName(filePath); var configFiles = argvs["--output"].split(","); //预计会有研发线、测试线、生产线3个配置文件 configFiles.forEach(function (confFile) { writeConfigJSON(confFile, fileName, md5); }); function getMD5(data) { var hash = crypto.createHash("md5"); hash.update(data); var md5Base64 = hash.digest("base64"); return md5Base64; } function getFileMD5(file) { var data = fs.readFileSync(file); return getMD5(data); } function writeConfigJSON(confFile,sourceFileName,md5) { var reg = /\/\/<fileConfig>([\s\S]+?)<\/fileConfig>/; var text = fs.readFileSync(confFile).toString(); var m = text.match(reg); var isReplace = false; if (m) { isReplace = true; } if (isReplace) { try { var Config_FileVersion; eval(m[1]); if (!Config_FileVersion) { throw "goto catch"; } } catch (e) { throw "eval <fileConfig> js error from" + confFile + "\r\n+++++\r\n" + m[1]; } } else { Config_FileVersion = {}; } Config_FileVersion["defaults"] = new Date().toISOString(); //替换资源文件版本号 Config_FileVersion[sourceFileName] = md5; var newConfString = "//<fileConfig>\r\nvar Config_FileVersion = " + JSON.stringify(Config_FileVersion, "", 4) + "\r\n//</fileConfig>"; if (isReplace) { text = text.replace(reg, newConfString); } else { text += "\r\n\r\n" + newConfString; } fs.writeFileSync(confFile, text); console.log("get file version " + sourceFileName + ":" + md5); /* //<fileConfig> var Config_FileVersion = { "defaults": "20130605_randomnum",//默认全部资源版本号 "index.html.pack.js": "asdasdasdas_1",//单独文件版本号,视自动化构建配置而定 md5_手动修改版本号数字 "compose.html.pack.js": "czxcascasca_1" } //</fileConfig> */ } function getFileName(full) { return full.match(/[^\/\\]+$/)[0]; } /* process.argv.forEach(function (item) { for(var p in argvs){ if (item.indexOf(p + "=") == 0) { argvs[p] = item.split("=")[1]; } } }); if (!argvs["--input"] || !argvs["--output"]) { console.log("no input argvs:--input --output"); } else { concatFile(argvs["--input"], argvs["--output"]); } */
看脚本可知,我们生成的版本号其实是文件的MD5值的base64编码。
3、那么我们生成的版本号保存在哪里?看我们的配置文件:
Node会将生成的版本号以JS对象的形式保存在配置文件config.10086.cn.js底部。
版本号的使用
如何利用配置文件中的版本号请求静态资源?index.html上定义了公共方法loadScript:
function loadScript(path, _doc, charset, rootPath) { //fixed proxy.htm if (path === "jquery.js") { if (_doc && _doc.location.pathname.indexOf("proxy.htm") > -1) { return; } } if (_doc && !_doc.location.pathname.match(/\/m2012.+?/) && !/http:|\/m2012/.test(path) && !_doc.location.pathname.match(/\/mpost2014.+?/)) { //旧版调用 loadScriptM2011(path, _doc, charset, rootPath); return; } else { if (path.indexOf(".js") > -1) { var jsVersion = getResourceVersion(path) || "20130612"; if (window.location.href.indexOf("reload=true") > -1) { jsVersion += "_reload"; } if (path.indexOf("/") == -1) { if (rootPath) { path = rootPath + path; } else { var base = "/m2012/js"; if (path.indexOf("pack.js") > -1) { base += "/packs"; } path = base + "/" + path; } } if (path.indexOf("?") == -1) { path += "?v=" + jsVersion; } if (path.indexOf("http://") == -1 && path.indexOf("/") == 0) { path = m2012ResourceDomain + path; } } if (!charset && path.indexOf("/m2012/") > -1) { charset = "utf-8"; } } (_doc || document).write("<script jsonload='0' onload='this.setAttribute(\"jsonload\",\"1\")' " + (charset ? "charset=\"" + charset + "\" " : "") + " type=\"text/javascript\" src=\"" + path + "\"></" + "script>"); }
getResourceVersion:
function getResourceVersion(path) { if (window.Config_FileVersion) { var fileName = path.match(/[^\/\\]*$/)[0]; return Config_FileVersion[fileName] || Config_FileVersion["defaults"]; } else { return ""; } }
为了保证脚本的执行顺序,我们使用document.write写入script标签,同时又可利用浏览器的并发特性。由于邮箱采用的是IFrame架构,所以其他功能模块的IFrame页面可通过top.loadScript载入脚本。从而保证了整站引入脚本方式的统一。
四、参考资料
http://fex.baidu.com/blog/2014/04/fis-static-resource-management/
http://fex.baidu.com/blog/2014/03/fis-optimize/
posted on 2014-09-19 08:49 Hellohuman 阅读(848) 评论(0) 编辑 收藏 举报