Service Worker离线缓存实战踩坑总结

Dev · 玉涵 移动 阅读 887
赞 16 收藏
二维码
手机扫码查看
反馈

这次移动端项目真让人头疼

最近做了个移动端项目,用户量还行,但加载速度一直被吐槽。用户反馈最多的就是打开慢、网络不好时直接白屏。之前用过一些缓存方案,但都不够彻底。后来想了个办法,干脆上Service Worker,搞离线缓存,这样至少能保证基础页面能访问。

Service Worker离线缓存实战踩坑总结

开始动手才发现问题一堆

想法挺好,但真正实现的时候发现不是那么回事。首先是兼容性问题,iOS Safari对SW的支持一直有问题,Android的Chrome倒是没问题。还有就是调试太麻烦,不像普通JS那样容易看到错误信息。

先说注册部分吧,这个看似简单其实挺讲究的:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered: ', registration);
      })
      .catch(registrationError => {
        console.log('SW registration failed: ', registrationError);
      });
  });
}

这里有个坑,SW文件必须跟主域名同源,不能跨域。而且路径层级也有讲究,注册位置决定了它的作用域范围。

缓存策略折腾得我够呛

缓存这块是最花时间的。一开始我用的是Network First策略,想着网络好的时候优先加载最新资源,不好时再用缓存。结果发现用户体验反而不好,因为每次都要等网络请求超时才显示缓存,用户感觉很慢。

后来改成Cache First,关键资源先从缓存拿:

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    // API请求用网络优先
    event.respondWith(
      fetch(event.request).catch(() => {
        return caches.match(event.request);
      })
    );
  } else if (event.request.destination === 'document') {
    // 页面用缓存优先
    event.respondWith(
      caches.match(event.request).then(response => {
        return response || fetch(event.request);
      })
    );
  }
});

这里踩了一个坑:缓存更新的问题。用户访问旧版本缓存后,即使服务器更新了,用户可能还是看不到新内容。最后用了版本控制的方式:

const CACHE_NAME = 'app-v1.2.3';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/bundle.js',
  '/images/logo.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(urlsToCache))
  );
});

// 激活时清理旧缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

iOS的坑真是深不见底

Safari对Service Worker的支持真的是一言难尽。最开始在iOS上完全不起作用,查了好多资料才发现是Safari在隐私模式下不支持SW。而且iOS 11.3以下版本压根就不支持SW,这个得提前判断。

我还专门写了兼容性检测:

function isSWSupported() {
  return 'serviceWorker' in navigator && 
         !isIOS() && 
         !window.isSecureContext;
}

function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && 
         !window.MSStream;
}

不过说实话,这部分处理得还不够完美,iOS低版本还是有兼容问题,但影响用户不多,就没深入解决了。

动态缓存的处理也费了不少心思

静态资源还好说,动态数据的缓存比较麻烦。比如用户个人信息、列表数据这些,不能缓存太久,但也不能每次都去请求。

我的解决方案是给API响应加个时间戳,缓存超过5分钟就失效:

self.addEventListener('fetch', event => {
  const { request } = event;
  
  if (request.url.includes('/api/user') || 
      request.url.includes('/api/list')) {
    
    event.respondWith(
      caches.match(request).then(response => {
        const now = Date.now();
        
        if (response) {
          const cachedTime = response.headers.get('x-cached-time');
          if (cachedTime && (now - parseInt(cachedTime)) < 5 * 60 * 1000) {
            return response;
          }
        }
        
        return fetch(request).then(networkResponse => {
          const clonedResponse = networkResponse.clone();
          
          caches.open('api-cache').then(cache => {
            const modifiedResponse = new Response(clonedResponse.body, {
              status: networkResponse.status,
              statusText: networkResponse.statusText,
              headers: {
                ...networkResponse.headers,
                'x-cached-time': now.toString()
              }
            });
            
            cache.put(request, modifiedResponse);
          });
          
          return networkResponse;
        });
      })
    );
  }
});

这块代码写得有点复杂,但效果还行。唯一的不足是缓存大小控制不够精细,长时间使用可能会占用过多存储空间。

最后的效果还算满意

上线后确实有改善,特别是网络不好的情况下,用户至少能看到页面框架,体验好了不少。Lighthouse评分也从60多提升到80多,算是个不小的提升。

不过也有一些问题没完全解决,比如缓存更新机制还不够智能,有时候需要用户手动刷新才能看到最新内容。还有就是iOS上的兼容性问题,虽然影响面不大,但确实存在。

回头看还是有些遗憾

如果重来做的话,我会更早考虑SW的集成,而不是后面加上去。前期规划好缓存策略,后期维护会容易很多。还有就是要做好降级处理,毕竟不是所有设备都支持SW。

总体来说这次尝试是有价值的,虽然过程曲折,但确实提升了用户体验。SW这个技术还是很不错的,只是需要花时间去踩坑。

结语

以上是我这次移动端项目用Service Worker的完整经验,踩过的坑、走过的弯路都分享出来了。如果你也在做类似的功能,希望能帮你少走些弯路。这个方案肯定还有优化空间,有更好的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论