UniApp PWA开发实践:打造跨平台渐进式应用
前言
在过去的一年里,我们团队一直在探索如何利用UniApp框架开发高性能的PWA应用。特别是随着鸿蒙系统的普及,我们积累了不少有价值的实践经验。本文将分享我们在开发过程中的技术选型、架构设计和性能优化经验,希望能为大家提供一些参考。
技术栈选择
经过多轮技术评估,我们最终确定了以下技术栈:
- 基础框架:UniApp + Vue3 + TypeScript
- PWA框架:Workbox 7.x
- 状态管理:Pinia
- UI框架:uView UI
- 构建工具:Vite
- 鸿蒙适配:HMS Core
PWA基础配置
1. manifest.json配置
首先,我们需要在项目根目录下创建一个完整的manifest配置:
json
{
"name": "UniApp PWA Demo",
"short_name": "PWA Demo",
"description": "UniApp PWA应用示例",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#42b983",
"icons": [
{
"src": "/static/logo-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/static/logo-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"related_applications": [
{
"platform": "harmony",
"url": "market://details?id=com.example.pwa",
"id": "com.example.pwa"
}
]
}
2. Service Worker配置
我们使用Workbox来简化Service Worker的开发。以下是我们的核心配置:
typescript
// src/sw/service-worker.ts
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
// 预缓存
precacheAndRoute(self.__WB_MANIFEST);
// API请求缓存策略
registerRoute(
({ url }) => url.pathname.startsWith('/api'),
new StaleWhileRevalidate({
cacheName: 'api-cache',
plugins: [
new CacheableResponsePlugin({
statuses: [0, 200],
}),
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 24 * 60 * 60, // 1天
}),
],
})
);
// 静态资源缓存策略
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
}),
],
})
);
// 鸿蒙系统特殊处理
if (self.platform === 'harmony') {
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('hms-api')) {
// HMS API请求特殊处理
event.respondWith(
fetch(event.request)
.then((response) => {
const clonedResponse = response.clone();
caches.open('hms-api-cache').then((cache) => {
cache.put(event.request, clonedResponse);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
}
});
}
离线存储实现
为了提供更好的离线体验,我们实现了一个统一的存储管理器:
typescript
// src/utils/StorageManager.ts
import { openDB, IDBPDatabase } from 'idb';
import { Platform } from '@/utils/platform';
export class StorageManager {
private db: IDBPDatabase | null = null;
private platform: Platform;
constructor() {
this.platform = new Platform();
this.initStorage();
}
private async initStorage() {
if (this.platform.isHarmony()) {
// 使用HMS Core的存储API
const storage = uni.requireNativePlugin('storage');
this.db = await storage.openDatabase({
name: 'pwa-store',
version: 1
});
} else {
// 使用IndexedDB
this.db = await openDB('pwa-store', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('offline-data')) {
db.createObjectStore('offline-data', { keyPath: 'id' });
}
},
});
}
}
async saveData(key: string, data: any) {
if (!this.db) return;
if (this.platform.isHarmony()) {
await this.db.put({
table: 'offline-data',
data: { id: key, value: data }
});
} else {
const tx = this.db.transaction('offline-data', 'readwrite');
await tx.store.put({ id: key, value: data });
}
}
async getData(key: string) {
if (!this.db) return null;
if (this.platform.isHarmony()) {
const result = await this.db.get({
table: 'offline-data',
key
});
return result?.value;
} else {
const tx = this.db.transaction('offline-data', 'readonly');
const result = await tx.store.get(key);
return result?.value;
}
}
}
性能优化实践
1. 资源预加载
我们实现了一个智能预加载器来提升应用性能:
typescript
// src/utils/Preloader.ts
export class Preloader {
private static readonly PRELOAD_ROUTES = [
'/home',
'/profile',
'/settings'
];
static init() {
// 注册预加载观察器
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const link = entry.target as HTMLLinkElement;
if (!link.loaded) {
link.loaded = true;
import(link.dataset.module!);
}
}
});
},
{ threshold: 0.1 }
);
// 添加预加载链接
this.PRELOAD_ROUTES.forEach(route => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = route;
link.dataset.module = route;
document.head.appendChild(link);
observer.observe(link);
});
}
}
}
2. 性能监控
我们开发了一个性能监控模块:
typescript
// src/utils/PerformanceMonitor.ts
export class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map();
trackMetric(name: string, value: number) {
if (!this.metrics.has(name)) {
this.metrics.set(name, []);
}
this.metrics.get(name)!.push(value);
// 上报到性能监控平台
if (this.shouldReport(name)) {
this.reportMetrics(name);
}
}
private shouldReport(name: string): boolean {
const values = this.metrics.get(name)!;
return values.length >= 10;
}
private reportMetrics(name: string) {
const values = this.metrics.get(name)!;
const average = values.reduce((a, b) => a + b) / values.length;
// 上报逻辑
if (uni.getSystemInfoSync().platform === 'harmony') {
// 使用HMS Analytics上报
const analytics = uni.requireNativePlugin('analytics');
analytics.trackEvent({
name: `performance_${name}`,
value: average
});
} else {
// 使用通用统计SDK上报
console.log(`Performance metric ${name}: ${average}`);
}
// 清空已上报的数据
this.metrics.set(name, []);
}
}
实战案例:离线优先的新闻应用
以下是一个实际的新闻列表组件示例:
vue
<!-- components/NewsList.vue -->
<template>
<view class="news-list">
<view v-if="!online" class="offline-notice">
当前处于离线模式
</view>
<view
v-for="article in articles"
:key="article.id"
class="news-item"
@click="handleArticleClick(article)"
>
<image
:src="article.image"
mode="aspectFill"
class="news-image"
/>
<view class="news-content">
<text class="news-title">{{ article.title }}</text>
<text class="news-summary">{{ article.summary }}</text>
</view>
</view>
</view>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
import { StorageManager } from '@/utils/StorageManager';
import { PerformanceMonitor } from '@/utils/PerformanceMonitor';
export default defineComponent({
name: 'NewsList',
setup() {
const articles = ref([]);
const online = ref(navigator.onLine);
const storage = new StorageManager();
const performance = new PerformanceMonitor();
const loadArticles = async () => {
const startTime = performance.now();
try {
if (online.value) {
// 在线模式:从API获取数据
const response = await fetch('/api/articles');
articles.value = await response.json();
// 缓存数据
await storage.saveData('articles', articles.value);
} else {
// 离线模式:从缓存获取数据
articles.value = await storage.getData('articles') || [];
}
} catch (error) {
console.error('加载文章失败:', error);
// 降级处理:尝试从缓存加载
articles.value = await storage.getData('articles') || [];
}
// 记录性能指标
performance.trackMetric(
'articles_load_time',
performance.now() - startTime
);
};
onMounted(() => {
loadArticles();
// 监听网络状态变化
window.addEventListener('online', () => {
online.value = true;
loadArticles();
});
window.addEventListener('offline', () => {
online.value = false;
});
});
return {
articles,
online
};
}
});
</script>
<style>
.news-list {
padding: 16px;
}
.offline-notice {
background: #fef6e7;
padding: 8px;
text-align: center;
margin-bottom: 16px;
border-radius: 4px;
}
.news-item {
display: flex;
margin-bottom: 16px;
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.news-image {
width: 120px;
height: 120px;
object-fit: cover;
}
.news-content {
flex: 1;
padding: 12px;
}
.news-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
.news-summary {
font-size: 14px;
color: #666;
line-height: 1.4;
}
</style>
最佳实践总结
- 离线优先策略
- 优先使用缓存数据
- 实现优雅的降级处理
- 提供清晰的离线状态提示
- 性能优化要点
- 使用Service Worker缓存关键资源
- 实现智能预加载
- 监控关键性能指标
- 鸿蒙系统适配
- 使用HMS Core相关API
- 适配鸿蒙特有的存储机制
- 优化系统通知和推送
- 开发建议
- 采用TypeScript确保代码质量
- 实现统一的错误处理
- 保持代码模块化和可测试性
未来规划
随着PWA技术和鸿蒙生态的发展,我们计划在以下方面持续优化:
- 技术升级
- 支持新的PWA特性
- 深度整合HMS Core能力
- 优化离线体验
- 性能提升
- 引入更智能的预加载策略
- 优化首屏加载时间
- 提升动画流畅度
总结
通过在UniApp中开发PWA应用,我们不仅提供了优秀的离线体验,还实现了跨平台的统一部署。特别是在鸿蒙系统上,通过深度整合HMS Core,我们确保了应用能充分利用平台特性,为用户提供流畅的使用体验。
希望本文的实践经验能为大家在UniApp PWA开发中提供有价值的参考。记住,好的应用不仅要关注功能实现,更要注重用户体验的持续优化。在未来的开发中,我们也会持续关注PWA技术的发展,不断改进我们的实践方案。