Caching - Progressive Web Apps

在PWA应用中,最为重要功能之一就是缓存了。也即将一些静态资源缓存在本地,便于下次快速获取到,而不用从网络重新获取。这可以改善加载速度和节省网页浏览。所以,这个功能应该属于所有网站的一项必备能力。

Storage Options

首先来总结一下存储策略。主要有下面这么多种:

  • IndexedDB(原生API过于复杂)
  • Web SQL
  • Local Storage
  • Cache Storage API
  • pouchDB(Based on CouchDB)
  • localForage(自动帮你选择IndexedDB,Web SQL,Local Storage)
  • Lovefield

Cache Storage API

  • caches.open()
  • caches.has()
  • caches.delete()

Service Worker Cache Example

注册Service Worker代码


// Progressive Enhancement (SW supported)
// if ('serviceWorker' in navigator) {
if (navigator.serviceWorker) {

  // Register the SW
  navigator.serviceWorker.register('/sw.js').then((registration) => {

  }).catch(console.log);
}

Service Worker sw.js

// Service Worker
const pwaCache = 'pwa-cache-2';
self.addEventListener('install', (e) => {
  let cacheReady = caches.open(pwaCache).then((cache) => {
    console.log('New cache ready.');
    return cache.addAll([
      '/',
      'style.css',
      'thumb.png',
      'main.js'
    ]);
  });
  e.waitUntil(cacheReady);
});


self.addEventListener('activate', (e) => {
  let cacheCleaned = caches.keys().then((keys) => {
    keys.forEach((key) => {
      if( key !== pwaCache ) return caches.delete(key);
    });
  });
  e.waitUntil(cacheCleaned);
});

// 此处可以使用下面任意一个Cache策略
self.addEventListener('fetch', (e) => {
  // Skip for remote fetch
  if ( !e.request.url.match(location.origin) )  return;
  // Serve local fetch from cache
  let newRes = caches.open(pwaCache).then((cache) => {
    return cache.match(e.request).then((res) => {
      // Check request was found in cache
      if (res) {
        console.log(`Serving ${res.url} from cache.`);
        return res;
      }
      // Fetch on behalf of client and cache
      return fetch(e.request).then((fetchRes) => {
        cache.put(e.request, fetchRes.clone());
        return fetchRes;
      });
    });
  });
  e.respondWith(newRes);

});

Caching Strategies

总结一下客户端缓存的集中策略。

1. Cache Only

只使用Cache,如果Cache中没有该资源,或者Cache被清理,则会失败。这仅使用与纯本地应用。

// 1. Cache only. Static assets - App Shell
self.addEventListener('fetch', (e) => {
  e.respondWith(caches.match(e.request));
})

2. Cache with Network Fallback

适用于存储静态资源,对实时性要求不高的资源。例如图片,CSS等。

self.addEventListener('fetch', (e) => {

  // 2. Cache with Network Fallback
  e.respondWith(
    caches.match(e.request).then( (res) => {
      if(res) return res;

      // Fallback
      return fetch(e.request).then( (newRes) => {
        // Cache fetched response
        caches.open(pwaCache).then( cache => cache.put(e.request, newRes) );
        return newRes.clone();
      })
    })
  );
})

3. Network with cache fallback

在慢速网络下,不一定是一个好的方案。

self.addEventListener('fetch', (e) => {
  e.respondWith(
    fetch(e.request).then( (res) => {
      // Cache latest version
      caches.open(pwaCache).then( cache => cache.put(e.request, res) );
      return res.clone();
  
    // Fallback to cache
    }).catch( err => caches.match(e.request) )
  );
})

4. Cache with Network Update

在Workbox中该策略被称为Stale-While-Revalidate,The stale-while-revalidate pattern allows you to respond the request as quickly as possible with a cached response if available, falling back to the network request if it’s not cached. The network request is then used to update the cache.

// 4. Cache with Network Update
self.addEventListener('fetch', (e) => {
  e.respondWith(
    caches.open(pwaCache).then( (cache) => {
  
      // Return from cache
      return cache.match(e.request).then( (res) => {
  
        // Update
        let updatedRes = fetch(e.request).then( (newRes) => {
          // Cache new response
          cache.put(e.request, newRes.clone());
          return newRes;
        });
  
        return res || updatedRes;
      })
    })
  );
})

5. Cache & Network Race with offline content

self.addEventListener('fetch', (e) => {

  // 5. Cache & Network Race with offline content
  let firstResponse = new Promise((resolve, reject) => {

    // Track rejections
    let firstRejectionReceived = false;
    let rejectOnce = () => {
      if (firstRejectionReceived) {

        if (e.request.url.match('thumb.png')) {
          resolve(caches.match('/placeholder.png'));
        } else {
          reject('No response received.')
        }
      } else {
        firstRejectionReceived = true;
      }
    };

    // Try Network
    fetch(e.request).then( (res) => {
      // Check res ok
      res.ok ? resolve(res) : rejectOnce();
    }).catch(rejectOnce);

    // Try Cache
    caches.match(e.request).then( (res) => {
      // Check cache found
      res ? resolve(res) : rejectOnce();
    }).catch(rejectOnce);

  });
  e.respondWith(firstResponse);

})

要想写一个完备的缓存策略,并不是那么容易。以上的缓存策略,仅仅作为参考和学习。那些代码不适合在生产环境中直接使用。如果想再生产环境中实现这项功能,最好使用Workbox。

Resources