上一章: 不一樣的煙火:記OTT端半屏互動能力建設 | 《優酷OTT網際網路大屏前端技術實踐》第六章>>> 點選免費下載下傳 《優酷OTT網際網路大屏前端技術實踐》>>>

一、背景
目前,做2C業務的應用,更多強調SSR、用戶端緩存以及PWA等,以實作首屏加載體驗優化、秒開等性能名額,相比較而言,這些政策更加“綜合”“強壯”,如果合理運用以及借助端能力,實作冷啟動提速、首屏加載優化、秒開等不在話下。
但是筆者業務服務于“OTT端酷喵APP”前端業務,主要是酷喵APP的HTML5投放(目前更換使用Rax),而端内浏覽器并不支援service worker(PWA),且受制于端及浏覽器核心,并無zcache類似能力。至此,大寫的無奈湧上心頭,這種情況還能不能搶救一把?答案是:可以,localStorage迂回包抄方案。也介于此,本文方案誕生,雖不完美,但是終究有閃光所在。
二、方案
在localStorage出現之前,浏覽器層面可用的本地存儲隻有一個:cookie。作為前端本地存儲的獨苗,cookie在很長一段歲月裡扮演了極其重要的角色,如賬号資料存儲、狀态标記等等。然而,4kb的容量讓cookie在資源緩存道路上無力前行。随着HTML5的興起,一個嶄新的名詞出現“localStorage”,容量比cookie大千倍、同步讀寫的特點,一經面世就被認定為實用型本地存儲政策,因而被廣大開發者關注。
三、優勢與局限
1、localStorage的優勢
1) localStorage拓展了cookie的4K限制,5mb(各浏覽器略有不同);
2) 鍵值對格式,同步讀寫,使用簡單;
3) 持久存儲,不随請求發出,不影響帶寬資源。
2、localStorage的局限
1) 浏覽器支援不統一,比如IE8以上的IE版本才支援localStorage這個屬性;
2) 目前所有的浏覽器中都會把localStorage的值類型限定為string類型,這個在對我們日常比較常見的JSON對象類型需要一些轉換;
3) localStorage本質上是對字元串的讀取,大量讀寫影響浏覽器性能。
相容情況
本地緩存分析
首先5MB的容量,對于存儲前端部分常用的js、css等,如果經過系統邏輯篩選存儲,雖然不從容,但也算能用,這也是localStorage可以作為前端本地緩存的第一要素。當然這5MB的容積,“大”是相對于cookie而言,真正存儲資源則需要篩選和過濾的。比如一個網頁中,包含大量js、css、img、font檔案,這個量級是不可預估的,如果選擇全部存儲,5MB空間可能瞬間爆滿。一般而言,我們更傾向于存儲js、css這種資源(尤其是阻塞式加載的js資源)。
其次,localStorage隻能存儲字元串類型資料,這就決定了我們無法使用< script src="static/a.js">的形式加載js資源,這種情況下,我們無法拿到“檔案句柄(字元)”,同時也無法賦予其本地緩存的資源。如果要拿到js檔案句柄則需要轉換思路,使用xhr或則fetch的形式得到檔案字元。
再次,得到字元後,我們可以選擇将其存儲至localStorage的同時,進行eval或者new Function,使js代碼執行。
最後,當頁面再次打開,我們可以根據js路徑情況,判斷本地是否有緩存資源,如果有,則取出并且eval/new Function,使代碼執行;如果沒有,則使用xhr/fetch進行請求,檔案資源傳回後,存儲至localStorage并執行eval/new Function。
至此,整個緩存邏輯即告成功。下面我們以實際代碼形式進行技術方向解析
四、緩存技術實作
用戶端本地請求/緩存/加載器
1、localStorage api
設定localStorage 項,如下:
localStorage.setItem('keyName', 'keyValue');
讀取 localStorage 項,如下:
localStorage.getItem('keyName');
移除 localStorage 項,如下:
localStorage.removeItem('keyName');
移除所有localStorage 項,如下:
localStorage.clear();
2、實作原理圖:
3、主要代碼實作:
(function () {
constoHead=document.getElementsByTagName('head')[0];
const_localStorage=window.localStorage|| {};
//建立ajax函數letajax=function (_options) {
constoptions=_options|| {};
options.type= (options.type||'GET').toUpperCase();
options.dataType=options.dataType||'javascript';
constparams=options.data?formatParams(options.data) : '';
//建立-第一步letxhr;
if (window.XMLHttpRequest) {
xhr=newXMLHttpRequest();
} else {
xhr=ActiveXObject('Microsoft.XMLHTTP');
}
//在響應成功前設定一個定時器(響應逾時提示)consttimer=setTimeout(function () {
//讓後續的函數停止執行xhr.onreadystatechange=null;
console.log('timeout:'+options.url);
options.error&&options.error(status);
}, options.timeout||8000);
//接收-第三步xhr.onreadystatechange=function () {
if (xhr.readyState==4) {
clearTimeout(timer);
conststatus=xhr.status;
if (status>=200&&status<300) {
options.success&&options.success(xhr.responseText, xhr.responseXML);
} else {
options.error&&options.error(status);
}
}
}
//連接配接和發送-第二步if (options.type=='GET') {
xhr.open('GET', options.url+'?'+params, true);
//xhr.setRequestHeader("Accept-Encoding", "gzip");xhr.send(null);
} elseif (options.type=='POST') {
xhr.open('POST', options.url, true);
//設定表單送出時的内容類型xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(params);
}
}
//格式化參數letformatParams=function (data) {
letarr= [];
for (letnameindata) {
arr.push(encodeURIComponent(name) +'='+encodeURIComponent(data[name]));
}
//arr.push(('v=' + Math.random()).replace('.',''));returnarr.join('&');
}
//時間戳轉日期格式 letformatDateTime=function (time, format) {
constt= (time&&newDate(time)) ||newDate();
lettf=function (i) {
return (i<10?'0' : '') +i
};
returnformat.replace(/YYYY|MM|DD|hh|mm|ss/g, function (a) {
switch (a) {
case'YYYY':
returntf(t.getFullYear());
break;
case'MM':
returntf(t.getMonth() +1);
break;
case'DD':
returntf(t.getDate());
break;
case'hh':
returntf(t.getHours());
break;
case'mm':
returntf(t.getMinutes());
break;
case'ss':
returntf(t.getSeconds());
break;
}
})
};
lethandleError=function (url, callback, _send) {
letscript=document.createElement('script');
script.type='text/javascript';
script.onload=script.onreadystatechange=function () {
if (!this.readyState||this.readyState==="loaded"||this.readyState==="complete") {
console.log('create script loaded : '+url);
callback&&callback();
_send&&_send();
script.onload=script.onreadystatechange=null;
}
};
script.onerror=function () {
console.log('create script error : '+url);
_send&&_send();
script.onload=null;
}
script.src=url;
oHead.appendChild(script);
}
//eval js字元串代碼let_eval=function (fnString) {
window['eval'].call(window, fnString);
}
letautoDel=function () {
if (!_localStorage.setItem) {
return;
}
for (letkeyin_localStorage) {
//console.log('第'+ (i+1) +'條資料的鍵值為:' + _localStorage.key(i) +',資料為:' + _localStorage.getItem(_localStorage.key(i)));//let key = _localStorage.key(i);constvalue=key.indexOf('##') >-1?_localStorage.getItem(key) : '';
constisSetExpire=value.split('##').length>1;
constdate=isSetExpire&&value.split('##')[1];
constisExpire=date&&nowDateNum>+date.replace(/-/ig, '');
if (isExpire) {
_localStorage.removeItem(key);
_localStorage.removeItem(key+'_data');
console.log('DEL:'+key+'|'+key+'_data');
}
}
}
letonIndex=-1;
letsend=function (list, index) {
constnum=list.length;
if (!num||num<index+1) {
autoDel();
return;
}
if (index<=onIndex) {
return;
}
onIndex=index;
constitem_url=list[index].url;
constitem_aliases=list[index].aliases;
constcallback=list[index].callback;
constisStorage=list[index].storage!==false ;
// if (!_localStorage) {// handleError(item_url, callback);// send(list, index + 1);// return;// }constisDone=item_url=== (_localStorage.getItem&&_localStorage.getItem(item_aliases));
if (isDone) {
constfnString=_localStorage.getItem(item_aliases+'_data');
try {
_eval(fnString);
} catch (e) {
console.log('eval error');
}
callback&&callback();
console.log('local:'+item_aliases);
send(list, index+1);
return;
}
ajax({
url: item_url,
success: function (response, xml) {
//請求成功後執行try {
_eval(response);
} catch (e) {
console.log('eval error');
}
//window['eval'].call(window, response);// ( window.execScript || function( script ) {// window[ 'eval' ].call( window, script );// } )( response );callback&&callback();
console.log('ajax:'+item_aliases);
constisSetExpire=item_url.split('##').length>1;
constdate=isSetExpire&&item_url.split('##')[1];
constisExpire=date&&nowDateNum>+date.replace(/-/ig, '');
if (isStorage&&_localStorage.setItem&&!isExpire) {
try {
_localStorage.setItem(item_aliases, item_url);
_localStorage.setItem(item_aliases+'_data', response);
} catch (oException) {
if (oException.name=='QuotaExceededError') {
console.log('超出本地存儲限額!');
_localStorage.clear();
_localStorage.setItem(item_aliases, item_url);
_localStorage.setItem(item_aliases+'_data', response);
}
}
}
send(list, index+1);
},
error: function (status) {
//失敗後執行console.log('ajax '+item_aliases+' error');
handleError(item_url.replace('2580', ''), callback, function () {
send(list, index+1);
});
setTimeout(function () {
send(list, index+1);
}, 300)
}
});
}
constnowDateNum=+formatDateTime(false, 'YYYYMMDD');
send(window.__page_static, 0);
})();
//一期實作:localStorage緩存/存儲;//後續加強:PWA/indexeddb(待定);//後續加強:引入前端靜态資源版本diff算法,局部更新本地版本;
4、使用方法:
• 4.1-配置靜态資源别名
window.__page_static= [
{
aliases: 'FocusEngine',
url:'//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js'
},
{
aliases: 'alitv-h5-system',
url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js',
callback: ()=>{ window.Page.init() );
}
]
• 4.2-引入種子檔案:
<script charset="utf-8" src="//g.alicdn.com/de/local_cache/0.0.1/page/localStorage/index-min.js"></script>
• 4.3、過期設定:
window.__page_static= [
{
aliases: 'FocusEngine',
url:'//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js##2018-08-10'
},
{
aliases: 'alitv-h5-system',
url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js##2018-08-30',
callback: ()=>{ window.Page.init() );
}
]
tips:
每次加載TVcache後,TVcache通過xhr方式請求得到js資源,并自動根據“##”分割得到有效期:
1、當無“##日期”時,則預設長期緩存;
2、當“##日期”大于請求日期時,則請求會資源後,正常存入local;
3、當“##日期”小于請求日期時,則請求會資源後,不存入local,并在所有請求結束後,檢索local,删除之;
• 4.4、禁用本地緩存:
window.__page_static= [
{
aliases: 'FocusEngine',
url: '//g.alicdn.com/de/focus-engine/2.0.20/FocusEngine.min.js##2018-08-10',
storage: false
},
{
aliases: 'alitv-h5-system',
url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js##2018-08-30',
storage: falsecallback: ()=>{ window.Page.init() };
}
]
• 4.5、删除本地緩存:
方法1:同别名檔案,修改檔案版本号如
前期布設:
window.__page_static= [
{
aliases: 'alitv-h5-system',
url: '//g.alicdn.com/de/alitv-h5-system/1.4.9/page/main/index-min.js'
}
]
更新布設:
window.__page_static= [
{
aliases: 'alitv-h5-system',
url: '//g.alicdn.com/de/alitv-h5-system/1.4.10/page/main/index-min.js',
}
]
方法2:手動設定删除數組 ( 不推薦 )
window.__page_static_del= ['alitv-h5-system']
業務應用
OTT端酷喵APP-H5投放頁
五、總結
我們上文提到,該方案将資源檔案緩存到本地,之後項目頁面再次請求時與本地版本号比對,之後進行決定是用本地緩存還是重新fetch。而在筆者所從事的OTT端HTML5業務,是将“焦點引擎檔案”、“架構檔案”、“公共元件檔案”預設存儲于本地,業務檔案視情況配置。
經過該部署方案的投放,我們監測顯示,冷啟動平均提速30%,尤其在弱網情況下更加明顯,而相應使load逾時情況大大降低。
然而,技術總是在進步的,近一兩年阿裡集團内2C業務,H5逐漸被Weex/Rax技術棧取代,相應的在OTT端H5緩存的技術探索也失去了業務依托,當然這并不是結束。換句話說,這是新的探索的開始,在OTT端Rax能力建立建設中,我們也開啟了新的緩存方案建設,并且取得了一定的成績,後續筆者将繼續行文介紹,Rax在OTT端的緩存建設。