如何高效地管理網(wǎng)站靜態(tài)資源
背景
隨著互聯(lián)網(wǎng)開發(fā)和迭代速度越來(lái)越快,網(wǎng)站也變得越來(lái)越龐大,存在大量靜態(tài)資源,我們?cè)泄芾盱o態(tài)資源的方式變得越來(lái)越不適用,就如同封面圖一樣,靜態(tài)資源之間的關(guān)系錯(cuò)綜復(fù)雜,給工程師帶來(lái)了很多麻煩:
- 人工管理依賴的噩夢(mèng),工程師需要頻繁管理和維護(hù)每個(gè)頁(yè)面需要的 JS & CSS 文件,包括靜態(tài)資源之間的依賴關(guān)系以及加載順序等。
- 性能優(yōu)化成本高且不可持續(xù)性,為了提高網(wǎng)站性能,工程師總是在忙于優(yōu)化頁(yè)面靜態(tài)資源的加載,包括動(dòng)態(tài)加載靜態(tài)資源、按需加載靜態(tài)資源和修改靜態(tài)資源合并策略等,但是過(guò)了一段時(shí)間性能又降下來(lái)了,又需要周而復(fù)始的重復(fù)。
- 靜態(tài)資源差異化的挑戰(zhàn),PC和無(wú)線的適配,不同的網(wǎng)絡(luò)和終端需要適配相應(yīng)的靜態(tài)資源;當(dāng)網(wǎng)站需要支持國(guó)際化的時(shí)候,需要對(duì)不同的國(guó)家進(jìn)行差異化處理,返回不同的靜態(tài)資源,這些需求對(duì)原有的靜態(tài)資源管理方式提出巨大挑戰(zhàn)。
- 缺少快速迭代和試驗(yàn)新功能的有效支持,從開發(fā)到上線流程繁瑣,導(dǎo)致項(xiàng)目迭代周期長(zhǎng)
每天工程師都會(huì)提交大量的 new feature/bug fixes,每次項(xiàng)目發(fā)布和迭代都面臨著以上的問(wèn)題,是否可以有一套系統(tǒng)幫助我們管理/調(diào)度靜態(tài)資源來(lái)減少人工管理靜態(tài)資源成本和風(fēng)險(xiǎn),來(lái)達(dá)到更快、更可靠、低成本的自動(dòng)化項(xiàng)目交付。在實(shí)際項(xiàng)目開發(fā)中,我們進(jìn)行了大量探索和試驗(yàn),實(shí)現(xiàn)了一套 “靜態(tài)資源管理系統(tǒng)”,對(duì)靜態(tài)資源進(jìn)行全流程的管理和調(diào)度:
- 幫助工程師管理靜態(tài)資源間的依賴以及資源的加載
- 管理靜態(tài)資源版本更新與緩存,自動(dòng)處理CDN
- 自動(dòng)生成最優(yōu)的靜態(tài)資源合并策略,實(shí)現(xiàn)網(wǎng)站自適應(yīng)優(yōu)化
- 實(shí)現(xiàn)靜態(tài)資源的分級(jí)發(fā)布,快速迭代,輕松回滾
- 根據(jù)國(guó)際化和終端的差異,送達(dá)不同的資源給不同的用戶
下面本文將會(huì)介紹我們是如何通過(guò)靜態(tài)資源系統(tǒng)來(lái)高效管理靜態(tài)資源的。
架構(gòu)
靜態(tài)資源管理系統(tǒng)主要包含Compile、Sourcemap、Backend-Framework、Frontend-Loader幾個(gè)核心模塊:
- Compile,對(duì)靜態(tài)資源進(jìn)行編譯處理,包括對(duì)靜態(tài)資源進(jìn)行預(yù)處理,url 處理(添加md5戳、添加CDN前綴),優(yōu)化(壓縮、合并),生成 Sourcemap 等
- Sourcemap,在 compile 階段系統(tǒng)會(huì)掃描靜態(tài)資源,建立一張靜態(tài)資源關(guān)系表,記錄每個(gè)靜態(tài)資源的部署路徑以及依賴關(guān)系等信息
- Backend-Framework,后端運(yùn)行時(shí)根據(jù)組件使用情況來(lái)調(diào)度靜態(tài)資源,為前端返回頁(yè)面渲染需要的資源。
- Frontend-Loader,前端運(yùn)行時(shí)根據(jù)用戶的交互行為動(dòng)態(tài)請(qǐng)求靜態(tài)資源。
靜態(tài)資源管理系統(tǒng)通過(guò)自動(dòng)化工具對(duì)靜態(tài)資源進(jìn)行預(yù)處理并產(chǎn)出 Sourcemap,SourceMap 中記錄著靜態(tài)資源的調(diào)度信息,這樣框架在運(yùn)行時(shí)會(huì)根據(jù) SourceMap 中提供的調(diào)度信息自動(dòng)為用戶進(jìn)行靜態(tài)資源調(diào)度,不僅可以做到送達(dá)不同資源給不同用戶,還可以自適應(yīng)優(yōu)化靜態(tài)資源合并和加載。
自動(dòng)管理靜態(tài)資源依賴
靜態(tài)資源管理系統(tǒng)為工程師提供了聲明依賴關(guān)系的語(yǔ)法和規(guī)則,在 compile 階段系統(tǒng)會(huì)掃描靜態(tài)資源,建立一張靜態(tài)資源關(guān)系表,記錄每個(gè)靜態(tài)資源的部署路徑以及依賴關(guān)系等信息。
在html中聲明依賴
在項(xiàng)目的 index.html 里使用注釋聲明依賴關(guān)系:
- <!--
- @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中聲明依賴
支持識(shí)別 js 文件中的 require 函數(shù),或者 注釋中的 @require 字段 標(biāo)記的依賴關(guān)系,這些分析處理對(duì) html 的 script 標(biāo)簽內(nèi)容 同樣有效。
- //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中聲明依賴
支持識(shí)別 css 文件 注釋中的 @require 字段 標(biāo)記的依賴關(guān)系,這些分析處理對(duì) html 的 style 標(biāo)簽內(nèi)容 同樣有效。
- //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#
按需加載靜態(tài)資源
在靜態(tài)資源管理系統(tǒng)接管了項(xiàng)目中的靜態(tài)資源后,可以知道靜態(tài)資源的運(yùn)行情況以及依賴關(guān)系,然后可以做到自動(dòng)為頁(yè)面按需加載靜態(tài)資源,下面通過(guò)一個(gè)例子來(lái)詳細(xì)講解:
sidebar.tpl 中的內(nèi)容如下,
- <!--
- @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}
對(duì)項(xiàng)目編譯后,自動(dòng)化工具會(huì)分析依賴關(guān)系,并生成 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 模塊被調(diào)用后,靜態(tài)資源管理系統(tǒng)通過(guò)查詢 sourcemap 可以得知,當(dāng)前 sidebar 模塊同步依賴 sidebar.js、sidebar.css,異步依賴 sdebar.async.js,在要輸出的 html 前面,生成靜態(tài)資源外鏈,我們得到最終的 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 統(tǒng)一生成到頁(yè)面底部,將 css url 統(tǒng)一生成在 head 中,對(duì)于異步模塊(require.async)注冊(cè) resourceMap 代碼,框架會(huì)通過(guò) {script} 標(biāo)簽收集到頁(yè)面所有 script,統(tǒng)一管理并按順序輸出 script 到相應(yīng)位置。
當(dāng)我們想對(duì)模塊進(jìn)行打包,只需要使用一個(gè) pack 配置項(xiàng),對(duì)網(wǎng)站的靜態(tài)資源進(jìn)行打包,這樣在 SourceMap 中,所有被打包的資源會(huì)有一個(gè) pkg 屬性指向該表中的資源,而這個(gè)資源,正是我們配置的打包策略。這樣靜態(tài)資源系統(tǒng)可以根據(jù)對(duì)應(yīng)信息找到某個(gè)資源最終被合并后的 package 的 url,最后把這個(gè) url 返回給頁(yè)面。
自動(dòng)合并靜態(tài)資源
靜態(tài)資源管理系統(tǒng)可以根據(jù)產(chǎn)品線上靜態(tài)資源使用的數(shù)據(jù),自動(dòng)完成靜態(tài)資源合并工作,對(duì)工程師完全透明,解決手工維護(hù)的未及時(shí)排除廢棄資源、不可持續(xù)、成本大等問(wèn)題。
詳情請(qǐng)見 靜態(tài)資源自動(dòng)合并;
靜態(tài)資源版本更新與緩存
靜態(tài)資源管理系統(tǒng)采用基于文件內(nèi)容的 hash 值來(lái)控制靜態(tài)資源的版本更新,如下所示:
- <script type="text/javascript" src="a_8244e91.js"></script>
其中”_82244e91 ”這串字符是根據(jù) a.js 的文件內(nèi)容進(jìn)行 hash 運(yùn)算得到的,只有文件內(nèi)容發(fā)生變化了才會(huì)有更改。這樣做的好處有:
- 線上的 a.js 不是同名文件覆蓋,而是文件名 +hash 的冗余,所以可以先上線靜態(tài)資源,再上線 html 頁(yè)面,不存在間隙問(wèn)題;
- 遇到問(wèn)題回滾版本的時(shí)候,無(wú)需回滾 a.js,只須回滾頁(yè)面即可;
- 由于靜態(tài)資源版本號(hào)是文件內(nèi)容的 hash,因此所有靜態(tài)資源可以開啟永久強(qiáng)緩存,只有更新了內(nèi)容的文件才會(huì)緩存失效,緩存利用率大增;
- 修改靜態(tài)資源后會(huì)在線上產(chǎn)生新的文件,一個(gè)文件對(duì)應(yīng)一個(gè)版本,因此不會(huì)受到構(gòu)造 CDN 緩存形式的攻擊
靜態(tài)資源管理系統(tǒng)會(huì)在 compile 階段識(shí)別文件中的定位標(biāo)記(url),計(jì)算對(duì)應(yīng)文件的 hash,并自動(dòng)替換為 '文件名 + hash',無(wú)需工程師手動(dòng)修改。
靜態(tài)資源分級(jí)控制
靜態(tài)資源管理系統(tǒng)可以對(duì)靜態(tài)資源做進(jìn)一步控制(Controlling Access to Features)以達(dá)到分級(jí)發(fā)布的效果,主要包括以下兩塊核心功能,
- feature flags, 用來(lái)控制 feature 對(duì)應(yīng)的靜態(tài)資源是否加載
- feature flippers, 可以靈活控制 feature,不僅僅是 on 或 off, 可以做到類似'3%用戶可以訪問(wèn)此功能'、'對(duì)內(nèi)部所有員工開放' 類似的效果
通過(guò)以上的控制我們可以輕松做到發(fā)布一個(gè)新功能,讓這個(gè)功能只對(duì)部分用戶可訪問(wèn),當(dāng)功能完善后對(duì)所有用戶開放,如果功能出現(xiàn)問(wèn)題直接一鍵回滾即可。
在項(xiàng)目中的類似代碼如下:
- {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.
靜態(tài)資源管理系統(tǒng)會(huì)根據(jù)配置在運(yùn)行時(shí)對(duì) $config.some 進(jìn)行干預(yù).實(shí)現(xiàn)對(duì)靜態(tài)資源的訪問(wèn)權(quán)控制,通過(guò)運(yùn)行時(shí)的配置(feature flag)來(lái)控制靜態(tài)資源,還可以支持“主干開發(fā)”的方式,來(lái)達(dá)到更快的迭代速度。
我們還可以實(shí)現(xiàn)國(guó)際化的需求,原理同分級(jí)發(fā)布,在運(yùn)行時(shí)的做一些更細(xì)致的差異化處理
- {if $lang == 'zh-CN'}
- zh-CN
- {/if}
總結(jié)
靜態(tài)資源管理系統(tǒng)的核心是對(duì)靜態(tài)資源進(jìn)行調(diào)度,可以很靈活的適應(yīng)各種性能優(yōu)化和差異化處理的場(chǎng)景,來(lái)達(dá)到更快、更可靠、低成本的自動(dòng)化項(xiàng)目交付。但是同時(shí)這個(gè)系統(tǒng)十分復(fù)雜,承載著各種職責(zé),這個(gè)系統(tǒng)本身會(huì)成為整個(gè)網(wǎng)站的關(guān)鍵節(jié)點(diǎn)和瓶頸。
作者:walter (http://weibo.com/u/1916384703) - F.I.S
原文鏈接:http://fex.baidu.com/blog/2014/04/fis-static-resource-management/