RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

什么是PWA?PWA離線方案研究分析

OSC開源社區(qū) ? 來源:京東云開發(fā)者-CHO 張鵬程 ? 2023-12-19 09:53 ? 次閱讀

作者:京東云開發(fā)者-CHO 張鵬程

本文并不是介紹如何將一個(gè)網(wǎng)頁配置成離線應(yīng)用并支持安裝下載的。研究 PWA 的目的僅僅是為了保證用戶的資源可以直接從本地加載,來忽略全國或者全球網(wǎng)絡(luò)質(zhì)量對頁面加載速度造成影響。當(dāng)然,如果頁面上所需的資源,除了資源文件外并不需要任何的網(wǎng)絡(luò)請求,那它除了不支持安裝到桌面,已經(jīng)算是一個(gè)離線應(yīng)用了。

什么是 PWA

PWA(Progressive Web App)是一種結(jié)合了網(wǎng)頁和原生應(yīng)用程序功能的新型應(yīng)用程序開發(fā)方法。PWA 通過使用現(xiàn)代 Web 技術(shù),例如 Service Worker 和 Web App Manifest,為用戶提供了類似原生應(yīng)用的體驗(yàn)。 從用戶角度來看,PWA 具有以下特點(diǎn): 1.可離線訪問:PWA 可以在離線狀態(tài)下加載和使用,使用戶能夠在沒有網(wǎng)絡(luò)連接的情況下繼續(xù)瀏覽應(yīng)用; 2.可安裝:用戶可以將 PWA 添加到主屏幕,就像安裝原生應(yīng)用一樣,方便快捷地訪問; 3.推送通知:PWA 支持推送通知功能,可以向用戶發(fā)送實(shí)時(shí)更新和提醒; 4.響應(yīng)式布局:PWA 可以適應(yīng)不同設(shè)備和屏幕大小,提供一致的用戶體驗(yàn)。

從開發(fā)者角度來看,PWA 具有以下優(yōu)勢: 1.跨平臺(tái)開發(fā):PWA 可以在多個(gè)平臺(tái)上運(yùn)行,無需單獨(dú)開發(fā)不同的應(yīng)用程序; 2.更新便捷:PWA 的更新可以通過服務(wù)器端更新 Service Worker 來實(shí)現(xiàn),用戶無需手動(dòng)更新應(yīng)用; 3.可發(fā)現(xiàn)性:PWA 可以通過搜索引擎進(jìn)行索引,增加應(yīng)用的可發(fā)現(xiàn)性; 4.安全性:PWA 使用 HTTPS 協(xié)議傳輸數(shù)據(jù),提供更高的安全性。 總之,PWA 是一種具有離線訪問、可安裝、推送通知和響應(yīng)式布局等特點(diǎn)的新型應(yīng)用開發(fā)方法,為用戶提供更好的體驗(yàn),為開發(fā)者帶來更高的效率。 我們從 PWA 的各種能力中,聚焦下其可離線訪問的能力。

Service Worker

離線加載本質(zhì)上是頁面所需的各種js、css以及頁面本身的html,都可以緩存到本地,不再從網(wǎng)絡(luò)上請求。這個(gè)能力是通過Service Worker來實(shí)現(xiàn)的。 Service Worker 是一種在瀏覽器背后運(yùn)行的腳本,用于處理網(wǎng)絡(luò)請求和緩存數(shù)據(jù)。它可以攔截和處理網(wǎng)頁請求,使得網(wǎng)頁能夠在離線狀態(tài)下加載和運(yùn)行。Service Worker 可以緩存資源,包括 HTML、CSS、JavaScript 和圖像等,從而提供更快的加載速度和離線訪問能力。它還可以實(shí)現(xiàn)推送通知和后臺(tái)同步等功能,為 Web 應(yīng)用帶來更強(qiáng)大的功能和用戶體驗(yàn)。 某些情況下,Service Worker 和瀏覽器插件的 background 很相似,但在功能和使用方式上有一些區(qū)別:

功能差異:Service Worker 主要用于處理網(wǎng)絡(luò)請求和緩存數(shù)據(jù),可以攔截和處理網(wǎng)頁請求,實(shí)現(xiàn)離線訪問和資源緩存等功能。而瀏覽器插件的 background 主要用于擴(kuò)展瀏覽器功能,例如修改頁面、攔截請求、操作 DOM 等。

運(yùn)行環(huán)境:Service Worker 運(yùn)行在瀏覽器的后臺(tái),獨(dú)立于網(wǎng)頁運(yùn)行。它可以在網(wǎng)頁關(guān)閉后繼續(xù)運(yùn)行,并且可以在多個(gè)頁面之間共享狀態(tài)。而瀏覽器插件的 background 也在后臺(tái)運(yùn)行,但是它的生命周期與瀏覽器窗口相關(guān),關(guān)閉瀏覽器窗口后插件也會(huì)被終止。

權(quán)限限制:由于安全考慮,Service Worker 受到一定的限制,無法直接訪問 DOM,只能通過 postMessage () 方法與網(wǎng)頁進(jìn)行通信。而瀏覽器插件的 background 可以直接操作 DOM,對頁面有更高的控制權(quán)。

總的來說,Service Worker 更適合用于處理網(wǎng)絡(luò)請求和緩存數(shù)據(jù),提供離線訪問和推送通知等功能;而瀏覽器插件的 background 則更適合用于擴(kuò)展瀏覽器功能,操作頁面 DOM,攔截請求等。

注冊

注冊一個(gè) Service Worker 其實(shí)是非常簡單的,下面舉個(gè)簡單的例子




  Service Worker 示例


  



// service-worker.js

// 定義需要預(yù)緩存的文件列表
const filesToCache =[
  '/',
  '/index.html',
  '/styles.css',
  '/script.js',
  '/image.jpg'
];

// 安裝Service Worker時(shí)進(jìn)行預(yù)緩存
self.addEventListener('install',function(event){
  event.waitUntil(
    caches.open('my-cache')
      .then(function(cache){
        return cache.addAll(filesToCache);
      })
  );
});

// 激活Service Worker
self.addEventListener('activate',function(event){
  event.waitUntil(
    caches.keys().then(function(cacheNames){
      return Promise.all(
        cacheNames.filter(function(cacheName){
          return cacheName !=='my-cache';
        }).map(function(cacheName){
          return caches.delete(cacheName);
        })
      );
    })
  );
});

// 攔截fetch事件并從緩存中返回響應(yīng)
self.addEventListener('fetch',function(event){
  event.respondWith(
    caches.match(event.request)
      .then(function(response){
        return response ||fetch(event.request);
      })
  );
});


上述示例中,注冊 Service Worker 的邏輯包含在 HTML 文件的 同時(shí)調(diào)整下sw的攔截邏輯。
// 新增runtime緩存
const runtimeCacheName ='runtime-cache-'+ version;

// 符合條件也是緩存優(yōu)先,但是每次都重新發(fā)起網(wǎng)絡(luò)請求更新緩存
constisStaleWhileRevalidate =(request)=>{
  const url = request.url;
  const index =['http://127.0.0.1:5500/mock.js'].indexOf(url);
  return index !==-1;
};

self.addEventListener('fetch',function(event){
  event.respondWith(
    // 嘗試從緩存中獲取響應(yīng)
    caches.match(event.request).then(function(response){
      var fetchPromise =fetch(event.request).then(function(networkResponse){

        // 符合匹配條件才克隆響應(yīng)并將其添加到緩存中
        if(isStaleWhileRevalidate(event.request)){
          var responseToCache = networkResponse.clone();
          caches.open(runtimeCacheName).then(function(cache){
            cache.put(event.request, responseToCache.clone());
          });
        }
        return networkResponse;
      });

      // 返回緩存的響應(yīng),然后更新緩存中的響應(yīng)
      return response || fetchPromise;
    })
  );
});

現(xiàn)在每次用戶打開新的頁面,

優(yōu)先從緩存中獲取資源,同時(shí)發(fā)起一個(gè)網(wǎng)絡(luò)請求

有緩存則直接返回緩存,沒有則返回一個(gè)fetchPromise

fetchPromise內(nèi)部更新符合緩存條件的請求

用戶下一次打開新頁面或刷新當(dāng)前頁面,就會(huì)展示最新的內(nèi)容

通過修改isStaleWhileRevalidate中 url 的匹配條件,就能夠控制是否更新緩存。在上面的示例中,我們可以將index.html從precache列表中移除,放入runtime中,或者專門處理下index.html的放置規(guī)則,去更新precache中的緩存。最好不要出現(xiàn)多個(gè)緩存桶中存在同一個(gè)request的緩存,那樣就不知道走的到底是哪個(gè)緩存了。 一般來說,微前端的應(yīng)用,資源文件都有個(gè)固定的存放位置,文件本身通過在文件名上增加hash或版本號(hào)來進(jìn)行區(qū)分。我們在isStaleWhileRevalidate函數(shù)中匹配存放資源位置的路徑,這樣用戶在第二次打開頁面時(shí),就可以直接使用緩存了。如果是內(nèi)嵌頁面,可以與平臺(tái)溝通,是否可以在應(yīng)用冷起的時(shí)候,偷偷訪問一個(gè)資源頁面,提前進(jìn)行預(yù)加載,這樣就能在首次打開的時(shí)候也享受本地緩存了。

緩存過期

即使我們緩存了一些資源文件,例如 Iconfont、字體庫等只會(huì)更新自身內(nèi)容,但不會(huì)變化名稱的文件。僅使用Stale-While-Revalidate其實(shí)也是可以的。用戶會(huì)在第二次打開頁面時(shí)看到最新的內(nèi)容。 但為了提高一些體驗(yàn),例如,用戶半年沒打開頁面了,突然在今天打開了一下,展示歷史的內(nèi)容就不太合適了,這時(shí)候可以增加一個(gè)緩存過期的策略。 如果我們使用的是Workbox,通過使用ExpirationPlugin來實(shí)現(xiàn)的。ExpirationPlugin是Workbox中的一個(gè)緩存插件,它允許為緩存條目設(shè)置過期時(shí)間。示例如下所示

import{ registerRoute }from'workbox-routing';
import{ CacheFirst, StaleWhileRevalidate }from'workbox-strategies';
import{ ExpirationPlugin }from'workbox-expiration';

// 設(shè)置緩存的有效期為一小時(shí)
const cacheExpiration ={
  maxAgeSeconds:60*60,// 一小時(shí)
};

// 使用CacheFirst策略,并應(yīng)用ExpirationPlugin
registerRoute(
  ({ request })=> request.destination ==='image',
  newCacheFirst({
    cacheName:'image-cache',
    plugins:[
      newExpirationPlugin(cacheExpiration),
    ],
  })
);

// 使用StaleWhileRevalidate策略,并應(yīng)用ExpirationPlugin
registerRoute(
  ({ request })=> request.destination ==='script',
  newStaleWhileRevalidate({
    cacheName:'script-cache',
    plugins:[
      newExpirationPlugin(cacheExpiration),
    ],
  })
);

或者我們可以實(shí)現(xiàn)一下自己的緩存過期策略。首先是增加緩存過期時(shí)間。在原本的更新緩存的基礎(chǔ)上,設(shè)置自己的cache-control,然后再放入緩存中。示例中直接刪除了原本的cache-control,真正使用中,需要判斷下,比如no-cache類型的資源,就不要使用緩存了。 每次命中緩存時(shí),都會(huì)判斷下是否過期,如果過期,則直接返回從網(wǎng)絡(luò)中獲取的最新的請求,并更新緩存。
self.addEventListener('fetch',function(event){
  event.respondWith(
    // 嘗試從緩存中獲取響應(yīng)
    caches.match(event.request).then(function(response){
      var fetchPromise =fetch(event.request).then(function(networkResponse){
        if(isStaleWhileRevalidate(event.request)){
          // 檢查響應(yīng)的狀態(tài)碼是否為成功
          if(networkResponse.status ===200){
            // 克隆響應(yīng)并將其添加到緩存中
            var clonedResponse = networkResponse.clone();
            // 在存儲(chǔ)到緩存之前,設(shè)置正確的緩存頭部
            var headers =newHeaders(networkResponse.headers);
            headers.delete('cache-control');
            headers.append('cache-control','public, max-age=3600');// 設(shè)置緩存有效期為1小時(shí)

            // 創(chuàng)建新的響應(yīng)對象并存儲(chǔ)到緩存中
            var cachedResponse =newResponse(clonedResponse.body,{
              status: networkResponse.status,
              statusText: networkResponse.statusText,
              headers: headers,
            });

            caches.open(runtimeCacheName).then((cache)=>{
              cache.put(event.request, cachedResponse);
            });
          }
        }
        return networkResponse;
      });

      // 檢查緩存的響應(yīng)是否存在且未過期
      if(response &&!isExpired(response)){
        return response;// 返回緩存的響應(yīng)
      }
      return fetchPromise;
    })
  );
});

functionisExpired(response){
  // 從響應(yīng)的headers中獲取緩存的有效期信息
  var cacheControl = response.headers.get('cache-control');
  if(cacheControl){
    var maxAgeMatch = cacheControl.match(/max-age=(d+)/);
    if(maxAgeMatch){
      var maxAgeSeconds =parseInt(maxAgeMatch[1],10);
      var requestTime = Date.parse(response.headers.get('date'));
      var expirationTime = requestTime + maxAgeSeconds *1000;

      // 檢查當(dāng)前時(shí)間是否超過了緩存的有效期
      if(Date.now()< expirationTime){
        returnfalse;// 未過期
      }
    }
  }

  returntrue;// 已過期
}

從 Service Worker 發(fā)起的請求,可能會(huì)被瀏覽器自身的內(nèi)存緩存或硬盤緩存捕獲,然后直接返回。

精確清理緩存

下面的內(nèi)容,默認(rèn)為微前端應(yīng)用。 隨著微前端應(yīng)用的更新,會(huì)逐漸出現(xiàn)失效的資源文件一直出現(xiàn)在緩存中,時(shí)間長了可能會(huì)導(dǎo)致緩存溢出。

定時(shí)更新

例如以半年為期限,定期更新sw文件的版本號(hào),每次更新都會(huì)一刀切的將上一個(gè)版本中的動(dòng)態(tài)緩存干掉,此操作會(huì)導(dǎo)致下次加載變慢,因?yàn)闀?huì)重新通過網(wǎng)絡(luò)請求的方式加載來創(chuàng)建緩存。但如果更新頻率控制得當(dāng),并且資源拆分合理,用戶感知不會(huì)很大。

處理不常用緩存

上文中的緩存過期策略,并不適用于此處。因?yàn)槲⒎?wù)中資源文件中,只要文件名不變,內(nèi)容就應(yīng)該不變。我們只是期望刪除超過一定時(shí)間沒有使用的條目,防止緩存溢出。這里也使用Stale-While-Revalidate的原因是為了幫助我們識(shí)別長期不使用的js文件,方便刪除。 本來可以使用self.registration.periodicSync.register來創(chuàng)建一個(gè)周期性任務(wù),但是由于兼容性問題,放棄了。需要的可自行研究,附上網(wǎng)址。 這里我們換一個(gè)條件。每當(dāng)有網(wǎng)絡(luò)請求被觸發(fā)時(shí),啟動(dòng)一個(gè)延遲 20s 的debounce函數(shù),來處理緩存問題。先把之前的清除舊版本緩存的函數(shù)改名成clearOldResources。然后設(shè)定緩存過期時(shí)間為 10s,刷新兩次頁面來觸發(fā)網(wǎng)路請求,20s 之后,runtime緩存中的mock.js就會(huì)被刪除了。真實(shí)場景下,延遲函數(shù)和緩存過期都不會(huì)這么短,可以設(shè)置成 5min 和 3 個(gè)月。

functiondebounce(func, delay){
  let timerId;

  returnfunction(...args){
    clearTimeout(timerId);

    timerId =setTimeout(()=>{
      func.apply(this, args);
    }, delay);
  };
}

const clearOutdateResources =debounce(function(){
  cache
    .open(runtimeCacheName)
    .keys()
    .then(function(requests){
      requests.forEach(function(request){
        cache.match(request).then(function(response){
          // response為匹配到的Response對象
          if(isExpiredWithTime(response,10)){
            cache.delete(request);
          }
        });
      });
    });
});

functionisExpiredWithTime(response, time){
  var requestTime = Date.parse(response.headers.get('date'));
  if(!requestTime){
    returnfalse;
  }
  var expirationTime = requestTime + time *1000;

  // 檢查當(dāng)前時(shí)間是否超過了緩存的有效期
  if(Date.now()< expirationTime){
    returnfalse;// 未過期
  }
  returntrue;// 已過期
}

重新總結(jié)下微前端應(yīng)用下的緩存配置: 1.使用版本號(hào),并初始化preCache和runtimeCache 2.preCache中預(yù)緩存基座數(shù)據(jù),使用Cache First策略,sw不更新則基座數(shù)據(jù)不更新 3.runtimeCache使用Stale-While-Revalidate策略負(fù)責(zé)動(dòng)態(tài)緩存業(yè)務(wù)資源的數(shù)據(jù),每次訪問頁面都動(dòng)態(tài)更新一次 4.使用debounce函數(shù),每次訪問頁面都會(huì)延遲清除過期的緩存 5.如果需要更新preCache中的基座數(shù)據(jù),則需要升級(jí)版本號(hào)并重新安裝sw文件。新服務(wù)激活后會(huì)刪除上一個(gè)版本的數(shù)據(jù) 6.runtimeCache和preCache不能同時(shí)存儲(chǔ)一個(gè)資源,否則可能導(dǎo)致混亂。

最終示例

下面是最終的sw.js,我刪除掉了緩存過期的邏輯,如有需要請自行從上文代碼中獲取。順便我增加了一點(diǎn)點(diǎn)喪心病狂的錯(cuò)誤處理邏輯。 理論上,index.html應(yīng)該放入預(yù)緩存的列表里,但我懶得寫在Stale-While-Revalidate里分別更新preCache和runtimeCache了,相信看完上面內(nèi)容的你,一定可以自己實(shí)現(xiàn)對應(yīng)邏輯。 如果你用了下面的文件,每次刷新完頁面的 20s 后,runtime 的緩存就會(huì)被清空,因?yàn)槲覀冞^期時(shí)間只設(shè)置了 10s。而每次發(fā)起請求后的 20s 后就會(huì)進(jìn)行過期判斷。 在真實(shí)的驗(yàn)證過程中,有部分

const version ='v1';

const preCacheName ='pre-cache-'+ version;
const runtimeCacheName ='runtime-cache';// runtime不進(jìn)行整體清除

const filesToCache =[];// 這里將index.html放到動(dòng)態(tài)緩存里了,為了搭自動(dòng)更新的便車。這個(gè)小項(xiàng)目也沒別的需要預(yù)緩存的了

const maxAgeSeconds =10;// 緩存過期時(shí)間,單位s

const debounceClearTime =20;// 延遲清理緩存時(shí)間,單位s

// 符合條件也是緩存優(yōu)先,但是每次都重新發(fā)起網(wǎng)絡(luò)請求更新緩存
constisStaleWhileRevalidate =(request)=>{
  const url = request.url;
  const index =[`${self.location.origin}/mock.js`,`${self.location.origin}/index.html`].indexOf(url);
  return index !==-1;
};

/*********************上面是配置代碼***************************** */

constaddResourcesToCache =async()=>{
  return caches.open(preCacheName).then((cache)=>{
    return cache.addAll(filesToCache);
  });
};

// 安裝Service Worker時(shí)進(jìn)行預(yù)緩存
self.addEventListener('install',function(event){
  event.waitUntil(
    addResourcesToCache().then(()=>{
      self.skipWaiting();
    })
  );
});

// 刪除上個(gè)版本的數(shù)據(jù)
asyncfunctionclearOldResources(){
  return caches.keys().then(function(cacheNames){
    return Promise.all(
      cacheNames
        .filter(function(cacheName){
          return![preCacheName, runtimeCacheName].includes(cacheName);
        })
        .map(function(cacheName){
          return caches.delete(cacheName);
        })
    );
  });
}

// 激活Service Worker
self.addEventListener('activate',function(event){
  event.waitUntil(
    clearOldResources().finally(()=>{
      self.clients.claim();
      clearOutdateResources();
    })
  );
});

// 緩存優(yōu)先
constisCacheFirst =(request)=>{
  const url = request.url;
  const index = filesToCache.findIndex((u)=> url.includes(u));
  return index !==-1;
};

functionaddToCache(cacheName, request, response){
  try{
    caches.open(cacheName).then((cache)=>{
      cache.put(request, response);
    });
  }catch(error){
    console.error('add to cache error =>', error);
  }
}

asyncfunctioncacheFirst(request){
  try{
    return caches
      .match(request)
      .then((response)=>{
        if(response){
          return response;
        }

        returnfetch(request).then((response)=>{
          // 檢查是否成功獲取到響應(yīng)
          if(!response || response.status !==200){
            return response;// 返回原始響應(yīng)
          }

          var clonedResponse = response.clone();
          addToCache(runtimeCacheName, request, clonedResponse);
          return response;
        });
      })
      .catch(()=>{
        console.error('match in cacheFirst error', error);
        returnfetch(request);
      });
  }catch(error){
    console.error(error);
    returnfetch(request);
  }
}

// 緩存優(yōu)先,同步更新
asyncfunctionhandleFetch(request){
  try{
    clearOutdateResources();
    // 嘗試從緩存中獲取響應(yīng)
    return caches.match(request).then(function(response){
      var fetchPromise =fetch(request).then(function(networkResponse){
        // 檢查響應(yīng)的狀態(tài)碼是否為成功
        if(!networkResponse || networkResponse.status !==200){
          return networkResponse;
        }
        // 克隆響應(yīng)并將其添加到緩存中
        var clonedResponse = networkResponse.clone();
        addToCache(runtimeCacheName, request, clonedResponse);

        return networkResponse;
      });

      // 返回緩存的響應(yīng),然后更新緩存中的響應(yīng)
      return response || fetchPromise;
    });
  }catch(error){
    console.error(error);
    returnfetch(request);
  }
}

self.addEventListener('fetch',function(event){
  const{ request }= event;

  if(isCacheFirst(request)){
    event.respondWith(cacheFirst(request));
    return;
  }
  if(isStaleWhileRevalidate(request)){
    event.respondWith(handleFetch(request));
    return;
  }
});

functiondebounce(func, delay){
  let timerId;

  returnfunction(...args){
    clearTimeout(timerId);

    timerId =setTimeout(()=>{
      func.apply(this, args);
    }, delay);
  };
}

const clearOutdateResources =debounce(function(){
  try{
    caches.open(runtimeCacheName).then((cache)=>{
      cache.keys().then(function(requests){
        requests.forEach(function(request){
          cache.match(request).then(function(response){
            const isExpired =isExpiredWithTime(response, maxAgeSeconds);
            if(isExpired){
              cache.delete(request);
            }
          });
        });
      });
    });
  }catch(error){
    console.error('clearOutdateResources error => ', error);
  }
}, debounceClearTime *1000);

functionisExpiredWithTime(response, time){
  var requestTime = Date.parse(response.headers.get('date'));
  if(!requestTime){
    returnfalse;
  }
  var expirationTime = requestTime + time *1000;

  // 檢查當(dāng)前時(shí)間是否超過了緩存的有效期
  if(Date.now()< expirationTime){
    returnfalse;// 未過期
  }
  returntrue;// 已過期
}

注意

在真實(shí)的驗(yàn)證過程中,有部分資源獲取不到date這個(gè)數(shù)據(jù),因此為了保險(xiǎn),我們還是在存入緩存時(shí),自己補(bǔ)充一個(gè)存入時(shí)間

// 克隆響應(yīng)并將其添加到緩存中
var clonedResponse = networkResponse.clone();
// 在存儲(chǔ)到緩存之前,設(shè)置正確的緩存頭部
var headers =newHeaders(networkResponse.headers);

headers.append('sw-save-date', Date.now()); 

// 創(chuàng)建新的響應(yīng)對象并存儲(chǔ)到緩存中
var cachedResponse =newResponse(clonedResponse.body,{
  status: networkResponse.status,
  statusText: networkResponse.statusText,
  headers: headers,
});

在判斷過期時(shí),取我們自己寫入的key即可。
functionisExpiredWithTime(response, time){
  var requestTime =Number(response.headers.get('sw-save-date'));
  if(!requestTime){
    returnfalse;
  }
  var expirationTime = requestTime + time *1000;
  // 檢查當(dāng)前時(shí)間是否超過了緩存的有效期
  if(Date.now()< expirationTime){
    returnfalse;// 未過期
  }
  returntrue;// 已過期
}

不可見響應(yīng)

還記得上面為了安全考慮,在存入緩存時(shí),對響應(yīng)的狀態(tài)做了判斷,非 200 的都不緩存。然后就又發(fā)現(xiàn)異常場景了。

// 檢查是否成功獲取到響應(yīng)
if(!response || response.status !==200){
  return response;// 返回原始響應(yīng)
}

opaque響應(yīng)通常指的是跨源請求(CORS)中的一種情況,在該情況下,瀏覽器出于安全考慮,不允許訪問服務(wù)端返回的響應(yīng)內(nèi)容。opaque響應(yīng)通常發(fā)生在服務(wù)工作者(Service Workers)進(jìn)行的跨源請求中,且沒有 CORS 頭部的情況下。 opaque響應(yīng)的特征是:

響應(yīng)的內(nèi)容無法被 JavaScript 訪問。

響應(yīng)的大小無法確定,因此 Chrome 開發(fā)者工具中會(huì)顯示為 (opaque)。

響應(yīng)的狀態(tài)碼通常是 0,即使實(shí)際上服務(wù)器可能返回了不同的狀態(tài)碼。

因此我們需要做一些補(bǔ)充動(dòng)作。不單是補(bǔ)充cors模式,還得同步設(shè)置下credentials。

const newRequest =
  request.url ==='index.html'
    ? request
    :newRequest(request,{ mode:'cors', credentials:'omit'});

在 Service Workers 發(fā)起網(wǎng)絡(luò)請求時(shí),如果頁面本身需要認(rèn)證,那就像上面代碼那樣,對頁面請求做個(gè)判斷。request.url === 'index.html'是我寫的示例,真實(shí)請求中,需要拼出完整的 url 路徑。而對于資源文件,走非認(rèn)證的cors請求即可。將請求的request改為我們變更后的newRequest,請求資源就可以正常的被緩存了。
var fetchPromise =fetch(newRequest).then(function(networkResponse)

銷毀

離線緩存用得好升職加薪,用不好就刪庫跑路。除了上面的一點(diǎn)點(diǎn)的防錯(cuò)邏輯,整體的降級(jí)方案一定要有。 看到這里,應(yīng)該已經(jīng)忘了 Service Worker 是如何被注冊上的吧。沒事,我們看個(gè)新的腳本。在原本的基礎(chǔ)上,我們加了個(gè)變量SW_FALLBACK,如果離線緩存出問題了,趕緊到管理后臺(tái),把對應(yīng)的值改成true。讓用戶多刷新兩次就好了。只要不是徹底的崩潰導(dǎo)致html無法更新,這個(gè)方案就沒問題。

// 如果有問題,將此值改成true
SW_FALLBACK=false;
 
if('serviceWorker'in navigator){
  if(!SW_FALLBACK){
    navigator.serviceWorker
      .register('/eemf-service-worker.js')
      .then((registration)=>{
        console.log('Service Worker 注冊成功!');
      })
      .catch((error)=>{
        console.log('Service Worker 注冊失?。?, error);
      });
  }else{
    navigator.serviceWorker.getRegistration('/').then((reg)=>{
      reg && reg.unregister();
      if(reg){
        window.location.reload();
      }
    });
  }
}

對于沒有管理后臺(tái)配置html的項(xiàng)目,可以將上面的腳本移動(dòng)到sw-register.js的腳本中,在html以script的形式加載該腳本,并將該文件緩存設(shè)置為no-cache,也不要在sw中緩存該文件。這樣出問題后,覆寫下該文件即可。

總結(jié)

所有要說的,在上面都說完了。PWA 的離線方案,是一種很好的解決方案,但是也有其局限性。本項(xiàng)目所用的 demo 已經(jīng)上傳到了github,可自行查看。

審核編輯:黃飛

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • Web
    Web
    +關(guān)注

    關(guān)注

    2

    文章

    1262

    瀏覽量

    69440
  • 網(wǎng)絡(luò)連接
    +關(guān)注

    關(guān)注

    0

    文章

    88

    瀏覽量

    10870
  • HTML
    +關(guān)注

    關(guān)注

    0

    文章

    278

    瀏覽量

    35205
收藏 人收藏

    評論

    相關(guān)推薦

    tp-link pwa2701ntl 2801n電力線無線路由器使用說明

    tp-link pwa2701ntl 2801n電力線無線路由器使用說明:
    發(fā)表于 04-23 15:56 ?68次下載
    tp-link <b class='flag-5'>pwa</b>2701ntl 2801n電力線無線路由器使用說明

    微軟確認(rèn)在Win10 RS4版本更新中添加PWA支持

    近日,微軟正式宣布會(huì)在 Windows 10 Redstone 4版本中帶PWA應(yīng)用程序的全面支持。 先來解釋一下什么叫PWA(Progressive Web App)。PWA 其實(shí)是在普通的移動(dòng)
    的頭像 發(fā)表于 02-18 20:54 ?5675次閱讀

    Chrome瀏覽器的PWA應(yīng)用加入返回和重新加載功能

    此前,微軟已在 Chromium Edge 瀏覽器中,為漸進(jìn)式 Web 應(yīng)用(WPA)添加了后退按鈕和跳轉(zhuǎn)列表支持?,F(xiàn)在,谷歌 Chromium 團(tuán)隊(duì)亦為 Chrome 瀏覽器上的 PWA
    的頭像 發(fā)表于 10-24 14:38 ?3484次閱讀

    微軟Windows 10啟動(dòng)時(shí)PWA應(yīng)用可以自動(dòng)運(yùn)行

    微軟一直在努力使Progressive Web Apps(PWA)更像本機(jī)原生應(yīng)用程序,包括將它們添加到“開始”菜單中,并允許用戶使用通常的Windows工具卸載它們。
    的頭像 發(fā)表于 01-10 10:40 ?2733次閱讀

    谷歌嘗試全新的PWA,帶標(biāo)欄的UI

    昨日,外媒報(bào)道了微軟希望在 Windows 平臺(tái)上支持 PWA(漸進(jìn)式 Web 應(yīng)用)自啟運(yùn)行的消息。作為 Web Apps 正在進(jìn)行中的另一個(gè)開發(fā)路徑,Google 這邊也正在嘗試全新的 PWA 體驗(yàn) —— 當(dāng)啟用標(biāo)簽頁的條帶時(shí),PW
    的頭像 發(fā)表于 01-10 16:04 ?1680次閱讀

    Chrome OS中Google Play Store PWA應(yīng)用即將支持應(yīng)用內(nèi)購

    目前,Chrome OS 已經(jīng)支持運(yùn)行 Android 和 Linux 應(yīng)用程序,但并未提及 PWA 等基于網(wǎng)頁的應(yīng)用程序。由于它們沒有一個(gè)統(tǒng)一、受信任的應(yīng)用商城,為此谷歌一直嘗試在 Play
    的頭像 發(fā)表于 12-10 16:18 ?1939次閱讀

    用戶現(xiàn)在可從Win10設(shè)置菜單中卸載Chrome PWA應(yīng)用

    微軟已經(jīng)努力讓 PWA 應(yīng)用的工作方式盡可能接近常規(guī)的原生應(yīng)用,其創(chuàng)新之一是讓 Windows 用戶從 Windows 10 設(shè)置菜單中卸載 Edge PWA 應(yīng)用,微軟現(xiàn)在已經(jīng)通過 Open
    的頭像 發(fā)表于 12-14 10:01 ?1244次閱讀

    谷歌正在測試隱藏 PWA 應(yīng)用的狀態(tài)欄 或很快移除

    早些時(shí)候,Techdows 報(bào)道了可從“設(shè)置”或“控制面板”中卸載 Chrome 瀏覽器的漸進(jìn)式 Web 應(yīng)用(簡稱 PWA)。不過現(xiàn)在,我們又見到了 Chrome Canary 中引入的又一項(xiàng)改動(dòng)
    的頭像 發(fā)表于 01-18 17:24 ?1956次閱讀

    YouTube 現(xiàn)已支持 PWA 網(wǎng)頁應(yīng)用,Chrome 瀏覽器直接安裝

    1月24日消息 YouTube 在 Win10 系統(tǒng)上一直沒有獨(dú)立的應(yīng)用程序,但根據(jù)外媒 MSPoweruser 消息,YouTube 近日推出了 PWA 網(wǎng)頁版應(yīng)用,安裝后直接在桌面上出現(xiàn)快捷方式
    的頭像 發(fā)表于 01-25 16:48 ?3638次閱讀

    桌面版Firefox將不再開發(fā)類PWA應(yīng)用

    Mozilla 似乎決定在桌面端 Firefox 上停止對類 Progressive Web Apps(PWA)應(yīng)用的開發(fā)。援引外媒 Fast Company 報(bào)道,Mozilla 計(jì)劃停止開發(fā)名為“site-specific browsers”(網(wǎng)站特定瀏覽,簡稱 SSB)功能,該功能目前依然處于 Flag 實(shí)驗(yàn)
    的頭像 發(fā)表于 01-29 15:57 ?1666次閱讀

    微軟與谷歌合作 Chrome 瀏覽器崩潰重啟后 PWA 應(yīng)用將自動(dòng)恢復(fù)運(yùn)行

    2月18日消息 外媒 Windows Latest 報(bào)道,微軟正在與谷歌合作進(jìn)行一項(xiàng)新的改變,將改善基于 Chrome 的 PWA 應(yīng)用的體驗(yàn)。在 Chromium 中添加該功能后,PWA 將被正確
    的頭像 發(fā)表于 02-18 15:22 ?1537次閱讀

    谷歌將強(qiáng)制要求PWA應(yīng)用提供離線體驗(yàn)

    在今天發(fā)布的 Chrome 89 穩(wěn)定版更新中,谷歌已經(jīng)開始提醒開發(fā)者要為他們的可安裝漸進(jìn)式網(wǎng)頁應(yīng)用(PWA) 提供離線體驗(yàn)。此前,開發(fā)者可以在某些情況下規(guī)避這一條件,但現(xiàn)在谷歌將會(huì)在開發(fā)者工具
    的頭像 發(fā)表于 03-03 15:51 ?1641次閱讀

    谷歌正測試YouTube PWA 可支持下載視頻、離線觀看

    近日,有媒體爆料消息稱,谷歌公司目前正在進(jìn)行 YouTube 實(shí)驗(yàn),YouTube已經(jīng)可以作為 PWA 使用。YouTube用戶要求的首要功能之一,就是可以在筆記本電腦和混合設(shè)備上離線觀看視頻。
    的頭像 發(fā)表于 11-29 09:46 ?2355次閱讀

    Microsoft Teams PWA現(xiàn)已在Linux上可用

    因此,幾個(gè)月前,微軟決定在 2022 年 12 月淘汰現(xiàn)有的 Teams Linux 桌面客戶端,并專注于用新的基于 PWA(Progressive Web App 漸進(jìn)式 Web 應(yīng)用)的輕量級(jí)客戶端替代它。而現(xiàn)在,這已經(jīng)實(shí)現(xiàn)了。
    的頭像 發(fā)表于 11-15 11:12 ?1053次閱讀

    谷歌Chromebook引入徽標(biāo)功能,支持網(wǎng)頁快速安裝PWA應(yīng)用

    5 月 22 日報(bào)道,谷歌于近期的 I / O 2024 大會(huì)上發(fā)布了“Add to Chromebook”標(biāo)記。該標(biāo)記讓開發(fā)者能夠植入相應(yīng)的 API 代碼至其網(wǎng)頁中,從而使得ChromeBook用戶只需按下按鈕即可快速安裝各類PWA應(yīng)用至個(gè)人設(shè)備。
    的頭像 發(fā)表于 05-22 11:10 ?427次閱讀
    RM新时代网站-首页