React 与 PWA 深度集成:在 React 生命周期中管理 Service Worker 更新流与离线数据同步逻辑

嘿,各位代码探险家们,大家好!

今天我们不讲那些花里胡哨的框架新特性,也不聊那些只会让你秃头的“性能优化玄学”。我们要聊的是 PWA(渐进式 Web 应用)的核心灵魂——Service Worker,以及它如何在这个充满不确定性的网络世界里,与 React 这个“前台明星”进行一场深度的、纠缠不清的恋爱。

想象一下,如果你的应用是一个高端餐厅,React 就是那个在前台笑容满面、端着盘子招呼客人的服务员。而 Service Worker 呢?它就是那个躲在厨房后巷、甚至可能在你看不见的地方烤面包、切菜、甚至在客人走后悄悄擦桌子的幽灵大厨。React 不需要知道大厨怎么切洋葱,它只需要知道菜做好了没。

但问题是,大厨有时候会偷懒,有时候会发疯,有时候网络断了,大厨就得硬着头皮上。今天,我们就来聊聊如何驯服这只“幽灵大厨”,在 React 的生命周期里管理它的更新流,以及在离线时如何通过它来拯救你的数据。

准备好了吗?让我们把键盘敲得像打鼓一样响亮!


第一部分:Service Worker 是个什么鬼?(不仅仅是缓存)

首先,我们要给 Service Worker 正个名。它不是普通的 JavaScript 文件,它是一个完全独立的运行时环境

你可以把它看作是一个运行在你浏览器里的微型操作系统。它有自己的线程,有自己的事件循环。React 在主线程上跑,Service Worker 在它自己的线程上跑。它们互不干扰,但又通过一种叫做“消息传递”的机制(也就是 postMessageaddEventListener)来沟通。

为什么我们需要它?
为了离线。为了拦截网络请求。为了在用户没网的时候,依然能展示那张精美的“离线页面”。为了在用户点赞的时候,即使网络断了,点赞也能先存下来,等有网了再发出去。

1. 注册 Service Worker:第一次见面

在 React 应用里,我们通常在应用的入口文件(比如 index.jsApp.js)里注册 SW。这是 React 生命周期的“出生点”。

// src/index.js 或 App.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

// 检查浏览器是否支持 Service Worker
if ('serviceWorker' in navigator) {
  // navigator.serviceWorker.register('/sw.js')
  // 这行代码告诉浏览器:“嘿,去后台找个叫 sw.js 的文件,让它接管我的请求!”
  navigator.serviceWorker.register('/sw.js')
    .then((registration) => {
      console.log('SW 注册成功!就像给幽灵大厨发了入职通知书。');

      // 这里我们还能拿到 registration 对象,它是后续控制大厨的关键钥匙
      return registration;
    })
    .catch((error) => {
      console.error('SW 注册失败!可能是路径错了,或者大厨罢工了。', error);
    });
}

ReactDOM.render(<App />, document.getElementById('root'));

注意看,register 返回的是一个 Promise。这个 Promise 解决后,我们拿到的是 Registration 对象。这个对象里有个属性叫 active,那个才是真正干活的大厨。


第二部分:React 生命周期与 Service Worker 的第一次握手

当用户刷新页面时,React 重新挂载。此时,Service Worker 也会重新激活。我们需要知道,当前的页面到底是由哪个版本的 Service Worker 控制的。

1. 获取 Controller

React 的 useEffect 钩子是我们感知外部世界变化的最佳场所。

import React, { useEffect, useState } from 'react';

function App() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const [swVersion, setSwVersion] = useState('Unknown');
  const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);

  useEffect(() => {
    // 1. 初始化时检查当前是否有 Controller
    // controller 为 null 的情况通常发生在第一次加载或者 SW 还没激活时
    if (navigator.serviceWorker.controller) {
      console.log('当前页面由已激活的 SW 控制');
      setSwVersion('Current');
    }

    // 2. 监听 controller 变化事件
    // 这就是 React 感知 SW 变化的核心:当 SW 更新并接管页面时,触发这个事件
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      console.log('大厨换人了!新的大厨接管了厨房。');
      setSwVersion('New Version Active');

      // 新的大厨来了,页面通常会自动刷新吗?不一定,取决于 SW 的逻辑。
      // 但在很多场景下,我们需要手动刷新,或者提示用户。
    });

    // 3. 监听在线/离线状态
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return (
    <div className="App">
      <h1>React & PWA 深度集成演示</h1>
      <p>当前网络状态: {isOnline ? '在线 (光纤已连接)' : '离线 (信号正在挣扎)'}</p>
      <p>当前 SW 版本: {swVersion}</p>
      <p>新版本可用: {isNewVersionAvailable ? '是的!快去更新!' : '没有新版本'}</p>
    </div>
  );
}

export default App;

这段代码展示了 React 如何监听 SW 的生命周期事件。controllerchange 是一个关键的钩子,它告诉我们:嘿,后台那个家伙更新了,现在他开始控制前台了。


第三部分:Service Worker 更新流管理(如何追上大厨的步伐)

这是最让人头疼的部分。Service Worker 默认是“懒”的。它不会一注册就立即去下载最新的代码。它必须等到用户下次访问时,才会去检查是否有新版本。

1. 手动触发更新

有时候,我们需要在用户点击“检查更新”按钮时,强制 SW 去检查。

const triggerUpdate = () => {
  if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
  } else {
    console.log('没有 Controller,无法触发更新。');
  }
};

注意,这行代码把消息发给了当前的 controller(旧的大厨)。旧的大厨收到消息后,会把控制权交给 waiting 队列里的新大厨。但是,旧的大厨通常不会立刻切换,除非它调用了 skipWaiting()

2. 监听 waiting 状态

当 SW 检测到新版本并下载完成,但还没激活时,它会进入 waiting 状态。我们需要在 React 里检测这个状态,然后弹出一个漂亮的 Toast 提示:“亲,有新版本了哦~”。

import React, { useEffect, useState, useRef } from 'react';

function UpdatePrompt() {
  const [shouldShow, setShouldShow] = useState(false);
  const registrationRef = useRef(null);

  useEffect(() => {
    // 注册 SW
    const setupSW = async () => {
      if (!('serviceWorker' in navigator)) return;

      const registration = await navigator.serviceWorker.register('/sw.js');
      registrationRef.current = registration;

      // 监听 'updatefound' 事件:当 SW 正在下载新版本时触发
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;

        // 监听新 Worker 的状态变化
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // 关键时刻到了!
            // newWorker.state === 'installed' 且 controller 存在
            // 说明有新版本在等待,但当前页面还是用的旧版本
            console.log('新版本已安装,正在等待激活。');
            setShouldShow(true);
          }
        });
      });
    };

    setupSW();

    // 监听 controllerchange,如果用户手动刷新了,且新版本已经激活
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      setShouldShow(false);
    });
  }, []);

  const handleUpdate = () => {
    if (registrationRef.current && registrationRef.current.waiting) {
      // 告诉等待中的新大厨:“别等了,赶紧上!”
      registrationRef.current.waiting.postMessage({ type: 'SKIP_WAITING' });
      // 刷新页面,让新大厨接管一切
      window.location.reload();
    }
  };

  if (!shouldShow) return null;

  return (
    <div style={{ position: 'fixed', bottom: 20, right: 20, background: '#333', color: '#fff', padding: 15, borderRadius: 8 }}>
      <p>发现新版本!</p>
      <button onClick={handleUpdate}>立即更新</button>
    </div>
  );
}

export default UpdatePrompt;

这里有个细节:为什么我们要在 updatefound 里监听 installed 状态?因为如果用户没有网络,或者 SW 没有更新,这个状态永远不会触发。只有当新版本下载完毕,且当前页面还在运行旧版本时,这个状态才会出现。

但是! 这里有个巨大的坑。window.location.reload() 会重置 React 的状态。如果你在页面顶部有个计数器,更新后它会归零。这是 Service Worker 的“副作用”。为了解决这个问题,我们通常需要把关键状态保存在 localStorage 或 IndexedDB 里,然后在 SW 切换时从那里恢复。


第四部分:离线数据同步逻辑(当网络消失时)

光有缓存是不够的,我们还需要保存用户输入的数据。当用户在离线状态下提交了一个表单,或者保存了一张图片,React 应该怎么做?是报错?还是直接吞掉?显然,我们不能吞掉,我们需要把它存下来,等有网了再发出去。

1. IndexedDB:离线数据的保险箱

React 里的 localStorage 只能存字符串,而且容量很小(5MB)。对于图片、大文件或者复杂的 JSON 对象,localStorage 会直接崩溃。这时候,我们要祭出 IndexedDB。它是一个浏览器内置的 NoSQL 数据库,存取方便,容量巨大。

为了方便在 React 里操作,我们可以写一个简单的封装工具。

// src/utils/db.js
import { openDB } from 'idb'; // 使用 idb 库,它是个好东西,省去了写大量样板代码的痛苦

const DB_NAME = 'MyPWA_DB';
const DB_VERSION = 1;
const STORE_NAME = 'pending-sync';

// 打开数据库
const dbPromise = openDB(DB_NAME, DB_VERSION, {
  upgrade(db) {
    // 创建一个对象仓库,用来存待同步的数据
    if (!db.objectStoreNames.contains(STORE_NAME)) {
      db.createObjectStore(STORE_NAME);
    }
  },
});

// 存储数据
export const saveOfflineData = async (key, data) => {
  try {
    const db = await dbPromise;
    await db.put(STORE_NAME, data, key);
    console.log(`数据 [${key}] 已存入保险箱(离线)。`);
  } catch (error) {
    console.error('存入数据库失败', error);
  }
};

// 获取数据
export const getOfflineData = async (key) => {
  try {
    const db = await dbPromise;
    return await db.get(STORE_NAME, key);
  } catch (error) {
    console.error('从数据库获取失败', error);
  }
};

// 删除数据(同步成功后)
export const deleteOfflineData = async (key) => {
  try {
    const db = await dbPromise;
    await db.delete(STORE_NAME, key);
  } catch (error) {
    console.error('删除数据库记录失败', error);
  }
};

2. React 中的离线提交逻辑

现在,我们在 React 组件里使用这个工具。

import React, { useState, useEffect } from 'react';
import { saveOfflineData, deleteOfflineData } from '../utils/db';

function OfflineForm() {
  const [status, setStatus] = useState('Ready');
  const [formData, setFormData] = useState({ title: '', content: '' });

  const handleSubmit = async (e) => {
    e.preventDefault();
    setStatus('Submitting...');

    // 模拟一个 API 请求
    const apiCall = async () => {
      await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟
      // 假设这里返回成功
      return { success: true };
    };

    try {
      // 尝试发送
      await apiCall();
      setStatus('Success!');
      setFormData({ title: '', content: '' });
    } catch (error) {
      console.error('网络断了!保存离线数据。');
      // 如果失败,存入 IndexedDB
      await saveOfflineData('form_data', formData);
      setStatus('Saved Offline. Waiting for network...');
    }
  };

  // 监听网络状态,如果网络恢复了,尝试同步
  useEffect(() => {
    if (navigator.onLine) {
      // 网络回来了!去数据库里看看有没有漏网之鱼
      getOfflineData('form_data').then(data => {
        if (data) {
          // 这里应该调用同步 API
          console.log('网络恢复,正在同步数据...', data);
          deleteOfflineData('form_data'); // 同步成功后删除
          setStatus('Synced successfully!');
        }
      });
    }
  }, []);

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={formData.title} 
        onChange={e => setFormData({...formData, title: e.target.value})}
        placeholder="Title"
      />
      <textarea 
        value={formData.content} 
        onChange={e => setFormData({...formData, content: e.target.value})}
        placeholder="Content"
      />
      <button type="submit" disabled={status === 'Submitting...' || status === 'Saved Offline'}>
        {status}
      </button>
    </form>
  );
}

export default OfflineForm;

这段代码展示了 React 如何处理“乐观更新”的反向情况——悲观更新。当网络不可用时,我们主动将数据存入数据库,并提示用户。


第五部分:Service Worker 中的同步逻辑(后台的执行者)

刚才我们在 React 里把数据存进了 DB。现在,最关键的一步来了:当网络恢复时,Service Worker 如何知道?

我们不能指望 React 去一直轮询 navigator.onLine。这不仅浪费性能,而且 React 还可能被卸载。真正的力量在于 Service Worker。

1. SyncManager API

Service Worker 提供了 SyncManager API。我们可以给 Service Worker 注册一个同步任务。当网络恢复时,浏览器会自动触发这个任务。

// src/sw.js
self.addEventListener('install', (event) => {
  console.log('[SW] Installing...');
  self.skipWaiting(); // 立即激活新版本
});

self.addEventListener('activate', (event) => {
  console.log('[SW] Activating...');
  event.waitUntil(
    self.clients.claim() // 立即控制所有页面
  );
});

// 监听网络恢复事件
self.addEventListener('online', () => {
  console.log('[SW] 网络恢复了!去干活吧!');
  // 触发同步任务
  syncPendingTasks();
});

async function syncPendingTasks() {
  // 获取 SyncManager
  const registration = await navigator.serviceWorker.ready;

  // 注册一个名为 'sync-data' 的同步任务
  // 注意:这个任务会在网络恢复后的某个时间点触发(不是立刻)
  try {
    await registration.sync.register('sync-data');
  } catch (err) {
    console.error('注册同步任务失败', err);
  }
}

// 处理同步事件
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-data') {
    event.waitUntil(syncData());
  }
});

async function syncData() {
  console.log('[SW] 开始同步数据...');

  // 1. 从 IndexedDB 获取数据
  // 这里需要引入我们在 React 里用的 idb 库,或者 Service Worker 里也写一套 DB 操作
  // 为了简单演示,我们假设有个函数叫 getPendingData()
  const pendingData = await getPendingData(); 
  // ... 实际上 Service Worker 里也需要连接数据库逻辑 ...

  if (pendingData) {
    try {
      // 2. 发送请求到后端 API
      await fetch('https://api.example.com/data', {
        method: 'POST',
        body: JSON.stringify(pendingData),
        headers: { 'Content-Type': 'application/json' }
      });

      // 3. 同步成功,清理数据
      console.log('[SW] 同步成功,清理本地数据');
      await deletePendingData(pendingData.id);

      // 4. 通知所有打开的页面
      self.clients.matchAll().then(clients => {
        clients.forEach(client => {
          client.postMessage({ type: 'SYNC_SUCCESS', payload: pendingData });
        });
      });
    } catch (error) {
      console.error('[SW] 同步失败', error);
      // 失败了怎么办?通常需要重试机制,或者标记为死信,稍后重试
    }
  }
}

这里有一个非常重要的概念:事件循环。Service Worker 是单线程的。如果在 sync 事件里执行了一个非常耗时的数据库操作,或者一个很慢的 API 请求,整个 Service Worker 可能会“卡住”,导致后续的网络请求(比如用户正在加载图片)也被阻塞。

所以,在 Service Worker 里,对于同步任务,我们要确保它不阻塞。如果同步失败,不要让整个 Worker 冻结。

2. React 接收同步成功的消息

现在,Service Worker 同步完了,它怎么告诉 React 呢?它通过 postMessage

// 在 React 组件里
useEffect(() => {
  const messageHandler = (event) => {
    if (event.data && event.data.type === 'SYNC_SUCCESS') {
      // 用户看到同步成功的提示
      setStatus('数据已同步到云端!');
      // 清空本地状态
      setFormData({ title: '', content: '' });
    }
  };

  navigator.serviceWorker.addEventListener('message', messageHandler);

  return () => {
    navigator.serviceWorker.removeEventListener('message', messageHandler);
  };
}, []);

第六部分:高级缓存策略(不仅仅是 Cache First)

在 Service Worker 里,fetch 事件是控制网络请求的核心。我们不能对所有请求都用一种策略。

1. 策略选择

  • Cache First (缓存优先): 适用于静态资源(JS, CSS, 图片)。先去缓存找,找不到再联网。这是 PWA 的基石。
  • Network First (网络优先): 适用于 API 请求。先联网,失败再读缓存。确保用户看到的是最新的数据。
  • Stale While Revalidate (过期但重新验证): 最聪明的策略。先返回缓存里的旧数据(保证快),同时在后台去更新缓存。用户感觉不到延迟,但数据总是最新的。

2. 实现 Stale While Revalidate

sw.js 里:

self.addEventListener('fetch', (event) => {
  // 1. 判断是否是 API 请求
  const isApi = event.request.url.includes('/api');

  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      // 2. 无论有没有缓存,都尝试去网络获取新数据(Promise.race)
      const fetchPromise = fetch(event.request).then((networkResponse) => {
        // 3. 把新数据存入缓存(更新缓存)
        const cloneResponse = networkResponse.clone();
        caches.open('api-cache').then((cache) => {
          cache.put(event.request, cloneResponse);
        });
        return networkResponse;
      });

      // 4. 返回缓存(如果有)或者 等待网络(如果没缓存)
      // 这就是 Stale While Revalidate 的精髓:先给缓存,后台更新
      return cachedResponse || fetchPromise;
    })
  );
});

第七部分:处理 Service Worker 的更新与页面状态丢失

这是最痛苦的部分。当用户点击“更新”后,页面刷新。React 组件重新挂载。之前的状态没了。比如用户正在编辑一篇长文章,更新后,文章内容清空了。

解决方案:数据持久化

我们需要在 React 组件卸载前(useEffect 的清理函数),把当前状态存入 IndexedDB。在组件挂载时,先去 DB 读,如果有数据,先恢复。

function Editor() {
  const [content, setContent] = useState('');

  useEffect(() => {
    // 挂载时恢复数据
    const loadContent = async () => {
      const saved = await getOfflineData('editor_content');
      if (saved) setContent(saved);
    };
    loadContent();

    // 卸载时保存数据
    return () => {
      saveOfflineData('editor_content', content);
    };
  }, []);

  return <textarea value={content} onChange={e => setContent(e.target.value)} />;
}

这样,即使用户更新了 PWA,他的草稿也不会丢失。虽然 Service Worker 会重置 React 的执行上下文,但我们的 IndexedDB 是独立于 React 生命周期的,它就像一个忠实的秘书,记录了用户的所有操作。


第八部分:Service Worker 的错误处理与降级

Service Worker 很强大,但它也很脆弱。如果 sw.js 文件加载出错,或者 SW 报错,会发生什么?

默认情况下,如果 SW 失败,浏览器会尝试恢复到旧的版本。但如果 SW 持续失败,浏览器可能会禁用 SW。

我们需要在 sw.js 里捕获错误,并上报给服务器(或者 Sentry)。

self.addEventListener('error', (event) => {
  console.error('[SW] 发生了全局错误', event.error);
  // 上报错误
  // navigator.serviceWorker.controller.postMessage({ type: 'SW_ERROR', error: event.error });
});

同时,在 React 里,我们要处理 navigator.serviceWorker.controller 为 null 的情况。如果 SW 不可用,我们不应该强行依赖 SW,而是回退到普通的 HTTP 缓存逻辑,或者直接请求网络。


第九部分:实战演练——一个完整的 PWA 组件

让我们把这些串起来。一个包含自动更新检测、离线表单提交、数据同步的完整组件。

import React, { useEffect, useState } from 'react';
import { saveOfflineData, getOfflineData, deleteOfflineData } from '../utils/db';

function OfflineTodoApp() {
  const [todos, setTodos] = useState([]);
  const [isOnline, setIsOnline] = useState(true);
  const [status, setStatus] = useState('Ready');

  // 1. 初始化数据(从 IndexedDB 恢复)
  useEffect(() => {
    const initData = async () => {
      const saved = await getOfflineData('todos');
      if (saved) {
        setTodos(saved);
        setStatus('Data restored from offline storage');
      }
    };
    initData();

    // 2. 监听网络状态
    window.addEventListener('online', () => {
      setIsOnline(true);
      checkSync(); // 网络恢复,尝试同步
    });
    window.addEventListener('offline', () => setIsOnline(false));
  }, []);

  // 3. 检查同步(从 Service Worker 传来的消息)
  useEffect(() => {
    const handleMsg = (e) => {
      if (e.data.type === 'SYNC_SUCCESS') {
        setTodos([]);
        setStatus('Synced!');
        // 刷新页面通常是个好主意,确保数据一致
        setTimeout(() => window.location.reload(), 1000);
      }
    };
    navigator.serviceWorker.addEventListener('message', handleMsg);
    return () => navigator.serviceWorker.removeEventListener('message', handleMsg);
  }, []);

  const addTodo = async (text) => {
    const newTodo = { id: Date.now(), text, synced: false };
    const updatedTodos = [...todos, newTodo];
    setTodos(updatedTodos);
    await saveOfflineData('todos', updatedTodos);

    if (navigator.onLine) {
      try {
        await fetch('https://api.example.com/todos', {
          method: 'POST',
          body: JSON.stringify(newTodo),
        });
        // API 成功,标记同步
        const syncedTodos = updatedTodos.map(t => t.id === newTodo.id ? { ...t, synced: true } : t);
        setTodos(syncedTodos);
      } catch (e) {
        console.error('API 失败,继续离线模式');
      }
    }
  };

  const checkSync = async () => {
    // 简单的同步逻辑:如果当前没有数据,说明之前是离线的,现在有网了,去取数据
    // 实际项目中这里应该从 Service Worker 的 SyncManager 逻辑中获取结果
    // 这里为了演示,直接从 DB 读取
    const pending = await getOfflineData('todos');
    if (pending && pending.length > 0) {
      // 尝试批量同步
      setStatus('Syncing...');
      try {
        await fetch('https://api.example.com/batch-sync', {
          method: 'POST',
          body: JSON.stringify(pending),
        });
        // 同步成功,清空 DB
        await deleteOfflineData('todos');
        setStatus('Synced successfully');
      } catch (e) {
        setStatus('Sync failed, will retry later');
      }
    }
  };

  return (
    <div style={{ padding: 20 }}>
      <h1>离线待办事项</h1>
      <p>状态: {isOnline ? '在线' : '离线'} | {status}</p>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.text} {todo.synced ? '✅' : '⏳'}
          </li>
        ))}
      </ul>
      <input 
        placeholder="Add a todo..." 
        onKeyDown={e => e.key === 'Enter' && addTodo(e.target.value)}
      />
    </div>
  );
}

export default OfflineTodoApp;

第十部分:Service Worker 的生命周期图解(脑补版)

为了确保大家理解,我们画个图(虽然是在文字里):

  1. Install (安装): 大厨搬进厨房,把新菜谱(缓存文件)摆好。此时是 installing 状态。
  2. Waiting (等待): 大厨搬完了,站在门口等。此时是 waiting 状态。React 检测到这个状态,弹出“更新”按钮。
  3. Activate (激活): 用户点了更新,或者页面刷新。waiting 的大厨进来了,踢走了旧大厨。此时是 active 状态。controllerchange 事件触发。
  4. Fetch (拦截): 旧大厨走后,新大厨开始工作。每次 React 想要请客(发起请求),都得先问新大厨:“有缓存吗?”新大厨去翻冰箱,没有就跑去买菜。

如果在这个过程中,网络断了,React 的请求失败。React 把菜谱(数据)存进保险箱(IndexedDB),告诉新大厨:“这单生意先记着,等网好了再上菜。”


第十一部分:高级话题——Background Sync 的坑与解

SyncManager 是个好东西,但它不是 100% 可靠的。浏览器有配额,或者后台进程被杀。

重试策略:
我们不能指望浏览器每次都自动重试。我们需要在 Service Worker 里实现指数退避算法。

async function retrySync(data, attempt = 1) {
  const MAX_ATTEMPTS = 3;
  const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s...

  try {
    await fetch('api', { method: 'POST', body: data });
    return true;
  } catch (e) {
    if (attempt < MAX_ATTEMPTS) {
      console.log(`Sync failed, retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
      return retrySync(data, attempt + 1);
    }
    return false;
  }
}

第十二部分:总结与展望

好了,各位。我们今天聊了很多。从 Service Worker 的注册,到 React 的 useEffect 监听,再到 IndexedDB 的存储,最后是 SyncManager 的后台同步。

PWA 的集成不是一蹴而就的,它是一场持久战。你需要理解 Service Worker 的生命周期,理解 React 的状态管理,理解 IndexedDB 的异步操作。

记住几个关键点:

  1. 分离关注点: React 负责视图和状态,Service Worker 负责网络拦截和离线存储。
  2. 优雅降级: SW 失败时,不要让应用崩溃,要能回退到普通网页模式。
  3. 用户体验: 更新提示要友好,离线状态要明确,数据同步要有反馈。

现在,去写你的 PWA 吧!让你的应用像个原生 App 一样,随时随地待命。当你的用户在地铁上打开应用,数据依然丝滑流畅时,你会感谢今天这个讲座的。

代码已给出,逻辑已理清,剩下的,就交给你的键盘了。加油!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注