OTT端性能优化建设之本地缓存设计 | 《优酷OTT互联网大屏前端技术实践》第七章-阿里云开发者社区

开发者社区> 阿里文娱技术> 正文

OTT端性能优化建设之本地缓存设计 | 《优酷OTT互联网大屏前端技术实践》第七章

简介: 目前,做2C业务的应用,更多强调SSR、客户端缓存以及PWA等,以实现首屏加载体验优化、秒开等性能指标,相比较而言,这些策略更加“综合”“强壮”,如果合理运用以及借助端能力,实现冷启动提速、首屏加载优化、秒开等不在话下。 但是笔者业务服务于“OTT端酷喵APP”前端业务,主要是酷喵APP的HTML5投放(目前更换使用Rax),而端内浏览器并不支持service worker(PWA),且受制于端及浏览器内核,并无zcache类似能力。至此,大写的无奈涌上心头,这种情况还能不能抢救一把?答案是:可以,localStorage迂回包抄方案。也介于此,本文方案诞生,虽不完美,但是终究有闪光所在。

上一章:不一样的烟火:记OTT端半屏互动能力建设 | 《优酷OTT互联网大屏前端技术实践》第六章>>>

点击免费下载
《优酷OTT互联网大屏前端技术实践》>>>

test

作者| 阿里巴巴文娱技术 魏家鲁

一、背景

目前,做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本质上是对字符串的读取,大量读写影响浏览器性能。

兼容情况

image.png

本地缓存分析

首先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、实现原理图:

image.png

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端的缓存建设。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:

带你了解阿里文娱技术

官方博客
官网链接