如何高效地管理網站靜態資源
背景
隨著互聯網開發和迭代速度越來越快,網站也變得越來越龐大,存在大量靜態資源,我們原有管理靜態資源的方式變得越來越不適用,就如同封面圖一樣,靜態資源之間的關系錯綜復雜,給工程師帶來了很多麻煩:
- 人工管理依賴的噩夢,工程師需要頻繁管理和維護每個頁面需要的 JS & CSS 文件,包括靜態資源之間的依賴關系以及加載順序等。
- 性能優化成本高且不可持續性,為了提高網站性能,工程師總是在忙于優化頁面靜態資源的加載,包括動態加載靜態資源、按需加載靜態資源和修改靜態資源合并策略等,但是過了一段時間性能又降下來了,又需要周而復始的重復。
- 靜態資源差異化的挑戰,PC和無線的適配,不同的網絡和終端需要適配相應的靜態資源;當網站需要支持國際化的時候,需要對不同的國家進行差異化處理,返回不同的靜態資源,這些需求對原有的靜態資源管理方式提出巨大挑戰。
- 缺少快速迭代和試驗新功能的有效支持,從開發到上線流程繁瑣,導致項目迭代周期長
每天工程師都會提交大量的 new feature/bug fixes,每次項目發布和迭代都面臨著以上的問題,是否可以有一套系統幫助我們管理/調度靜態資源來減少人工管理靜態資源成本和風險,來達到更快、更可靠、低成本的自動化項目交付。在實際項目開發中,我們進行了大量探索和試驗,實現了一套 “靜態資源管理系統”,對靜態資源進行全流程的管理和調度:
- 幫助工程師管理靜態資源間的依賴以及資源的加載
- 管理靜態資源版本更新與緩存,自動處理CDN
- 自動生成最優的靜態資源合并策略,實現網站自適應優化
- 實現靜態資源的分級發布,快速迭代,輕松回滾
- 根據國際化和終端的差異,送達不同的資源給不同的用戶
下面本文將會介紹我們是如何通過靜態資源系統來高效管理靜態資源的。
架構
靜態資源管理系統主要包含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" : {}
- }
#p#
按需加載靜態資源
在靜態資源管理系統接管了項目中的靜態資源后,可以知道靜態資源的運行情況以及依賴關系,然后可以做到自動為頁面按需加載靜態資源,下面通過一個例子來詳細講解:
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 返回給頁面。
自動合并靜態資源
靜態資源管理系統可以根據產品線上靜態資源使用的數據,自動完成靜態資源合并工作,對工程師完全透明,解決手工維護的未及時排除廢棄資源、不可持續、成本大等問題。
詳情請見 靜態資源自動合并;
靜態資源版本更新與緩存
靜態資源管理系統采用基于文件內容的 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}
總結
靜態資源管理系統的核心是對靜態資源進行調度,可以很靈活的適應各種性能優化和差異化處理的場景,來達到更快、更可靠、低成本的自動化項目交付。但是同時這個系統十分復雜,承載著各種職責,這個系統本身會成為整個網站的關鍵節點和瓶頸。
作者:walter (http://weibo.com/u/1916384703) - F.I.S
原文鏈接:http://fex.baidu.com/blog/2014/04/fis-static-resource-management/