JavaScript性能优化实战(8):缓存策略与离线优化

#JavaScript性能优化实战#

前言

在Web应用中,性能优化不仅仅是关于代码执行速度,还与资源获取和数据持久化密切相关。合理的缓存策略可以显著减少网络请求,提升应用响应速度,同时有效降低服务器负载和用户流量消耗。离线优化则进一步解决了网络不稳定或断网场景下的用户体验问题,为Web应用提供类似原生应用的可靠性。

本文作为JavaScript性能优化实战系列的第八篇,将深入探讨前端缓存策略与离线优化技术。我们将从浏览器原生缓存机制出发,逐步深入到Service Worker、IndexedDB等现代Web API,并探讨PWA离线体验优化的实战应用,最终构建离线优先的Web应用架构。

浏览器缓存机制全面解析

浏览器缓存是前端性能优化的第一道防线,合理利用浏览器缓存机制可以大幅减少网络请求,加快页面加载速度。

HTTP缓存基础

HTTP缓存是最基础的缓存形式,通过HTTP头部控制资源的缓存行为。

强缓存

强缓存是指在缓存期间不需要请求服务器,直接使用缓存的资源。主要通过以下HTTP头部控制:

  1. Cache-Control:HTTP/1.1的缓存控制头,优先级高于Expires

    • max-age:缓存有效期(秒)
    • public:可以被任何缓存区缓存
    • private:只能被浏览器缓存
    • no-cache:每次使用缓存前必须先验证资源是否有效
    • no-store:完全不使用缓存
  2. Expires:HTTP/1.0的缓存控制头,指定资源过期的具体时间

// 强缓存示例响应头
Cache-Control: max-age=3600, public
Expires: Wed, 21 Oct 2023 07:28:00 GMT

当浏览器发起请求时,会先检查是否命中强缓存:

  • 如果命中,直接从缓存读取资源,此时Chrome开发者工具的Network面板会显示(from disk cache)(from memory cache)
  • 如果未命中,则进入协商缓存环节
协商缓存

协商缓存是指浏览器需要向服务器发送请求以确认缓存是否有效。主要通过以下HTTP头部控制:

  1. ETag/If-None-Match:资源的唯一标识符

    • 服务器返回资源时设置ETag
    • 浏览器请求时在If-None-Match中带上上次响应的ETag值
  2. Last-Modified/If-Modified-Since:资源的最后修改时间

    • 服务器返回资源时设置Last-Modified
    • 浏览器请求时在If-Modified-Since中带上上次响应的Last-Modified值
// 协商缓存示例
// 服务器响应头
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT

// 浏览器后续请求头
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

协商缓存的工作流程:

  • 浏览器发送带有缓存校验字段的请求
  • 服务器判断缓存是否有效
    • 有效:返回304 Not Modified响应,无响应体
    • 无效:返回200 OK响应,带有新的资源
完整的缓存判断流程

下面是一个完整的浏览器缓存判断流程:

发起请求 ---> 是否有缓存? ---> 否 ---> 向服务器请求 --> 返回200,缓存资源
                |
               是
                |
           强缓存是否有效? ---> 是 ---> 使用缓存,不发送请求 (200 from cache)
                |
               否
                |
           发送协商缓存请求 ---> 服务器判断缓存是否有效
                                    |
                                   是
                                    |
                                返回304,使用缓存
                                    |
                                   否
                                    |
                                返回200,更新缓存

常见资源类型的缓存策略

不同类型的资源适合不同的缓存策略:

HTML文档

HTML文档通常包含应用的结构和入口,建议使用协商缓存而非强缓存,确保用户能及时获取最新内容:

Cache-Control: no-cache
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
JavaScript和CSS文件

对于带有版本号或哈希的静态资源,可以使用长期的强缓存:

Cache-Control: max-age=31536000, public

在构建系统中可以这样实现:

// webpack配置示例
module.exports = {
   
  output: {
   
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  }
};
图片和媒体文件

图片和媒体文件通常变化不频繁,适合使用长期缓存:

Cache-Control: max-age=86400, public
API响应数据

API响应通常是动态数据,不应该被强缓存,但可以根据业务需求使用短期缓存:

Cache-Control: max-age=60, private

对于不应该缓存的敏感数据:

Cache-Control: no-store

缓存控制最佳实践

版本化静态资源

对静态资源使用内容哈希作为文件名的一部分,确保内容变化时URL也随之变化:

// 在构建工具中的配置
{
   
  output: {
   
    // 使用contenthash确保内容变化时文件名变化
    filename: '[name].[contenthash:8].js',
    // 其他资源也使用类似策略
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]'
  }
}
合理设置Cache-Control

根据资源类型和更新频率设置适当的Cache-Control:

// Node.js服务器示例
app.use('/static', express.static('public', {
   
  etag: true,
  lastModified: true,
  setHeaders: (res, path) => {
   
    if (path.endsWith('.html')) {
   
      // HTML文件使用协商缓存
      res.setHeader('Cache-Control', 'no-cache');
    } else if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/)) {
   
      // 带版本号的静态资源使用长期缓存
      res.setHeader('Cache-Control', 'max-age=31536000, public');
    }
  }
}));
使用Vary头处理不同的客户端请求

当响应内容根据请求头变化时,应使用Vary头指定这些头部:

Vary: User-Agent, Accept-Encoding

这确保缓存能正确区分不同客户端或不同压缩格式的响应。

避免缓存私人信息

对于包含用户私人信息的响应,应明确防止缓存:

Cache-Control: private, no-store, max-age=0

实战案例:构建高效的缓存系统

前端缓存层设计

以下是一个典型的前端应用缓存层设计:

[用户请求] --> [Service Worker缓存] --> [HTTP缓存] --> [服务器]
                      |
                [浏览器存储]
                (IndexedDB/localStorage)
实现多级缓存策略

下面是一个结合HTTP缓存和前端存储的多级缓存实现:

// 封装一个带缓存的API请求函数
async function fetchWithCache(url, options = {
    }) {
   
  const cacheKey = `api:${
     url}`;
  const cacheTime = options.cacheTime || 60000; // 默认缓存1分钟
  
  // 尝试从localStorage获取缓存
  try {
   
    const cached = localStorage.getItem(cacheKey);
    if (cached) {
   
      const {
    timestamp, data } = JSON.parse(cached);
      // 检查缓存是否有效
      if (Date.now() - timestamp < cacheTime) {
   
        console.log('从本地缓存获取数据:', url);
        return data;
      }
    }
  } catch (e) {
   
    console.error('读取缓存出错:', e);
  }

  // 发起网络请求,使用HTTP缓存
  try {
   
    const response = await fetch(url, {
   
      ...options,
      headers: {
   
        ...options.headers,
        // 对于GET请求可以使用协商缓存
        ...(options.method === 'GET' ? {
   } : {
    'Cache-Control': 'no-cache' })
      }
    });
    
    if (!response.ok) {
   
      throw new Error(`请求失败: ${
     response.status}`);
    }
    
    const data = await response.json();
    
    // 将数据存入localStorage缓存
    try {
   
      localStorage.setItem(cacheKey, JSON.stringify({
   
        timestamp: Date.now(),
        data
      }));
    } catch (e) {
   
      console.error('写入缓存出错:', e);
    }
    
    return data;
  } catch (error) {
   
    console.error('网络请求失败:', error);
    throw error;
  }
}

// 使用示例
async function getUserProfile(userId) {
   
  return fetchWithCache(`/api/users/${
     userId}`, {
    cacheTime: 300000 }); // 缓存5分钟
}

async function getProductList() {
   
  return fetchWithCache('/api/products', {
    cacheTime: 60000 }); // 缓存1分钟
}

浏览器缓存查看与调试技巧

使用Chrome DevTools检查缓存
  1. 查看HTTP缓存

    • 打开Chrome DevTools > Network面板
    • 勾选"Disable cache"可临时禁用缓存
    • 查看Size列,显示"(from disk cache)“或”(from memory cache)"表示命中缓存
    • 查看Status列,304表示命中协商缓存
  2. 检查应用缓存存储

    • 打开Chrome DevTools > Application面板
    • 在Storage部分可以查看和管理各种缓存和存储
    • Cache Storage查看Service Worker缓存
    • IndexedDB和Local Storage查看对应存储
缓存调试常用命令
// 清除localStorage缓存
localStorage.clear();

// 编程方式检查缓存头
fetch('/example.js')
  .then(response => {
   
    console.log('Cache-Control:', response.headers.get('Cache-Control'));
    console.log('ETag:', response.headers.get('ETag'));
    console.log('Last-Modified:', response.headers.get('Last-Modified'));
  });

// 使用performance API分析缓存性能
performance.getEntriesByType('resource').forEach(resource => {
   
  console.log(`${
     resource.name}: ${
     resource.duration}ms`);
});

通过深入理解和合理应用浏览器缓存机制,我们可以显著提升Web应用的性能和用户体验。在下一节中,我们将探讨更强大的Service Worker缓存策略,它不仅能实现更细粒度的缓存控制,还能为Web应用提供真正的离线能力。

Service Worker缓存策略与实现

Service Worker是一种运行在浏览器背后的脚本,它独立于网页,能够拦截和处理网络请求,实现离线缓存、推送通知等功能。相比传统的HTTP缓存,Service Worker提供了更精细的缓存控制能力和离线支持。

Service Worker基础

Service Worker生命周期

Service Worker有着明确的生命周期,理解这一点对于实现可靠的缓存策略至关重要:

  1. 注册(Register):告知浏览器Service Worker的位置
  2. 安装(Install):首次安装或更新时触发,通常用于缓存静态资源
  3. 激活(Activate):安装成功后激活,通常用于清理旧缓存
  4. 空闲(Idle):未处理任何事件时的状态
  5. 终止(Terminated):空闲一段时间后被终止,节省资源
  6. 获取(Fetch):拦截网络请求,可以返回缓存或发起新请求
注册Service Worker

首先需要在主页面注册Service Worker:

// 检查浏览器是否支持Service Worker
if ('serviceWorker' in navigator) {
   
  window.addEventListener('load', () => {
   
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
   
        console.log('Service Worker注册成功,作用域为:', registration.scope);
      })
      .catch(error => {
   
        console.error('Service Worker注册失败:', error);
      });
  });
}
创建基本的Service Worker

以下是一个基本的Service Worker示例(sw.js):

// 定义缓存名称和缓存资源列表
const CACHE_NAME = 'app-v1';
const CACHE_ASSETS = [
  '/',
  '/index.html',
  '/css/style.css',
  '/js/main.js',
  '/images/logo.png',
  '/offline.html'
];

// 安装事件:预缓存静态资源
self.addEventListener('install', event => {
   
  console.log('Service Worker: 安装中');
  
  // 延长安装阶段直到缓存完成
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
   
        console.log('Service Worker: 缓存文件');
        return cache.addAll(CACHE_ASSETS);
      })
      .then(() => self.skipWaiting()) // 强制激活
  );
});

// 激活事件:清理旧缓存
self.addEventListener('activate', event => {
   
  console.log('Service Worker: 已激活');
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
   
      return Promise.all(
        cacheNames.map(cache => {
   
          if (cache !== CACHE_NAME) {
   
            console.log('Service Worker: 清理旧缓存', cache);
            return caches.delete(cache);
          }
        })
      );
    })
  );
});

// 拦截fetch请求
self.addEventListener('fetch', event => {
   
  console.log('Service Worker: 拦截请求', event.request.url);
  
  event.respondWith(
    // 检查缓存
    caches.match(event.request)
      .then(response => {
   
        // 如果在缓存中找到了响应,返回缓存的版本
        if (response) {
   
          return response;
        }
        
        // 没有缓存,发起网络请求
        return fetch(event.request)
          .then(response => {
   
            // 检查是否得到有效响应
            if (!response || response.status !== 200 || response.type !== 'basic') {
   
              return response;
            }
            
            // 复制响应(因为响应是流,只能消费一次)
            const responseToCache = response.clone();
            
            // 将响应添加到缓存
            caches.open(CACHE_NAME)
              .then(cache => {
   
                cache.put(event.request, responseToCache);
              });
              
            return response;
          })
          .catch(() => {
   
            // 网络请求失败时,返回离线页面
            if (event.request.headers.get('accept').includes('text/html')) {
   
              return caches.match('/offline.html');
            }
          });
      })
  );
});

高级缓存策略

Service Worker提供了灵活的缓存策略选择,根据不同资源类型和应用需求,可以实现多种缓存模式:

1. 缓存优先(Cache First)

优先从缓存获取资源,缓存未命中才发起网络请求。适合静态资源和不经常更改的内容。

function cacheFirst(request) {
   
  return caches.match(request)
    .then(cacheResponse => {
   
      return cacheResponse || fetch(request).then(networkResponse => {
   
        // 将网络响应存入缓存
        return caches.open(CACHE_NAME).then(cache => {
   
          cache.put(request, networkResponse.clone());
          return networkResponse;
        });
      });
    });
}
2. 网络优先(Network First)

优先从网络获取最新资源,网络请求失败才使用缓存。适合经常更新的内容,如API响应。

function networkFirst(request) {
   
  return fetch(request)
    .then(networkResponse => {
   
      // 将网络响应复制一份存入缓存
      caches.open(CACHE_NAME).then(cache => {
   
        cache.put(request, networkResponse.clone());
      });
      return networkResponse;
    })
    .catch(() => {
   
      // 网络请求失败,使用缓存
      return caches.match(request);
    });
}
3. 网络优先并更新缓存(Stale While Revalidate)

同时从缓存和网络获取资源,先返回缓存内容(如果有)以快速响应,同时更新缓存以备下次使用。适合内容更新不那么关键的场景。

function staleWhileRevalidate(request) {
   
  return caches.open(CACHE_NAME).then(cache => {
   
    return cache.match(request).then(cacheResponse => {
   
      const fetchPromise = fetch(request).then(networkResponse => {
   
        cache.put(request, networkResponse.clone());
        return networkResponse;
      });
      
      // 返回缓存响应,或等待网络响应
      return cacheResponse || fetchPromise;
    });
  });
}
4. 缓存与网络竞争(Cache and Network Race)

同时从缓存和网络获取资源,返回最先得到的响应。适合优化在不同网络环境下的体验。

function raceStrategy(request) {
   
  // 创建Promise.race,返回最快的响应
  return Promise.race([
    caches.match(request).then(response => response),
    fetch(request).then(response => {
   
      // 更新缓存
      const responseClone = response.clone();
      caches.open(CACHE_NAME).then(cache => {
   
        cache.put(request, responseClone);
      });
      return response;
    })
  ]);
}
5. 缓存回退网络(Cache Falling Back to Network)

先尝试使用缓存,缓存未命中时再发起网络请求。网络请求失败时可以提供通用的离线页面。

function cacheFallbackToNetwork(request) {
   
  return caches.match(request)
    .then(cacheResponse => {
   
      if (cacheResponse) {
   
        return cacheResponse;
      }
      
      return fetch(request)
        .catch(() => {
   
          // 请求HTML内容时返回通用离线页面
          if (request.headers.get('Accept').includes('text/html')) {
   
            return caches.match('/offline.html');
          }
          
          // 对于图片可以返回占位图
          if (request.url.match(/\.(jpg|png|gif|svg)$/)) {
   
            return caches.match('/images/offline-image.png');
          }
        });
    });
}

根据资源类型选择缓存策略

在实际应用中,通常需要根据不同的资源类型选择不同的缓存策略:

self.addEventListener('fetch', event => {
   
  const request = event.request;
  const url = new URL(request.url);
  
  // 对同源请求应用缓存策略
  if (url.origin === self.location.origin) {
   
    // 静态资源使用缓存优先策略
    if (url.pathname.startsWith('/static/') || 
        url.pathname.match(/\.(css|js|png|jpg|jpeg|svg|gif)$/)) {
   
      event.respondWith(cacheFirst(request));
      return;
    }
    
    // HTML页面使用网络优先策略
    if (request.headers.get('Accept').includes('text/html')) {
   
      event.respondWith(networkFirst(request));
      return;
    }
  }
  
  // API请求使用staleWhileRevalidate策略
  if (url.pathname.startsWith('/api/')) {
   
    event.respondWith(staleWhileRevalidate(request));
    return;
  }
  
  // 默认使用网络优先策略
  event.respondWith(networkFirst(request));
});

定期更新缓存内容

有些情况下,我们需要定期更新缓存内容,而不仅依赖于用户的请求:

// 定义需要定期更新的资源
const PERIODIC_UPDATES = [
  '/api/news',
  '/api/notifications'
];

// 设置更新间隔(例如每小时更新一次)
const UPDATE_INTERVAL = 60 * 60 * 1000; // 1小时

// 在SW激活后设置定期更新
self.addEventListener('activate', event => {
   
  // 其他激活代码...
  
  // 设置定期更新任务
  self.registration.periodicSync.register('update-cache', {
   
    minInterval: UPDATE_INTERVAL
  });
});

// 处理定期同步事件
self.addEventListener('periodicsync', event => {
   
  if (event.tag === 'update-cache') {
   
    event.waitUntil(updateCache());
  }
});

// 更新缓存的函数
function updateCache() {
   
  return Promise.all(
    PERIODIC_UPDATES.map(url => 
      fetch(url)
        .then(response => {
   
          if (!response.ok) return;
          
          const clonedResponse = response.clone();
          return caches.open(CACHE_NAME).then(cache => {
   
            return cache.put(url, clonedResponse);
          });
        })
        .catch(error => console.error('缓存更新失败:', url, error))
    )
  );
}

缓存失效与更新策略

管理缓存的更新和失效是Service Worker实现中的关键挑战。以下是几种常用的缓存更新策略:

基于版本号的缓存更新

使用缓存版本号,在Service Worker更新时清除旧缓存:

const CACHE_VERSION = 'v2';
const CACHE_NAME = `app-cache-${
     CACHE_VERSION}`;

self.addEventListener('activate', event => {
   
  event.waitUntil(
    caches.keys().then(cacheNames => {
   
      return Promise.all(
        cacheNames.map(cacheName => {
   
          // 删除不匹配当前版本的缓存
          if (cacheName.startsWith('app-cache-') && cacheName !== CACHE_NAME) {
   
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
基于内容哈希的缓存更新

对于静态资源,使用内容哈希作为文件名的一部分,确保内容变化时URL也会变化:

// 在构建过程中生成带哈希的文件名
// main.8e2d4a2.js 而不是 main.js

// 在Service Worker中预缓存这些资源
const CACHE_ASSETS = [
  '/',
  '/index.html',
  '/css/style.5f3e9b1.css',
  '/js/main.8e2d4a2.js',
  '/images/logo.3a7c4d8.png'
];
使用ETag进行缓存验证

对于API响应,可以使用ETag进行缓存验证,只在内容变化时更新缓存:

function updateWithEtag(request) {
   
  // 先检查缓存
  return caches.match(request).then(cachedResponse => {
   
    // 提取之前响应的ETag
    const etag = cachedResponse ? cachedResponse.headers.get('ETag') : null;
    
    // 创建一个新的请求,包含If-None-Match头
    const newRequest = etag ? 
      new Request(request, {
   
        headers: {
   
          'If-None-Match': etag
        }
      }) : request;
    
    // 发送带条件的请求
    return fetch(newRequest).then(networkResponse => {
   
      // 304表示内容未变化,使用缓存
      if (networkResponse.status === 304) {
   
        return cachedResponse;
      }
      
      // 内容已变化,更新缓存并返回新响应
      const clonedResponse = networkResponse.clone();
      caches.open(CACHE_NAME).then(cache => {
   
        cache.put(request, clonedResponse);
      });
      
      return networkResponse;
    }).catch(() => {
   
      // 网络请求失败,返回缓存(如果有)
      return cachedResponse;
    });
  });
}

处理Service Worker更新

Service Worker更新是一个需要谨慎处理的过程,以下是一种推荐的更新流程:

// 在主页面中监听Service Worker更新
if ('serviceWorker' in navigator) {
   
  // 注册Service Worker
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
   
      // 检查更新
      registration.addEventListener('updatefound', () => {
   
        const newWorker = registration.installing;
        
        newWorker.addEventListener('statechange', () => {
   
          // 当新的Service Worker变为activated状态
          if (newWorker.state === 'activated') {
   
            // 通知用户页面有更新
            if (window.confirm('网站已更新,是否刷新页面以应用新版本?')) {
   
              window.location.reload();
            }
          }
        });
      });
      
      // 检查控制页面的Service Worker是否发生变化
      let refreshing = false;
      navigator.serviceWorker.addEventListener('controllerchange', () => {
   
        if (!refreshing) {
   
          refreshing = true;
          window.location.reload();
        }
      });
    });
  
  // 定期检查更新
  setInterval(() => {
   
    navigator.serviceWorker.getRegistration().then(registration => {
   
      if (registration) {
   
        registration.update();
      }
    });
  }, 60 * 60 * 1000); // 每小时检查一次
}

实战案例:构建离线可用的博客应用

以下是一个实际案例,展示如何为博客应用实现离线访问功能:

// sw.js - 博客应用的Service Worker

// 缓存名称定义
const STATIC_CACHE = 'blog-static-v1';
const PAGES_CACHE = 'blog-pages-v1';
const IMAGES_CACHE = 'blog-images-v1';
const API_CACHE = 'blog-api-v1';

// 需要缓存的静态资源
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/css/main.css',
  '/js/app.js',
  '/js/vendor.js',
  '/offline.html',
  '/images/logo.svg',
  '/images/offline.svg'
];

// 安装事件 - 预缓存静态资源
self.addEventListener('install', event => {
   
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then(cache => cache.addAll(STATIC_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// 激活事件 - 清理旧缓存
self.addEventListener('activate', event => {
   
  const currentCaches = [STATIC_CACHE, PAGES_CACHE, IMAGES_CACHE, API_CACHE];
  
  event.waitUntil(
    caches.keys()
      .then(cacheNames => {
   
        return cacheNames.filter(
          cacheName => !currentCaches.includes(cacheName)
        );
      })
      .then(cachesToDelete => {
   
        return Promise.all(
          cachesToDelete.map(cacheToDelete => caches.delete(cacheToDelete))
        );
      })
      .then(() => self.clients.claim())
  );
});

// 拦截请求
self.addEventListener('fetch', event => {
   
  const request = event.request;
  const url = new URL(request.url);
  
  // 不处理非GET请求
  if (request.method !== 'GET') {
   
    return;
  }
  
  // 处理API请求
  if (url.pathname.startsWith('/api/')) {
   
    event.respondWith(handleApiRequest(request));
    return;
  }
  
  // 处理HTML页面请求
  if (request.headers.get('Accept').includes('text/html')) {
   
    event.respondWith(handleHtmlRequest(request));
    return;
  }
  
  // 处理图片请求
  if (url.pathname.match(/\.(jpg|jpeg|png|gif|svg)$/)) {
   
    event.respondWith(handleImageRequest(request));
    return;
  }
  
  // 处理其他静态资源
  event.respondWith(
    caches.match(request)
      .then(cachedResponse => cachedResponse || fetchAndCache(request, STATIC_CACHE))
  );
});

// 处理API请求
function handleApiRequest(request) {
   
  return fetchWithTimeout(request, 3000)
    .then(response => {
   
      // 复制响应以便缓存
      const clonedResponse = response.clone();
      
      // 只缓存成功的响应
      if (response.ok) {
   
        caches.open(API_CACHE)
          .then(cache => cache.put(request, clonedResponse));
      }
      
      return response;
    })
    .catch(() => {
   
      // 网络请求失败,尝试从缓存获取
      return caches.match(request)
        .then(cachedResponse => {
   
          if (cachedResponse) {
   
            // 给缓存响应添加标记,表明来自缓存
            const responseOptions = {
   
              headers: new Headers(cachedResponse.headers),
              status: cachedResponse.status,
              statusText: cachedResponse.statusText
            };
            responseOptions.headers.set('X-Data-Source', 'cache');
            
            return cachedResponse.blob()
              .then(body => new Response(body, responseOptions));
          }
          
          // 如果没有缓存,返回离线API响应
          return new Response(
            JSON.stringify({
   
              error: 'offline',
              message: '您当前处于离线状态,无法获取最新数据'
            }),
            {
   
              status: 503,
              headers: {
    'Content-Type': 'application/json' }
            }
          );
        });
    });
}

// 处理HTML请求
function handleHtmlRequest(request) {
   
  return fetchWithTimeout(request, 3000)
    .then(response => {
   
      // 复制响应并缓存
      const clonedResponse = response.clone();
      
      caches.open(PAGES_CACHE)
        .then(cache => cache.put(request, clonedResponse));
      
      return response;
    })
    .catch(() => {
   
      // 网络请求失败,尝试从缓存获取
      return caches.match(request)
        .then(cachedResponse => cachedResponse || caches.match('/offline.html'));
    });
}

// 处理图片请求
function handleImageRequest(request) {
   
  // 使用缓存优先策略
  return caches.match(request)
    .then(cachedResponse => {
   
      if (cachedResponse) {
   
        // 同时在后台更新缓存
        fetch(request)
          .then(networkResponse => {
   
            if (networkResponse.ok) {
   
              caches.open(IMAGES_CACHE)
                .then(cache => cache.put(request, networkResponse));
            }
          })
          .catch(() => {
   /* 静默失败 */});
        
        return cachedResponse;
      }
      
      // 缓存中没有,发起网络请求
      return fetch(request)
        .then(networkResponse => {
   
          // 缓存响应副本
          const clonedResponse = networkResponse.clone();
          caches.open(IMAGES_CACHE)
            .then(cache => cache.put(request, clonedResponse));
          
          return networkResponse;
        })
        .catch(() => {
   
          // 返回占位图片
          return caches.match('/images/offline.svg');
        });
    });
}

// 通用的获取并缓存函数
function fetchAndCache(request, cacheName) {
   
  return fetch(request)
    .then(response => {
   
      if (response.ok) {
   
        const clonedResponse = response.clone();
        caches.open(cacheName)
          .then(cache => cache.put(request, clonedResponse));
      }
      return response;
    })
    .catch(error => {
   
      console.error('获取资源失败:', error);
      throw error;
    });
}

// 添加超时的fetch函数
function fetchWithTimeout(request, timeout) {
   
  return new Promise((resolve, reject) => {
   
    // 设置超时
    const timeoutId = setTimeout(() => {
   
      reject(new Error('请求超时'));
    }, timeout);
    
    fetch(request).then(
      response => {
   
        clearTimeout(timeoutId);
        resolve(response);
      },
      err => {
   
        clearTimeout(timeoutId);
        reject(err);
      }
    );
  });
}

通过上述Service Worker实现,博客应用能够实现:

  1. 离线浏览已访问过的页面
  2. 即使在线,也能快速加载缓存资源
  3. 针对不同资源类型应用不同的缓存策略
  4. 提供优雅的离线体验和失败处理

Service Worker强大的缓存控制能力和离线支持,使它成为现代Web应用性能优化和用户体验提升的关键技术。在下一节中,我们将探讨另一种客户端存储技术——IndexedDB,它为Web应用提供了更强大的本地数据库能力。

IndexedDB高性能客户端存储应用

IndexedDB基础概念

IndexedDB是一种低级API,用于在客户端存储大量结构化数据。与localStorage和sessionStorage不同,IndexedDB提供了完整的事务性数据库系统,支持索引、游标和事务,能够高效处理大量数据。

IndexedDB的主要特点
  1. 大容量存储:可以存储远超localStorage的数据量(通常为数百MB甚至GB)
  2. 结构化数据:支持JavaScript对象直接存储,无需序列化
  3. 事务支持:提供类似传统数据库的事务隔离
  4. 异步API:所有操作都是异步的,不会阻塞主线程
  5. 同源策略:遵循浏览器同源策略,保证安全性
  6. 索引查询:可以为数据创建索引,支持高效查询
IndexedDB的基本工作流程
  1. 打开数据库连接
  2. 在连接回调中创建对象仓库(object store)
  3. 启动一个事务并请求执行操作
  4. 通过事件监听操作结果
  5. 使用查询方法或游标获取数据

创建和管理IndexedDB数据库

打开数据库
function openDatabase() {
   
  return new Promise((resolve, reject) => {
   
    // 打开数据库(如果不存在则创建)
    const request = indexedDB.open('MyAppDB', 1);
    
    // 处理数据库升级事件
    request.onupgradeneeded = event => {
   
      const db = event.target.result;
      
      // 创建对象仓库
      if (!db.objectStoreNames.contains('users')) {
   
        const usersStore = db.createObjectStore('users', {
    keyPath: 'id' });
        // 创建索引
        usersStore.createIndex('email', 'email', {
    unique: true });
        usersStore.createIndex('name', 'name', {
    unique: false });
      }
      
      if (!db.objectStoreNames.contains('articles')) {
   
        const articlesStore = db.createObjectStore('articles', {
    keyPath: 'id' });
        articlesStore.createIndex('author', 'authorId', {
    unique: false });
        articlesStore.createIndex('date', 'publishDate', {
    unique: false });
      }
    };
    
    // 成功回调
    request.onsuccess = event => {
   
      const db = event.target.result;
      resolve(db);
    };
    
    // 错误回调
    request.onerror = event => {
   
      console.error('打开数据库失败:', event.target.error);
      reject(event.target.error);
    };
  });
}
添加数据
function addItem(db, storeName, item) {
   
  return new Promise((resolve, reject) => {
   
    // 创建读写事务
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    
    // 添加或更新记录
    const request = store.put(item);
    
    // 处理结果
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
    
    // 事务完成处理
    transaction.oncomplete = () => console.log('事务完成');
    transaction.onerror = event => console.error('事务错误:', event.target.error);
  });
}
获取数据
function getItem(db, storeName, key) {
   
  return new Promise((resolve, reject) => {
   
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    
    const request = store.get(key);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
查询数据
function queryByIndex(db, storeName, indexName, value) {
   
  return new Promise((resolve, reject) => {
   
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const index = store.index(indexName);
    
    const request = index.getAll(value);
    
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}
使用游标遍历数据
function iterateWithCursor(db, storeName, callback) {
   
  return new Promise((resolve, reject) => {
   
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
    const results = [
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员查理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值