天天看點

OTT端性能優化建設之本地緩存設計 | 《優酷OTT網際網路大屏前端技術實踐》第七章

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

點選免費下載下傳

《優酷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本質上是對字元串的讀取,大量讀寫影響浏覽器性能。

相容情況

OTT端性能優化建設之本地緩存設計 | 《優酷OTT網際網路大屏前端技術實踐》第七章

本地緩存分析

首先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、實作原理圖:

OTT端性能優化建設之本地緩存設計 | 《優酷OTT網際網路大屏前端技術實踐》第七章

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端的緩存建設。

繼續閱讀