如何高效管理網站靜態資源

baidu發表於2014-04-15

 背景

  隨著網際網路開發和迭代速度越來越快,網站也變得越來越龐大,存在大量靜態資源,我們原有管理靜態資源的方式變得越來越不適用,就如同封面圖一樣,靜態資源之間的關係錯綜複雜,給工程師帶來了很多麻煩:

  • 人工管理依賴的噩夢,工程師需要頻繁管理和維護每個頁面需要的 JS & CSS 檔案,包括靜態資源之間的依賴關係以及載入順序等。
  • 效能優化成本高且不可持續性,為了提高網站效能,工程師總是在忙於優化頁面靜態資源的載入,包括動態載入靜態資源、按需載入靜態資源和修改靜態資源合併策略等,但是過了一段時間效能又降下來了,又需要周而復始的重複。
  • 靜態資源差異化的挑戰,PC和無線的適配,不同的網路和終端需要適配相應的靜態資源;當網站需要支援國際化的時候,需要對不同的國家進行差異化處理,返回不同的靜態資源,這些需求對原有的靜態資源管理方式提出巨大挑戰。
  • 缺少快速迭代和試驗新功能的有效支援,從開發到上線流程繁瑣,導致專案迭代週期長

  每天工程師都會提交大量的 new feature/bug fixes,每次專案釋出和迭代都面臨著以上的問題,是否可以有一套系統幫助我們管理/排程靜態資源來減少人工管理靜態資源成本和風險,來達到更快、更可靠、低成本的自動化專案交付。在實際專案開發中,我們進行了大量探索和試驗,實現了一套 “靜態資源管理系統”,對靜態資源進行全流程的管理和排程:

  • 幫助工程師管理靜態資源間的依賴以及資源的載入
  • 管理靜態資源版本更新與快取,自動處理CDN
  • 自動生成最優的靜態資源合併策略,實現網站自適應優化
  • 實現靜態資源的分級釋出,快速迭代,輕鬆回滾
  • 根據國際化和終端的差異,送達不同的資源給不同的使用者

  下面本文將會介紹我們是如何通過靜態資源系統來高效管理靜態資源的。

 架構

arctrhtrhtrhh1

  靜態資源管理系統主要包含Compile、Sourcemap、Backend-Framework、Frontend-Loader幾個核心模組:

  • Compile,對靜態資源進行編譯處理,包括對靜態資源進行預處理,url 處理(新增md5戳、新增CDN字首),優化(壓縮、合併),生成 Sourcemap 等
  • Sourcemap,在 compile 階段系統會掃描靜態資源,建立一張靜態資源關係表,記錄每個靜態資源的部署路徑以及依賴關係等資訊
  • Backend-Framework,後端執行時根據元件使用情況來排程靜態資源,為前端返回頁面渲染需要的資源。
  • Frontend-Loader,前端執行時根據使用者的互動行為動態請求靜態資源。

  靜態資源管理系統通過自動化工具對靜態資源進行預處理併產出 Sourcemap,SourceMap 中記錄著靜態資源的排程資訊,這樣框架在執行時會根據 SourceMap 中提供的排程資訊自動為使用者進行靜態資源排程,不僅可以做到送達不同資源給不同使用者,還可以自適應優化靜態資源合併和載入。

 自動管理靜態資源依賴

  靜態資源管理系統為工程師提供了宣告依賴關係的語法和規則,在 compile 階段系統會掃描靜態資源,建立一張靜態資源關係表,記錄每個靜態資源的部署路徑以及依賴關係等資訊。

  在html中宣告依賴

  在專案的 index.html 裡使用註釋宣告依賴關係:

<!--
    @require demo.js
    @require "demo.css"
-->

  在 SourceMap 中則可看到:

{
    "res" : {
        "demo.css" : {
            "uri" : "/static/css/demo_7defa41.css",
            "type" : "css"
        },
        "demo.js" : {
            "uri" : "/static/js/demo_33c5143.js",
            "type" : "js",
            "deps" : [ "demo.css" ]
        },
        "index.html" : {
            "uri" : "/index.html",
            "type" : "html",
            "deps" : [ "demo.js", "demo.css" ]
        }
    },
    "pkg" : {}
}

  在js中宣告依賴

  支援識別 js 檔案中的 require 函式,或者 註釋中的 @require 欄位 標記的依賴關係,這些分析處理對 html 的 script 標籤內容 同樣有效。

//demo.js
/**
 * @require demo.css
 * @require list.js
 */
var $ = require('jquery');

  在SourceMap中則可看到:

{
    "res" : {
        ...
        "demo.js" : {
            "uri" : "/static/js/demo_33c5143.js",
            "type" : "js",
            "deps" : [ "demo.css", "list.js", "jquery" ]
        },
        ...
    },
    "pkg" : {}
}

  在css中宣告依賴

  支援識別 css 檔案 註釋中的 @require 欄位 標記的依賴關係,這些分析處理對 html 的 style 標籤內容 同樣有效。

//demo.js
/**
 * @require demo.css
 * @require list.js
 */
var $ = require('jquery');

  在SourceMap中則可看到:

{
    "res" : {
        ...
        "demo.js" : {
            "uri" : "/static/js/demo_33c5143.js",
            "type" : "js",
            "deps" : [ "demo.css", "list.js", "jquery" ]
        },
        ...
    },
    "pkg" : {}
}

 按需載入靜態資源

  在靜態資源管理系統接管了專案中的靜態資源後,可以知道靜態資源的執行情況以及依賴關係,然後可以做到自動為頁面按需載入靜態資源,下面通過一個例子來詳細講解:

sidebar.tpl 中的內容如下,

<!--
    @require "common:ui/dialog/dialog.css"
-->

<a id="btn-navbar" class="btn-navbar">
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
</a>

{script}
    var sidebar = require("common:ui/dialog/dialog.js");
    sidebar.run();
{/script}

{script}
    $('a.btn-navbar').click(function() {
        require.async('common:ui/dialog/dialog.async.js', function( dialog ) {
            dialog.run();
        });
    });
{/script}

  對專案編譯後,自動化工具會分析依賴關係,並生成 sourcemap,如下

"common:widget/sidebar/sidebar.tpl": {
    "uri": "common/widget/sidebsr/sidebar.tpl",
    "type": "tpl",
    "extras": {
        "async": [
            "common:ui/dialog/dialog.async.js"
        ]
    },
    "deps": [
        "common:ui/dialog/dialog.js",
        "common:ui/dialog/dialog.css"
    ]
}

  在 sidebar 模組被呼叫後,靜態資源管理系統通過查詢 sourcemap 可以得知,當前 sidebar 模組同步依賴 sidebar.js、sidebar.css,非同步依賴 sdebar.async.js,在要輸出的 html 前面,生成靜態資源外鏈,我們得到最終的 html

<link rel="stylesheet" href="/static/ui/dialog/dialog_7defa41.css">

<a id="btn-navbar" class="btn-navbar">
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
    <span class="icon-bar"></span>
</a>

<script type="text/javascript" src="/static/common/ui/dialog/dialog$12cd4.js"></script>
<script type="text/javascript">
    require.resourceMap({
        "res": {
            "common:ui/dialog/dialog.async.js": {
                "url": "/satic/common/ui/dialog/dialog.async_449e169.js"
            }
        }
    });
</script>
<script type="text/javascript">
    var sidebar = require("common:ui/dialog/dialog.js");
    sidebar.run();

    $('a.btn-navbar').click(function() {
        require.async('common:ui/dialog/dialog.async.js', function( dialog ) {
            dialog.run();
        });
    });
</script>

  如上可見,後端模組化框架將同步模組的 script url 統一生成到頁面底部,將 css url 統一生成在 head 中,對於非同步模組(require.async)註冊 resourceMap 程式碼,框架會通過 {script} 標籤收集到頁面所有 script,統一管理並按順序輸出 script 到相應位置。

  當我們想對模組進行打包,只需要使用一個 pack 配置項,對網站的靜態資源進行打包,這樣在 SourceMap 中,所有被打包的資源會有一個 pkg 屬性指向該表中的資源,而這個資源,正是我們配置的打包策略。這樣靜態資源系統可以根據對應資訊找到某個資源最終被合併後的 package 的 url,最後把這個 url 返回給頁面。

 自動合併靜態資源

  靜態資源管理系統可以根據產品線上靜態資源使用的資料,自動完成靜態資源合併工作,對工程師完全透明,解決手工維護的未及時排除廢棄資源、不可持續、成本大等問題。

autopackageregreger2

  詳情請見 靜態資源自動合併;

 靜態資源版本更新與快取

  靜態資源管理系統採用基於檔案內容的 hash 值來控制靜態資源的版本更新,如下所示:

<script type="text/javascript" src="a_8244e91.js"></script>

  其中”_82244e91 ”這串字元是根據 a.js 的檔案內容進行 hash 運算得到的,只有檔案內容發生變化了才會有更改。這樣做的好處有:

  • 線上的 a.js 不是同名檔案覆蓋,而是檔名 +hash 的冗餘,所以可以先上線靜態資源,再上線 html 頁面,不存在間隙問題;
  • 遇到問題回滾版本的時候,無需回滾 a.js,只須回滾頁面即可;
  • 由於靜態資源版本號是檔案內容的 hash,因此所有靜態資源可以開啟永久強快取,只有更新了內容的檔案才會快取失效,快取利用率大增;
  • 修改靜態資源後會線上上產生新的檔案,一個檔案對應一個版本,因此不會受到構造 CDN 快取形式的攻擊

  靜態資源管理系統會在 compile 階段識別檔案中的定位標記(url),計算對應檔案的 hash,並自動替換為 ‘檔名 + hash’,無需工程師手動修改。

 靜態資源分級控制

  靜態資源管理系統可以對靜態資源做進一步控制(Controlling Access to Features)以達到分級釋出的效果,主要包括以下兩塊核心功能,

  • feature flags, 用來控制 feature 對應的靜態資源是否載入
  • feature flippers, 可以靈活控制 feature,不僅僅是 on 或 off, 可以做到類似’3%使用者可以訪問此功能’、’對內部所有員工開放’ 類似的效果

  通過以上的控制我們可以輕鬆做到釋出一個新功能,讓這個功能只對部分使用者可訪問,當功能完善後對所有使用者開放,如果功能出現問題直接一鍵回滾即可。

  在專案中的類似程式碼如下:

{if $config.some eq 'Fred'}
    do something new and amazing here.
{elseif $config.some eq 'Wilma'}
    do the current boring stuff.
{else}
    whatever you are.

  靜態資源管理系統會根據配置在執行時對 $config.some 進行干預.實現對靜態資源的訪問權控制,通過執行時的配置(feature flag)來控制靜態資源,還可以支援“主幹開發”的方式,來達到更快的迭代速度。

  我們還可以實現國際化的需求,原理同分級釋出,在執行時的做一些更細緻的差異化處理

{if $lang == 'zh-CN'}
    zh-CN
{/if}

 總結

  靜態資源管理系統的核心是對靜態資源進行排程,可以很靈活的適應各種效能優化和差異化處理的場景,來達到更快、更可靠、低成本的自動化專案交付。但是同時這個系統十分複雜,承載著各種職責,這個系統本身會成為整個網站的關鍵節點和瓶頸。

相關文章