上一章:不一样的烟火:记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端的缓存建设。