前端性能優(yōu)化之瀏覽器存儲(chǔ)

一、cookie

因?yàn)镠TTP請(qǐng)求無(wú)狀態(tài),所以需要cookie去維持客戶端狀態(tài)
過(guò)期時(shí)間 expire
cookie的生成方式

  1. http response header中的set-cookie
  2. js中可以通過(guò)document.cookie可以讀寫cookie
    僅僅作為瀏覽器存儲(chǔ)(大小4KB左右,能力被localstorage替代)
    cookie中在相關(guān)域名下面 —— cdn的流量損耗,所以我們需要將cdn與主域名獨(dú)立起來(lái)

種cookie

document.cookie = 'username=hello'

二、LocalStorage

HTML5設(shè)計(jì)出來(lái)專門用于瀏覽器存儲(chǔ)的
大小為5M左右
僅在客戶端使用,不和服務(wù)端進(jìn)行通信
接口封裝較好
瀏覽器本地緩存方案
存儲(chǔ)js文件

var testJsContent = localStorage.getItem('test')
    if(testJsContent) {
      eval(testJsContent)
    } else {
      axios.get('./test.js').then(res => {
        var responseText = res
        eval(responseText)
        localStorage.setItem('test', responseText)
      })
    }

三、SessionStorage

會(huì)話級(jí)別的瀏覽器存儲(chǔ)
大小為5M左右
僅在客戶端使用,不和服務(wù)端進(jìn)行通信
接口封裝較好
對(duì)于表單信息的維護(hù)

四、IndexedDB

IndexedDB 是一種低級(jí)API,用于客戶端存儲(chǔ)大量結(jié)構(gòu)化數(shù)據(jù)。該API使用索引來(lái)實(shí)現(xiàn)對(duì)該數(shù)據(jù)的高性能搜索。雖然 Web Storage 對(duì)于存儲(chǔ)較少量的數(shù)據(jù)很有用,但對(duì)于存儲(chǔ)更大量的結(jié)構(gòu)化數(shù)據(jù)來(lái)說(shuō),這種方法不太有用。IndexedDB提供了一個(gè)解決方案。
為應(yīng)用創(chuàng)建離線版本

使用方法參考:http://www.ruanyifeng.com/blog/2018/07/indexeddb.html

五、PWA

PWA (Progressive Web Apps) 是一種 Web App 新模型,并不是具體指某一種前沿的技術(shù)或者某一個(gè)單一的知識(shí)點(diǎn),我們從英文縮寫來(lái)看就能看出來(lái),這是一個(gè)漸進(jìn)式的 Web App,是通過(guò)一系列新的 Web 特性,配合優(yōu)秀的 UI 交互設(shè)計(jì),逐步的增強(qiáng) Web App 的用戶體驗(yàn)。
PWA優(yōu)點(diǎn):

  1. 可靠:在沒(méi)有網(wǎng)絡(luò)的環(huán)境中也能提供基本的頁(yè)面訪問(wèn),而不會(huì)出現(xiàn)“未連接到互聯(lián)網(wǎng)”的頁(yè)面。
  2. 快速:針對(duì)網(wǎng)頁(yè)渲染及網(wǎng)絡(luò)數(shù)據(jù)訪問(wèn)有較好優(yōu)化。
  3. 融入(Engaging):應(yīng)用可以被增加到手機(jī)桌面,并且和普通應(yīng)用一樣有全屏、推送等特性。

Manifest實(shí)現(xiàn)添加至主屏幕
index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

{
  "name": "Minimal PWA", // 必填 顯示的插件名稱
  "short_name": "PWA Demo", // 可選  在APP launcher和新的tab頁(yè)顯示,如果沒(méi)有設(shè)置,則使用name
  "description": "The app that helps you understand PWA", //用于描述應(yīng)用
  "display": "standalone", // 定義開(kāi)發(fā)人員對(duì)Web應(yīng)用程序的首選顯示模式。standalone模式會(huì)有單獨(dú)的
  "start_url": "/", // 應(yīng)用啟動(dòng)時(shí)的url
  "theme_color": "#313131", // 桌面圖標(biāo)的背景色
  "background_color": "#313131", // 為web應(yīng)用程序預(yù)定義的背景顏色。在啟動(dòng)web應(yīng)用程序和加載應(yīng)用程序的內(nèi)容之間創(chuàng)建了一個(gè)平滑的過(guò)渡。
  "icons": [ // 桌面圖標(biāo),是一個(gè)數(shù)組
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的圖片尺寸
    "type": "image/webp"  // 幫助userAgent快速排除不支持的類型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}

Manifest參考文檔:https://developer.mozilla.org/zh-CN/docs/Web/Manifest

可以打開(kāi)網(wǎng)站https://developers.google.cn/web/showcase/2015/chrome-dev-summit查看添加至主屏幕的動(dòng)圖。

如果用的是安卓手機(jī),可以下載chrome瀏覽器自己操作看看

六、Service Worker

Service Worker 是一個(gè)腳本,瀏覽器獨(dú)立于當(dāng)前網(wǎng)頁(yè),將其在后臺(tái)運(yùn)行,為實(shí)現(xiàn)一些不依賴頁(yè)面或者用戶交互的特性打開(kāi)了一扇大門。在未來(lái)這些特性將包括推送消息,背景后臺(tái)同步, geofencing(地理圍欄定位),但它將推出的第一個(gè)首要特性,就是攔截和處理網(wǎng)絡(luò)請(qǐng)求的能力,包括以編程方式來(lái)管理被緩存的響應(yīng)。

參見(jiàn)文檔:https://segmentfault.com/a/1190000012353473?utm_source=tag-newest#articleHeader4

查看瀏覽器Service Worker使用情況
chrome://serviceworker-internals/ 已安裝Service Worker的詳細(xì)情況
chrome://inspect/#service-workers

Service Workers 就像介于服務(wù)器和網(wǎng)頁(yè)之間的攔截器,能夠攔截進(jìn)出的HTTP 請(qǐng)求,從而完全控制你的網(wǎng)站。
最主要的特點(diǎn):

  1. 在頁(yè)面中注冊(cè)并安裝成功后,運(yùn)行于瀏覽器后臺(tái),不受頁(yè)面刷新的影響,可以監(jiān)聽(tīng)和截?cái)r作用域范圍內(nèi)所有頁(yè)面的 HTTP 請(qǐng)求。
  2. 網(wǎng)站必須使用 HTTPS。除了使用本地開(kāi)發(fā)環(huán)境調(diào)試時(shí)(如域名使用 localhost)
    運(yùn)行于瀏覽器后臺(tái),可以控制打開(kāi)的作用域范圍下所有的頁(yè)面請(qǐng)求
  3. 單獨(dú)的作用域范圍,單獨(dú)的運(yùn)行環(huán)境和執(zhí)行線程
  4. 不能操作頁(yè)面 DOM。但可以通過(guò)事件機(jī)制來(lái)處理
  5. 事件驅(qū)動(dòng)型服務(wù)線程

為什么要求網(wǎng)站必須是HTTPS的,大概是因?yàn)閟ervice worker權(quán)限太大能攔截所有頁(yè)面的請(qǐng)求吧,如果http的網(wǎng)站安裝service worker很容易被攻擊

瀏覽器支持情況詳見(jiàn): https://caniuse.com/#feat=serviceworkers

Service Worker生命周期如下圖:


image.png

當(dāng)用戶首次導(dǎo)航至 URL 時(shí),服務(wù)器會(huì)返回響應(yīng)的網(wǎng)頁(yè)。

  1. 當(dāng)你調(diào)用 register() 函數(shù)時(shí), Service Worker 開(kāi)始下載。
  2. 在注冊(cè)過(guò)程中,瀏覽器會(huì)下載、解析并執(zhí)行 Service Worker ()。如果在此步驟中出現(xiàn)任何錯(cuò)誤,register() 返回的 promise 都會(huì)執(zhí)行 reject 操作,并且 Service Worker 會(huì)被廢棄。
  3. 一旦 Service Worker 成功執(zhí)行了,install 事件就會(huì)激活
  4. 安裝完成,Service Worker 便會(huì)激活,并控制在其范圍內(nèi)的一切。如果生命周期中的所有事件都成功了,Service Worker 便已準(zhǔn)備就緒,隨時(shí)可以使用。

離線緩存
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img src="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async src="/js/script.js"></script>     
    <script>
      // 注冊(cè) service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 注冊(cè)成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 注冊(cè)失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

注:Service Worker 的注冊(cè)路徑?jīng)Q定了其 scope 默認(rèn)作用頁(yè)面的范圍。
如果 service-worker.js 是在 /sw/ 頁(yè)面路徑下,這使得該 Service Worker 默認(rèn)只會(huì)收到 頁(yè)面/sw/ 路徑下的 fetch 事件。
如果存放在網(wǎng)站的根路徑下,則將會(huì)收到該網(wǎng)站的所有 fetch 事件。
如果希望改變它的作用域,可在第二個(gè)參數(shù)設(shè)置 scope 范圍。示例中將其改為了根目錄,即對(duì)整個(gè)站點(diǎn)生效。

service-worker.js

var cacheName = 'helloWorld';     // 緩存的名稱  
// install 事件,它發(fā)生在瀏覽器安裝并注冊(cè) Service Worker 時(shí)        
self.addEventListener('install', event => { 
/* event.waitUtil 用于在安裝成功之前執(zhí)行一些預(yù)裝邏輯
 但是建議只做一些輕量級(jí)和非常重要資源的緩存,減少安裝失敗的概率
 安裝成功后 ServiceWorker 狀態(tài)會(huì)從 installing 變?yōu)?installed */
  event.waitUntil(
    caches.open(cacheName)                  
    .then(cache => cache.addAll([    // 如果所有的文件都成功緩存了,便會(huì)安裝完成。如果任何文件下載失敗了,那么安裝過(guò)程也會(huì)隨之失敗。        
      '/js/script.js',
      '/images/hello.png'
    ]))
  );
});
  
/**
為 fetch 事件添加一個(gè)事件監(jiān)聽(tīng)器。接下來(lái),使用 caches.match() 函數(shù)來(lái)檢查傳入的請(qǐng)求 URL 是否匹配當(dāng)前緩存中存在的任何內(nèi)容。如果存在的話,返回緩存的資源。
如果資源并不存在于緩存當(dāng)中,通過(guò)網(wǎng)絡(luò)來(lái)獲取資源,并將獲取到的資源添加到緩存中。
*/
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request)                  
    .then(function (response) {
      if (response) {                            
        return response;                         
      }
      var requestToCache = event.request.clone();  //          
      return fetch(requestToCache).then(                   
        function (response) {
          if (!response || response.status !== 200) {      
            return response;
          }
          var responseToCache = response.clone();          
          caches.open(cacheName)                           
            .then(function (cache) {
              cache.put(requestToCache, responseToCache);  
            });
          return response;             
    })
  );
});

注:為什么用request.clone()和response.clone()
需要這么做是因?yàn)閞equest和response是一個(gè)流,它只能消耗一次。因?yàn)槲覀円呀?jīng)通過(guò)緩存消耗了一次,然后發(fā)起 HTTP 請(qǐng)求還要再消耗一次,所以我們需要在此時(shí)克隆請(qǐng)求
Clone the request—a request is a stream and can only be consumed once.

serice worker實(shí)現(xiàn)消息推送

  1. 提示用戶并獲得他們的訂閱詳細(xì)信息
  2. 將這些詳細(xì)信息保存在服務(wù)器上
  3. 在需要時(shí)發(fā)送任何消息

步驟一和步驟二
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很復(fù)雜,但是可以不用具體看,知識(shí)用來(lái)轉(zhuǎn)化vapidPublicKey用
      function urlBase64ToUint8Array(base64String) {                                  
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, '+')
          .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 注冊(cè)失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步驟三 服務(wù)器發(fā)送消息給service worker
app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 發(fā)送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker監(jiān)聽(tīng)push事件,將通知詳情推送給用戶
service-worker.js

self.addEventListener('push', function (event) {
 // 檢查服務(wù)端是否發(fā)來(lái)了任何有效載荷數(shù)據(jù)
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息來(lái)顯示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});

七、workbox3

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標(biāo)準(zhǔn)。 注意:講述HT...
    kismetajun閱讀 28,774評(píng)論 1 45
  • Cookie 因?yàn)镠TTP請(qǐng)求無(wú)狀態(tài),所以需要cookie去維持客戶端狀態(tài), 過(guò)期時(shí)間 expire cookie...
    Ethan__Hunt閱讀 547評(píng)論 0 1
  • 很棒,新年開(kāi)了一個(gè)好頭,1號(hào)和2號(hào)都在11點(diǎn)半前就睡覺(jué)啦,堅(jiān)持早睡早起身體好~ 2號(hào)獨(dú)自一人去逛了圓明園,景色很好...
    用甲第為國(guó)相閱讀 368評(píng)論 0 1
  • 2017.6.28 今日一邊聽(tīng)課一邊大手指揉按趾縫,按得整個(gè)腳前掌熱熱的癢,心也癢癢的,大手指充滿欲望。到起身那會(huì)...
    潘語(yǔ)閱讀 328評(píng)論 2 8
  • 日子久了,難免會(huì)煩悶。人的心理很復(fù)雜,快樂(lè)時(shí)害怕幸福難以持久,苦惱時(shí)又自我安慰。不甘平庸,又感激平凡。所...
    賈方舟閱讀 422評(píng)論 0 5

友情鏈接更多精彩內(nèi)容