版本號的管理

1、面臨的問題javascript

出於性能優化的考慮,一般資源服務器會對靜態資源的HTTP響應首部添加Expires 或者Cache-Control: max-age設置失效時間,以下圖:css

這樣,在失效時間到達以前,瀏覽器會使用緩存文件而不用從新發送HTTP請求。這就引發另外一個問題:失效時間還未到,可是咱們有新功能上線,如何通知瀏覽器棄用緩存,從新發送HTTP請求獲取最新的文件?html

2、版本號的用途java

URL有變化時,瀏覽器老是會從新發送HTTP請求嘗試獲取最新的文件。利用瀏覽器的這一特性,咱們能夠爲靜態資源的URL添加版本號來更新瀏覽器緩存。生成版本號,業界有兩種經常使用方案:node

方案1、用更新時間當作版本號,以下:jquery

<script type="text/javascript" src="m139.core.pack.js?v=20130825"></script>web

方案2、根據文件內容進行 hash 運算獲得版本號,以下:json

<script type="text/javascript" src="m139.core.pack.js?v=opMnaQdlocvGACU1GtgYoQ%3D%3D"></script>瀏覽器

方案一的優點爲實現簡單,缺點顯而易見:緩存

(1)須要手動管理版本號

(2)沒法解決CDN 緩存攻擊。什麼是CDN緩存攻擊?黑客可提早猜想到你的下一個版本號,20130825的下一個版本極可能是20130826,提早訪問你的靜態資源,讓你的CDN緩存舊文件。這樣當真正的用戶請求資源時,永遠返回的都是過時的文件。

因爲以上缺陷,139郵箱web2.0採用方案二生成版本號

3、139郵箱版本號的生成與使用

1)依賴軟件

JdkAntNode 之因此須要Jdk是由於Ant須要Jdk,因此新同窗來了首先裝軟件,配置環境變量。

2)生成過程

1Ant的入口腳本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>

注意,合併文件任務在提取版本號任務以前,咱們是正對合並後的文件內容生成版本號,緣由很簡單:咱們在頁面上引入的原本就是合併後的文件。

2Ant的生成版本號腳本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載入腳本。從而保證了整站引入腳本方式的統一。

 

4、參考資料

http://fex.baidu.com/blog/2014/04/fis-static-resource-management/

http://fex.baidu.com/blog/2014/03/fis-optimize/

相關文章
相關標籤/搜索