1. 项目背景与核心挑战
最近在帮客户部署一个基于Uniapp开发的PWA应用时,遇到了不少棘手的坑。这个项目原本计划两周内上线,结果光是解决PWA的部署问题就耗掉了五天时间。现在把实战中遇到的HTTPS配置、缓存策略和CDN优化三大核心问题的解决方案整理出来,希望能帮到同样踩坑的朋友。
PWA(Progressive Web App)作为现代Web应用的重要形态,确实能给Uniapp项目带来原生应用般的体验。但在实际部署时,你会发现官方文档里没写的细节才是真正要命的地方。比如为什么有些安卓设备无法安装PWA?为什么CDN缓存会导致manifest.json更新延迟?这些都是在真实业务场景中才会暴露的问题。
2. HTTPS配置全流程解析
2.1 证书选择与部署
部署PWA的首要条件就是HTTPS。实测发现,不同证书类型对PWA的兼容性影响很大:
- Let's Encrypt免费证书:兼容性最好,但需要每三个月续期
- 商业OV证书:部分旧设备可能出现链式证书校验问题
- 自签名证书:开发环境可用,但会导致PWA安装功能失效
推荐使用acme.sh自动化管理Let's Encrypt证书:
bash复制# 安装acme.sh
curl https://get.acme.sh | sh
# 签发证书(DNS验证方式)
acme.sh --issue --dns dns_cf -d example.com
# 安装证书到Nginx目录
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.key \
--fullchain-file /etc/nginx/ssl/example.crt \
--reloadcmd "systemctl reload nginx"
2.2 Nginx关键配置
在nginx.conf中必须加入以下安全头:
nginx复制server {
listen 443 ssl;
server_name example.com;
# HSTS策略(强制HTTPS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 禁止MIME类型嗅探
add_header X-Content-Type-Options "nosniff";
# 点击劫持防护
add_header X-Frame-Options "SAMEORIGIN";
# CSP策略(根据实际资源调整)
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' https://unpkg.com;";
}
特别注意:如果使用WebSocket,需要在CSP中额外添加connect-src规则
3. 缓存策略深度优化
3.1 Service Worker缓存策略
Uniapp生成的service-worker.js默认使用CacheFirst策略,这在生产环境会导致严重问题。建议修改为:
javascript复制workbox.routing.registerRoute(
new RegExp('.*\.js'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'js-cache',
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30天
})
]
})
);
3.2 静态资源版本控制
在vue.config.js中配置:
javascript复制module.exports = {
chainWebpack: config => {
config.output.filename('js/[name].[contenthash:8].js');
config.output.chunkFilename('js/[name].[contenthash:8].js');
}
}
同时需要在Nginx配置长期缓存:
nginx复制location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
4. CDN部署特殊处理
4.1 回源策略优化
当使用CDN时,必须确保以下路径不走CDN缓存:
- /manifest.json
- /service-worker.js
- /index.html
在腾讯云CDN的配置示例:
json复制{
"CacheRules": [
{
"Type": "file",
"Value": "/manifest.json",
"Cache": false
},
{
"Type": "file",
"Value": "/service-worker.js",
"Cache": false
}
]
}
4.2 跨域资源共享(CORS)
如果静态资源部署在不同域名下,需要在CDN配置:
http复制Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Max-Age: 86400
5. 典型问题排查实录
5.1 PWA无法安装问题
常见症状:
- 安卓设备没有"添加到主屏幕"提示
- iOS设备提示"无法安装此网站"
排查步骤:
- 检查manifest.json是否可访问(返回200状态码)
- 验证manifest中的start_url是否与当前页面URL匹配
- 确认Service Worker已成功注册(Chrome DevTools → Application → Service Workers)
- 检查控制台是否有CSP策略拦截
5.2 缓存更新延迟问题
解决方案:
- 在service-worker.js中添加版本标识
javascript复制const CACHE_VERSION = 'v2.3.1';
- 修改Uniapp构建脚本,自动更新版本号
- 在install事件中清理旧缓存
javascript复制self.addEventListener('install', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.map(key =>
key.includes(CACHE_VERSION) ? null : caches.delete(key)
))
)
);
});
6. 性能优化实战技巧
6.1 预加载关键资源
在index.html中添加:
html复制<link rel="preload" href="/static/js/vendor.js" as="script">
<link rel="preload" href="/static/css/app.css" as="style">
6.2 图片优化方案
- 使用WebP格式(兼容性处理):
html复制<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>
- 实现懒加载:
javascript复制// 使用IntersectionObserver API
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
7. 多平台兼容性处理
7.1 iOS特殊处理
iOS对PWA的支持有诸多限制,需要额外处理:
- 状态栏颜色设置(在manifest.json中):
json复制{
"theme_color": "#4DBA87",
"apple-mobile-web-app-capable": "yes",
"apple-mobile-web-app-status-bar-style": "black-translucent"
}
- 解决键盘弹起布局问题:
css复制/* 在App.vue中全局生效 */
body {
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
7.2 微信浏览器兼容方案
微信内置浏览器需要特殊处理:
- 检测微信环境:
javascript复制const isWeChat = /micromessenger/i.test(navigator.userAgent);
- 显示引导提示:
javascript复制if (isWeChat) {
showModal({
title: '使用提示',
content: '建议点击右上角菜单,选择"在浏览器打开"获得最佳体验'
});
}
8. 监控与异常处理
8.1 错误监控实现
在main.js中集成Sentry:
javascript复制import * as Sentry from '@sentry/vue';
import { Integrations } from '@sentry/tracing';
if (process.env.NODE_ENV === 'production') {
Sentry.init({
Vue,
dsn: 'your-dsn-here',
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0.2
});
}
8.2 Service Worker异常处理
增强service-worker.js的健壮性:
javascript复制self.addEventListener('error', event => {
Sentry.captureException(event.error);
});
self.addEventListener('unhandledrejection', event => {
Sentry.captureException(event.reason);
});
9. 部署流程自动化
9.1 CI/CD集成示例
.gitlab-ci.yml配置示例:
yaml复制stages:
- build
- deploy
build:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
deploy:
stage: deploy
script:
- rsync -avz --delete dist/ user@server:/var/www/html/
- ssh user@server "cd /var/www/html && chmod -R 755 ."
only:
- master
9.2 版本回滚机制
创建版本化部署目录:
bash复制#!/bin/bash
TIMESTAMP=$(date +%Y%m%d%H%M%S)
DEPLOY_DIR="/var/www/releases/$TIMESTAMP"
# 创建新版本目录
mkdir -p $DEPLOY_DIR
cp -r dist/* $DEPLOY_DIR
# 切换软链接
ln -sfn $DEPLOY_DIR /var/www/current
# 保留最近5个版本
ls -dt /var/www/releases/* | tail -n +6 | xargs rm -rf
10. 高级调试技巧
10.1 Lighthouse深度优化
针对Lighthouse报告的优化建议:
- 消除阻塞渲染资源:
html复制<!-- 将关键CSS内联 -->
<style>/* 压缩后的关键CSS */</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'">
- 优化CLS(布局偏移):
javascript复制// 为动态内容预留空间
document.addEventListener('DOMContentLoaded', () => {
const adContainer = document.getElementById('ad-container');
if (!adContainer.innerHTML) {
adContainer.style.minHeight = '90px'; // 广告位的预估高度
}
});
10.2 远程调试方案
安卓设备远程调试步骤:
- 手机开启USB调试模式
- Chrome访问 chrome://inspect/#devices
- 在手机上用Chrome打开目标PWA
- 点击对应页面的"inspect"按钮
iOS设备需要:
- 使用Safari开发模式(需在偏好设置中启用)
- 通过USB连接Mac电脑
- 在Safari的"开发"菜单中选择设备
11. 安全加固措施
11.1 CSP策略配置
完整的内容安全策略示例:
nginx复制add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-eval' https://unpkg.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://*.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://socket.example.com;
frame-src 'none';
object-src 'none';
";
11.2 防止XSS攻击
在Vue中的全局防护措施:
javascript复制// main.js
Vue.prototype.$sanitize = (html) => {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
};
// 使用示例
this.renderedContent = this.$sanitize(userInput);
12. 离线功能增强
12.1 动态缓存策略
根据用户行为预缓存:
javascript复制// 监听路由变化
router.afterEach((to) => {
if (workbox) {
workbox.routing.registerRoute(
to.path,
new workbox.strategies.CacheFirst()
);
}
});
12.2 后台同步实现
处理离线时的表单提交:
javascript复制// 注册同步事件
navigator.serviceWorker.ready.then(registration => {
document.getElementById('submit-form').addEventListener('click', () => {
if (!navigator.onLine) {
registration.sync.register('form-submission').then(() => {
showToast('数据将在网络恢复后自动提交');
});
}
});
});
// Service Worker中处理
self.addEventListener('sync', event => {
if (event.tag === 'form-submission') {
event.waitUntil(submitPendingData());
}
});
13. 测试策略设计
13.1 自动化测试方案
使用Jest + Puppeteer的测试配置:
javascript复制// __tests__/pwa.test.js
describe('PWA Features', () => {
beforeAll(async () => {
await page.goto('https://example.com');
});
test('should register Service Worker', async () => {
const swState = await page.evaluate(() =>
navigator.serviceWorker.controller?.state
);
expect(swState).toBe('activated');
});
test('should have valid manifest', async () => {
const manifest = await page.evaluate(() =>
JSON.parse(document.querySelector('link[rel="manifest"]').href)
);
expect(manifest.name).toBeTruthy();
});
});
13.2 离线场景测试
模拟不同网络条件:
javascript复制// 在Chrome DevTools中执行
const conditions = [
{ download: 500, upload: 250, latency: 200 }, // 3G
{ download: 1500, upload: 750, latency: 40 }, // 4G
{ offline: true } // 完全离线
];
conditions.forEach(condition => {
describe(`Network: ${JSON.stringify(condition)}`, () => {
beforeAll(async () => {
await page.emulateNetworkConditions(condition);
});
test('should show offline fallback', async () => {
// 测试逻辑
});
});
});
14. 性能指标监控
14.1 核心Web指标采集
使用web-vitals库:
javascript复制import { getCLS, getFID, getLCP } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify(metric);
navigator.sendBeacon('/analytics', body);
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);
14.2 自定义性能标记
关键业务流程打点:
javascript复制// 在Vue组件中
export default {
mounted() {
performance.mark('component_visible');
this.$nextTick(() => {
performance.measure('render_time', 'component_visible');
const measures = performance.getEntriesByName('render_time');
console.log(`Render took ${measures[0].duration}ms`);
});
}
}
15. 渐进增强策略
15.1 功能检测方案
优雅降级处理:
javascript复制// 检查PWA安装能力
const canInstallPWA = () => {
return 'BeforeInstallPromptEvent' in window ||
(navigator.standalone !== undefined && !navigator.standalone);
};
// 检查Service Worker支持
const hasSWSupport = 'serviceWorker' in navigator;
// 根据能力展示不同UI
if (!hasSWSupport) {
document.getElementById('offline-feature').style.display = 'none';
}
15.2 本地存储策略
IndexedDB封装示例:
javascript复制class LocalDB {
constructor(name) {
this.dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(name, 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('cache')) {
db.createObjectStore('cache', { keyPath: 'id' });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = reject;
});
}
async get(key) {
const db = await this.dbPromise;
return new Promise((resolve) => {
const tx = db.transaction('cache', 'readonly');
const store = tx.objectStore('cache');
const request = store.get(key);
request.onsuccess = () => resolve(request.result?.value);
});
}
}
16. 用户引导设计
16.1 PWA安装提示
优化安装流程:
javascript复制let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// 显示自定义安装按钮
document.getElementById('install-btn').style.display = 'block';
});
document.getElementById('install-btn').addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
ga('send', 'event', 'PWA', 'install');
}
deferredPrompt = null;
document.getElementById('install-btn').style.display = 'none';
});
16.2 离线使用教育
首次离线体验设计:
javascript复制// 在Service Worker中
self.addEventListener('fetch', event => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match('/offline.html').then(response => {
if (response) return response;
// 动态生成离线页
return new Response(`
<h1>离线模式</h1>
<p>您当前处于离线状态,部分功能受限</p>
<button onclick="location.reload()">重试连接</button>
`, { headers: { 'Content-Type': 'text/html' } });
});
})
);
}
});
17. 多语言支持方案
17.1 动态manifest.json
根据语言生成不同manifest:
javascript复制// server-side处理(Node.js示例)
app.get('/manifest.json', (req, res) => {
const lang = req.acceptsLanguages(['zh', 'en']) || 'en';
const manifest = {
name: lang === 'zh' ? '我的应用' : 'My App',
// 其他多语言字段...
};
res.json(manifest);
});
17.2 Service Worker多语言缓存
按语言版本缓存资源:
javascript复制// 获取用户语言偏好
const userLang = navigator.language.split('-')[0] || 'en';
workbox.routing.registerRoute(
({url}) => url.pathname.startsWith('/locales/'),
new workbox.strategies.CacheFirst({
cacheName: `lang-${userLang}-cache`,
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 20
})
]
})
);
18. 更新策略设计
18.1 静默更新方案
后台自动更新逻辑:
javascript复制// 在注册Service Worker时
navigator.serviceWorker.register('/service-worker.js').then(reg => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本已就绪,下次加载时生效
console.log('New version available');
}
});
});
});
// 每小时检查一次更新
setInterval(() => {
navigator.serviceWorker.getRegistration().then(reg => reg?.update());
}, 60 * 60 * 1000);
18.2 用户提示更新
自定义更新提示:
javascript复制// 在Service Worker激活时发送消息
self.addEventListener('activate', event => {
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
clientList.forEach(client => {
client.postMessage({
type: 'UPDATE_AVAILABLE',
version: CACHE_VERSION
});
});
})
);
});
// 在页面中接收消息
navigator.serviceWorker.addEventListener('message', event => {
if (event.data.type === 'UPDATE_AVAILABLE') {
showUpdateDialog(event.data.version);
}
});
19. 数据分析集成
19.1 PWA特有指标追踪
关键指标监控:
javascript复制// 跟踪PWA安装来源
window.addEventListener('appinstalled', () => {
ga('send', 'event', 'PWA', 'installed',
window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser'
);
});
// 跟踪显示模式变化
window.matchMedia('(display-mode: standalone)').addListener((e) => {
if (e.matches) {
ga('send', 'event', 'PWA', 'launch', 'standalone');
}
});
19.2 离线行为分析
记录离线事件:
javascript复制// 检测网络状态变化
window.addEventListener('online', () => {
ga('send', 'event', 'Network', 'online');
submitOfflineEvents(); // 提交缓存的离线事件
});
window.addEventListener('offline', () => {
ga('send', 'event', 'Network', 'offline');
});
// 离线事件队列
const offlineEvents = [];
function trackEvent(category, action, label) {
const event = { category, action, label, timestamp: Date.now() };
if (navigator.onLine) {
ga('send', 'event', category, action, label);
} else {
offlineEvents.push(event);
showOfflineWarning();
}
}
function submitOfflineEvents() {
while (offlineEvents.length) {
const event = offlineEvents.shift();
ga('send', 'event',
event.category,
event.action,
event.label,
{ queueTime: Date.now() - event.timestamp }
);
}
}
20. 高级调试工具链
20.1 Workbox调试技巧
启用详细日志:
javascript复制// 在service-worker.js开头添加
workbox.setConfig({ debug: true });
// 或者在页面中通过URL参数控制
if (new URLSearchParams(location.search).has('sw-debug')) {
navigator.serviceWorker.register('/service-worker.js?debug=true');
}
20.2 缓存检查工具
开发自定义缓存查看器:
javascript复制function listCaches() {
return caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(name => {
return caches.open(name).then(cache => {
return cache.keys().then(requests => {
return {
name,
size: requests.length,
sample: requests.slice(0, 3).map(req => req.url)
};
});
});
})
);
});
}
// 在控制台直接调用
listCaches().then(console.table);
21. 构建优化策略
21.1 分包加载方案
优化vue.config.js配置:
javascript复制module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
maxSize: 244 * 1024, // 244KB
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
}
21.2 资源预加载
动态生成preload链接:
javascript复制// 在构建完成后生成preload清单
const { generatePreloadLinks } = require('./preload-generator');
module.exports = {
chainWebpack: config => {
config.plugin('html').tap(args => {
args[0].preloadLinks = generatePreloadLinks();
return args;
});
}
}
// 在public/index.html中添加
<% for (const link of htmlWebpackPlugin.options.preloadLinks) { %>
<link rel="preload" href="<%= link.href %>" as="<%= link.as %>">
<% } %>
22. 服务端渲染(SSR)兼容
22.1 基本SSR配置
修改server.js处理PWA:
javascript复制const renderPWAHeaders = (req, res, next) => {
res.set({
'Service-Worker-Allowed': '/',
'Cache-Control': 'no-cache'
});
next();
};
app.get('*', renderPWAHeaders, (req, res) => {
// SSR渲染逻辑...
});
22.2 离线SSR方案
预渲染关键路由:
javascript复制// 在Service Worker安装时
self.addEventListener('install', event => {
const urlsToCache = [
'/',
'/about',
'/contact'
];
event.waitUntil(
caches.open('ssr-cache').then(cache => {
return Promise.all(
urlsToCache.map(url =>
fetch(url).then(response => cache.put(url, response))
)
);
})
);
});
23. 混合应用集成
23.1 Cordova混合方案
在config.xml中添加:
xml复制<widget>
<preference name="DisallowOverscroll" value="true"/>
<preference name="BackgroundColor" value="#4DBA87"/>
<allow-navigation href="https://example.com/*" />
</widget>
23.2 Capacitor集成
配置capacitor.config.json:
json复制{
"server": {
"hostname": "example.com",
"iosScheme": "https",
"androidScheme": "https"
},
"plugins": {
"SplashScreen": {
"launchShowDuration": 3000,
"launchAutoHide": true
}
}
}
24. 无障碍访问优化
24.1 ARIA属性增强
关键交互元素优化:
vue复制<template>
<button
@click="toggleMenu"
aria-haspopup="true"
aria-expanded="isMenuOpen"
aria-controls="main-menu"
>
菜单
</button>
<ul id="main-menu" aria-hidden="!isMenuOpen">
<!-- 菜单项 -->
</ul>
</template>
24.2 焦点管理
确保键盘导航可用:
javascript复制// 在路由变化后管理焦点
router.afterEach((to) => {
setTimeout(() => {
const main = document.querySelector('main') || document.body;
const focusTarget = main.querySelector('[autofocus]') || main;
focusTarget.setAttribute('tabindex', '-1');
focusTarget.focus();
}, 100);
});
25. 移动端特殊适配
25.1 安全区域处理
适配iPhone X+系列:
css复制/* 在App.vue中 */
body {
padding: env(safe-area-inset-top)
env(safe-area-inset-right)
env(safe-area-inset-bottom)
env(safe-area-inset-left);
}
/* 底部固定元素 */
.fixed-bottom {
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
25.2 手势冲突解决
禁用下拉刷新:
javascript复制document.body.addEventListener('touchmove', (e) => {
if (window.scrollY === 0 && e.cancelable) {
e.preventDefault();
}
}, { passive: false });
26. 支付流程优化
26.1 支付SDK集成
动态加载支付SDK:
javascript复制function loadPaymentSDK() {
return new Promise((resolve, reject) => {
if (window.PaymentSDK) return resolve();
const script = document.createElement('script');
script.src = 'https://pay.example.com/sdk/v3';
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
}
// 在路由守卫中预加载
router.beforeEach((to, from, next) => {
if (to.meta.requiresPayment) {
loadPaymentSDK().then(next);
} else {
next();
}
});
26.2 离线支付队列
处理离线状态下的支付:
javascript复制// 在Service Worker中
self.addEventListener('sync', event => {
if (event.tag === 'process-payment') {
event.waitUntil(
getPendingPayments().then(payments =>
Promise.all(payments.map(processPayment))
)
);
}
});
// 页面中触发
function initiatePayment(data) {
if (navigator.onLine) {
return processPaymentOnline(data);
} else {
return savePaymentLocally(data).then(() => {
return navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('process-payment');
});
});
}
}
27. 通知系统实现
27.1 推送通知集成
处理推送事件:
javascript复制// 在Service Worker中
self.addEventListener('push', event => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/notification.png',
badge: '/icons/badge.png',
data: data.url
})
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
const url = event.notification.data;
for (const client of clientList) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
27.2 本地通知策略
定时提醒实现:
javascript复制// 请求通知权限
function requestNotificationPermission() {
return Notification.requestPermission().then(permission => {
if (permission !== 'granted') {
throw new Error('Permission not granted');
}
});
}
// 显示本地通知
function showLocalNotification(title, options) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(title, options);
} else if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(reg => {
reg.showNotification(title, options);
});
}
}
28. 状态持久化方案
28.1 数据同步策略
实现后台同步:
javascript复制// 页面中注册同步标签
navigator.serviceWorker.ready.then(reg => {
document.getElementById('sync-btn').addEventListener('click', () => {
reg.sync.register('sync-data').then(() => {
showToast('数据将在后台同步');
});
});
});
// Service Worker中处理
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncPendingChanges());
}
});
28.2 冲突解决机制
乐观UI更新:
javascript复制// 在Vuex store中
const store = new Vuex.Store({
mutations: {
updateItem(state, payload) {
// 本地立即更新
const item = state.items.find(i => i.id === payload.id);
if (item) Object.assign(item, payload.changes);
// 发起网络请求
api.updateItem(payload.id, payload.changes).catch(error => {
// 回滚变更
Object.assign(item, payload.original);
showError('更新失败,已恢复原值');
});
}
}
});
29. 测试覆盖率提升
29.1 离线场景测试套件
Jest离线测试配置:
javascript复制// __tests__/offline.test.js
beforeAll(() => {
// 模拟离线状态
Object.defineProperty(navigator, 'onLine', {
value: false,
writable: true
});
});
test('should show offline indicator', async () => {
const wrapper = mount(App);
await wrapper.vm.$nextTick();
expect(wrapper.find('.offline-alert').exists()).toBe(true);
});
29.2 Service Worker测试方案
测试SW生命周期:
javascript复制// __tests__/sw.test.js
describe('Service Worker', () => {
let swRegistration;
beforeAll(async () => {
swRegistration = await navigator.serviceWorker.register('/mock-sw.js');
await new Promise(resolve => {
const worker = swRegistration.installing;
worker.addEventListener('statechange', () => {
if (worker.state === 'activated') resolve();
});
});
});
test('should cache core assets', async () => {
const cache = await caches.open('core-assets');
const requests = await cache.keys();
expect(requests.some(req => req.url.includes('app.css'))).toBe(true);
});
});
30. 部署后监控体系
30.1 性能异常报警
监控关键指标波动:
javascript复制// 使用web-vitals库
import { getCLS, getFID, getLCP } from 'web-vitals';
function monitorMetrics() {
const metrics = {};
function logMetric(name, value) {
metrics[name] = value;
// 异常值检测
const thresholds = {
CLS: 0.25,
FID: 300,
LCP: 2500
};
if (value > thresholds[name]) {
reportError(`${name} exceeded threshold: ${value}`);
}
}
getCLS(logMetric);
getFID(logMetric);
getLCP(logMetric);
// 每10秒上报一次
setInterval(() => {
if (Object.keys(metrics).length) {
sendToAnalytics(metrics);
}
}, 10000);
}
monitorMetrics();
30.2 用户行为分析
跟踪核心流程:
javascript复制// 路由变化追踪
router.afterEach(to => {
ga('send', 'pageview', to.path);
// 标记PWA启动模式
if (window.matchMedia('(display-mode: standalone)').matches) {
ga('set', 'dimension1', 'standalone');
}
});
// 关键交互追踪
document.addEventListener('click', event => {
const target = event.target.closest('[data-track]');
if (target) {
const action = target.dataset.track;
ga('send', 'event', 'Interaction', action);
}
});
31. 持续优化策略
31.1 A/B测试实施
基于Service Worker的A/B测试:
javascript复制// 在Service Worker安装时
self.addEventListener('install', () => {
// 随机分配测试组
const group = Math.random() > 0.5 ? 'A' : 'B';
caches.open('config').then(cache =>
cache.put('/ab-group', new Response(group))
);
});
// 在请求拦截时
self.addEventListener('fetch', event => {
if (event.request.url.includes('/home')) {
event.respondWith(
caches.match('/ab-group').then(res =>
res?.text().then(group => {
const url = group ===