嘿,各位代码探险家们,大家好!
今天我们不讲那些花里胡哨的框架新特性,也不聊那些只会让你秃头的“性能优化玄学”。我们要聊的是 PWA(渐进式 Web 应用)的核心灵魂——Service Worker,以及它如何在这个充满不确定性的网络世界里,与 React 这个“前台明星”进行一场深度的、纠缠不清的恋爱。
想象一下,如果你的应用是一个高端餐厅,React 就是那个在前台笑容满面、端着盘子招呼客人的服务员。而 Service Worker 呢?它就是那个躲在厨房后巷、甚至可能在你看不见的地方烤面包、切菜、甚至在客人走后悄悄擦桌子的幽灵大厨。React 不需要知道大厨怎么切洋葱,它只需要知道菜做好了没。
但问题是,大厨有时候会偷懒,有时候会发疯,有时候网络断了,大厨就得硬着头皮上。今天,我们就来聊聊如何驯服这只“幽灵大厨”,在 React 的生命周期里管理它的更新流,以及在离线时如何通过它来拯救你的数据。
准备好了吗?让我们把键盘敲得像打鼓一样响亮!
第一部分:Service Worker 是个什么鬼?(不仅仅是缓存)
首先,我们要给 Service Worker 正个名。它不是普通的 JavaScript 文件,它是一个完全独立的运行时环境。
你可以把它看作是一个运行在你浏览器里的微型操作系统。它有自己的线程,有自己的事件循环。React 在主线程上跑,Service Worker 在它自己的线程上跑。它们互不干扰,但又通过一种叫做“消息传递”的机制(也就是 postMessage 和 addEventListener)来沟通。
为什么我们需要它?
为了离线。为了拦截网络请求。为了在用户没网的时候,依然能展示那张精美的“离线页面”。为了在用户点赞的时候,即使网络断了,点赞也能先存下来,等有网了再发出去。
1. 注册 Service Worker:第一次见面
在 React 应用里,我们通常在应用的入口文件(比如 index.js 或 App.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 的生命周期图解(脑补版)
为了确保大家理解,我们画个图(虽然是在文字里):
- Install (安装): 大厨搬进厨房,把新菜谱(缓存文件)摆好。此时是
installing状态。 - Waiting (等待): 大厨搬完了,站在门口等。此时是
waiting状态。React 检测到这个状态,弹出“更新”按钮。 - Activate (激活): 用户点了更新,或者页面刷新。
waiting的大厨进来了,踢走了旧大厨。此时是active状态。controllerchange事件触发。 - 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 的异步操作。
记住几个关键点:
- 分离关注点: React 负责视图和状态,Service Worker 负责网络拦截和离线存储。
- 优雅降级: SW 失败时,不要让应用崩溃,要能回退到普通网页模式。
- 用户体验: 更新提示要友好,离线状态要明确,数据同步要有反馈。
现在,去写你的 PWA 吧!让你的应用像个原生 App 一样,随时随地待命。当你的用户在地铁上打开应用,数据依然丝滑流畅时,你会感谢今天这个讲座的。
代码已给出,逻辑已理清,剩下的,就交给你的键盘了。加油!