1. 离线优先应用架构概览:Service Worker、IndexedDB、Cache API、本地优先同步模式

1.1 本章定位与学习目标

本章是《离线优先应用集成测试方法》课程的起点,旨在建立对离线优先应用四大核心支柱——Service Worker、IndexedDB、Cache API 和本地优先同步模式——的全局认知。学完本章后,你将能够:

  • 描述离线优先架构的四种核心组件及其职责边界;
  • 绘制四者之间的协作流程图与数据流向;
  • 识别集成测试中需要重点覆盖的交互点与风险区域。

1.2 离线优先架构的核心问题

传统 Web 应用依赖持续的网络连接:请求→服务器响应→渲染。离线优先架构将这一顺序反转——本地是主数据源,网络是同步通道。这意味着四个关键技术必须协同工作:

  1. Service Worker —— 网络代理与离线资源管控层;
  2. Cache API —— 静态资源与响应缓存层;
  3. IndexedDB —— 结构化数据持久化层;
  4. 本地优先同步模式 —— 数据冲突解决与双向同步策略层。

下表列出各组件在架构中的核心职责与测试关注点:

组件 核心职责 集成测试关注点
Service Worker 拦截网络请求、管理缓存、后台同步、推送通知 生命周期、请求拦截逻辑、缓存策略、更新与激活
Cache API 缓存 HTTP 响应与静态资源(HTML/CSS/JS/图片) 缓存命中/未命中、过期策略、版本管理、存储配额
IndexedDB 存储结构化业务数据(JSON、Blob、大型对象) 事务读写、索引查询、模式迁移、存储限制
本地优先同步模式 本地数据与远端服务器的双向同步、冲突处理 冲突检测与解决策略、同步顺序、离线队列、幂等性

1.3 架构协作流程:一张图理解四者关系

离线优先应用四大组件协作架构图 离线优先应用核心架构 — 组件协作关系 Service Worker 网络代理 · 请求拦截 · 缓存策略 · 后台同步 Cache API 静态资源缓存 · 响应缓存 版本管理 · 存储配额 IndexedDB 结构化数据持久化 事务 · 索引 · 大对象存储 本地优先同步模式 双向同步 · 冲突检测 · 离线队列 · 幂等性 缓存读写 数据读写 离线资源队列 变更集推送 同步触发事件 ⓘ 集成测试要点: SW → Cache/IDB 的读写路径,SW → 同步模式的触发协议,以及同步模式对 IDB 变更集的消费逻辑。
架构要点: 所有离线数据流都经过 Service Worker 中转,它是离线优先应用的“交通警察”。Cache API 和 IndexedDB 分别管理静态资源与动态数据,而同步模式层负责将本地变更可靠地送达远端。

1.4 各组件实现原理与关键 API

1.4.1 Service Worker 的生命周期与拦截原理

Service Worker 是一个独立于主线程的 JavaScript 运行环境,通过 install → activate → fetch 事件驱动。其核心 API 如下:

// 注册 Service Worker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
        .then(reg => console.log('SW 注册成功:', reg.scope))
        .catch(err => console.error('SW 注册失败:', err));
}

// sw.js — 安装阶段预缓存
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('v1').then(cache => {
            return cache.addAll([
                '/',
                '/index.html',
                '/styles.css',
                '/app.js'
            ]);
        })
    );
});

// sw.js — 请求拦截与缓存策略
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(cachedResponse => {
            return cachedResponse || fetch(event.request)
                .then(response => {
                    return caches.open('v1').then(cache => {
                        cache.put(event.request, response.clone());
                        return response;
                    });
                });
        })
    );
});
调试提示: 在 Chrome DevTools → Application → Service Workers 面板中,可以手动“Update”、“Unregister”、“Push”和“Sync”来测试 SW 生命周期。勾选“Update on reload”可确保每次刷新都获取最新 SW。

1.4.2 Cache API 的版本管理与过期策略

Cache API 使用 caches.open(name) 创建或打开命名缓存。旧版本清理是集成测试中容易遗漏的环节:

// sw.js — activate 阶段清理旧缓存
self.addEventListener('activate', event => {
    const expectedCacheNames = ['v1'];
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames
                    .filter(name => !expectedCacheNames.includes(name))
                    .map(name => caches.delete(name))
            );
        })
    );
});

1.4.3 IndexedDB 的事务模型与索引

IndexedDB 使用对象存储(Object Store)和索引(Index)实现高效查询。所有读写操作都必须包裹在事务中:

// 打开/创建数据库
const request = indexedDB.open('OfflineAppDB', 2);

request.onupgradeneeded = event => {
    const db = event.target.result;
    // 创建对象存储
    if (!db.objectStoreNames.contains('tasks')) {
        const store = db.createObjectStore('tasks', {
            keyPath: 'id',
            autoIncrement: true
        });
        // 创建索引
        store.createIndex('status', 'status', { unique: false });
        store.createIndex('createdAt', 'createdAt', { unique: false });
    }
};

// 写入事务
function saveTask(task) {
    return new Promise((resolve, reject) => {
        const db = /* 从全局引用获取 */;
        const tx = db.transaction('tasks', 'readwrite');
        const store = tx.objectStore('tasks');
        const req = store.put(task);
        req.onsuccess = () => resolve(req.result);
        req.onerror = () => reject(req.error);
    });
}

1.4.4 本地优先同步模式的核心接口

同步模式通常封装为 SyncManager 类,它依赖 Service Worker 的 SyncEvent:

// 在主线程注册同步任务
navigator.serviceWorker.ready.then(registration => {
    registration.sync.register('sync-tasks').then(() => {
        console.log('后台同步已注册');
    });
});

// sw.js — 处理同步事件
self.addEventListener('sync', event => {
    if (event.tag === 'sync-tasks') {
        event.waitUntil(syncPendingTasks());
    }
});

async function syncPendingTasks() {
    const db = await openDB();
    const tx = db.transaction('tasks', 'readonly');
    const store = tx.objectStore('tasks');
    const pending = await store.getAll(); // 筛选待同步项
    for (const task of pending) {
        try {
            await fetch('/api/tasks', {
                method: 'POST',
                body: JSON.stringify(task),
                headers: { 'Content-Type': 'application/json' }
            });
            // 同步成功后更新本地状态
            task.synced = true;
            await updateTaskInDB(task);
        } catch (err) {
            console.error('同步失败,保留在队列中:', task.id, err);
        }
    }
}

1.5 集成测试的关键交互点

下图列出了四组件之间必须通过集成测试覆盖的 9 个交互点,按照数据流向分为三个层级:

层级 交互路径 测试重点 常见错误
L1: 注册与就绪 主线程 → SW SW 注册成功、作用域正确、install 完成 SW 因 HTTPS 或 localhost 限制无法注册
L2: 缓存与存储 SW ↔ Cache API 缓存策略一致性、版本升级后旧缓存清理 activate 事件中未清理旧缓存,导致存储膨胀
L2: 数据持久化 SW ↔ IndexedDB 事务读写、索引查询、大对象序列化 事务超时或未捕获的 abort 事件
L3: 同步触发 SW → 同步模式 sync 事件注册与触发、tag 唯一性 重复注册导致同步风暴
L3: 数据推送 同步模式 → IndexedDB 变更集读取、状态标记、冲突版本号维护 同步未完成时修改本地数据导致冲突
⚠ 集成测试陷阱: 不要单独测试 SW、IndexedDB 或 Cache API。离线优先应用的故障多数发生在组件交界处——例如 SW 从 Cache 获取了过期响应,但 IndexedDB 中的业务数据已经更新。始终使用 端到端离线场景 来验证完整链路。

1.6 常见错误与稳定性优化

1.6.1 高频错误清单

  • SW 注册作用域错误: SW 文件路径决定了其作用域,register('/sw.js') 作用域为根目录,register('sw.js') 作用域为当前目录。
  • Cache API 的 addAll 原子性失败: 只要有一个资源请求失败,整个 addAll 就会拒绝,导致预缓存全部失效。
  • IndexedDB 版本号冲突: 多个选项卡同时打开时,onupgradeneeded 可能被并发触发,导致模式迁移冲突。
  • 同步队列幂等性缺失: 重复同步同一变更导致服务端数据重复或状态错乱。

1.6.2 性能与稳定性优化策略

组件 优化策略 集成测试验证方法
Service Worker 避免在 install 中缓存过多资源;使用 Stale-While-Revalidate 策略 模拟慢速网络,检查页面首次加载与二次加载的响应来源
Cache API 按版本组织缓存,activate 时及时淘汰旧版 升级 SW 版本后检查旧缓存是否被清除
IndexedDB 将大对象拆分为分块存储;使用索引加速查询 插入 1000 条记录并验证索引查询性能
同步模式 实现指数退避重试;使用变更集增量同步 模拟服务端 503 错误,验证重试间隔递增

1.7 调试与验证方法

在进入后续章节前,掌握以下调试手段有助于快速定位问题:

  1. Chrome DevTools → Application 面板: 查看 SW 状态、Cache Storage 内容、IndexedDB 数据、Storage 配额。
  2. Network 面板结合 Offline 模式: 勾选“Offline”,验证离线页面是否正常加载,SW 是否返回缓存内容。
  3. navigator.storage.estimate(): 监控存储使用量,避免超出配额导致写入失败。
  4. Service Worker 内部日志: 在 SW 代码中使用 console.log,在 DevTools 的 Console 面板勾选“Service Workers”过滤查看。
// 在 sw.js 中添加调试日志
self.addEventListener('fetch', event => {
    console.log('[SW Fetch]', event.request.method, event.request.url);
    event.respondWith(
        caches.match(event.request).then(cached => {
            console.log('[SW Cache] 命中?', !!cached);
            return cached || fetch(event.request);
        })
    );
});

1.8 课后自检清单

在进入第 2 章之前,请确保你已掌握以下知识点:

  • ✅ 能口头解释 Service Worker 的 install → activate → fetch 生命周期;
  • ✅ 知道 Cache API 与 IndexedDB 的存储目标差异;
  • ✅ 理解本地优先同步模式中“本地是主数据源”的含义;
  • ✅ 能列举至少 3 个组件交界处可能出现的集成故障;
  • ✅ 掌握使用 DevTools Application 面板验证离线缓存的基本操作。
小结: 离线优先架构不是四个独立技术的堆叠,而是一套精心编排的协作系统。Service Worker 是调度中枢,Cache API 和 IndexedDB 分别承担静态与动态存储职责,本地优先同步模式则保证了数据最终一致性。后续章节将逐一深入每个组件的集成测试方法,本章建立的全局视图是理解所有测试策略的基础。