MAUI 嵌入式 Web 架构实战(九)PicoServer + PWA 离线系统:构建真正的本地 Web App
MAUI 嵌入式 Web 架构实战(九)
PicoServer + PWA 离线系统:构建真正的本地 Web App
源码地址:
https://github.com/densen2014/MauiPicoAdmin
在前面的系列文章中,我们已经逐步构建了一个完整的 PicoServer 本地 Web Admin 系统:
- MAUI 内嵌 Web Server
- REST API 架构
- Web Admin 管理后台
- WebSocket 实时通信
- Controller 自动发现与插件化
现在系统已经具备:
Web UI
↓
REST API
↓
MAUI 本地服务
↓
SQLite / 设备
但仍然存在一个问题:
如果浏览器离线,系统还能运行吗?
答案是:可以。
通过 PWA(Progressive Web App)+ Service Worker,我们可以让 Web Admin 具备:
- 离线运行
- 本地缓存
- 后台同步
- 自动更新
最终实现:
PicoServer + PWA = 本地 Web 应用平台
一、系统整体架构
完整架构如下:
Browser / WebView
│
Service Worker
│
┌───────────┴───────────┐
│ │
Cache Storage IndexedDB
│ │
│ │
└───────────┬───────────┘
│
PicoServer
│
REST API
│
MAUI
│
SQLite / Device
说明:
Cache Storage
用于缓存:
HTML
JS
CSS
Images
IndexedDB
用于缓存:
商品列表
订单
离线单据
同步队列
Service Worker
负责:
请求拦截
离线缓存
后台更新
二、PWA 基础组件
PWA 主要由三个部分组成。
1 Manifest
manifest.json:
{
"name": "Pico Admin",
"short_name": "PicoAdmin",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1976d2",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
作用:
- 允许网页 安装为 App
- 定义图标
- 定义启动方式
2 注册 Service Worker
在前端入口:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
console.log("SW registered");
});
}
三、缓存策略设计
在离线系统中,不同资源需要不同策略。
| 资源类型 | 策略 |
|---|---|
| HTML | Network First |
| JS / CSS | Cache First |
| 图片 | Cache First |
| 商品 API | Stale While Revalidate |
说明:
Cache First
优先读取缓存:
cache → network
适用于:
JS
CSS
图片
Network First
优先访问网络:
network → cache
适用于:
HTML
API
Stale While Revalidate
先返回缓存,再后台更新:
cache → network update
适用于:
商品列表
配置数据
四、完整 Service Worker 实现
sw.js:
const STATIC_CACHE = "pico-static-v1";
const API_CACHE = "pico-api-v1";
const STATIC_FILES = [
"/",
"/index.html",
"/app.js",
"/style.css"
];
self.addEventListener("install", event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_FILES))
);
});
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(k => k !== STATIC_CACHE && k !== API_CACHE)
.map(k => caches.delete(k))
);
})
);
});
五、拦截请求
Service Worker 可以拦截所有请求。
fetch → Service Worker
→ Cache
→ Network
实现:
self.addEventListener("fetch", event => {
const url = new URL(event.request.url);
// 商品 API
if (url.pathname.startsWith("/api/products")) {
event.respondWith(cacheProductApi(event.request));
return;
}
// 静态资源
event.respondWith(cacheFirst(event.request));
});
六、商品 API 缓存示例
商品接口:
GET /api/products
缓存策略:
Stale While Revalidate
代码:
async function cacheProductApi(request) {
const cache = await caches.open(API_CACHE);
const cached = await cache.match(request);
const networkFetch = fetch(request)
.then(response => {
cache.put(request, response.clone());
return response;
})
.catch(() => cached);
return cached || networkFetch;
}
说明:
流程:
1 返回缓存商品
2 后台请求最新商品
3 更新缓存
用户不会感知延迟。
七、静态资源缓存
async function cacheFirst(request) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
适用于:
js
css
images
八、定时刷新商品列表
在前端页面可以定时更新商品缓存。
例如:
setInterval(async () => {
try {
await fetch("/api/products");
console.log("products refreshed");
} catch (e) {
console.log("offline");
}
}, 300000);
每 5分钟刷新一次商品列表。
这样:
缓存商品
↓
后台更新
↓
离线可用
九、离线数据架构
浏览器数据库:
IndexedDB
典型结构:
IndexedDB
├─ products
├─ orders
├─ syncQueue
作用:
商品缓存
products
离线订单
orders
同步队列
syncQueue
同步流程:
离线下单
↓
IndexedDB
↓
网络恢复
↓
POST /api/orders
↓
同步完成
十、PicoServer + PWA 的优势
普通 PWA:
Browser
↓
Internet
↓
Remote Server
PicoServer 架构:
Browser
↓
PicoServer (本地)
↓
MAUI
↓
SQLite
优势:
1 本地 API
http://127.0.0.1/api
延迟:
< 1ms
2 完全离线
系统组件:
UI
API
DB
全部本地运行。
3 跨平台
MAUI 支持:
Windows
Android
iOS
Mac
十一、最终系统结构
完整系统:
PWA Web Admin
│
Service Worker
│
┌────────┴────────┐
│ │
Cache Storage IndexedDB
│ │
└────────┬────────┘
│
PicoServer
│
API
│
MAUI
│
SQLite
一句话总结:
PicoServer + PWA = 本地 Web 应用平台
十二、其他代码
因篇幅有限, 部分代码并未完整贴出, 请参考工程同步源码: https://github.com/densen2014/MauiPicoAdmin
MauiPicoAdmin\Resources\Raw\wwwroot\search.html 部分代码
<input id="productIdInput" type="text" class="form-control form-control-sm" style="width:120px;" placeholder="输入商品ID" oninput="loadProducts(this.value)" autocomplete="off">
<div class="card-body">
<table id="table">
<tbody></tbody>
</table>
<script>
async function loadProducts(id) {
id = (id || '').trim();
if (!id) {
document.querySelector("#table tbody").innerHTML = "";
return;
}
document.getElementById("spinner").style.display = "block";
try {
let res = await fetch(`/api/product/detail?id=${encodeURIComponent(id)}`);
let json = await res.json();
let p = json.data || [];
let tbody = document.querySelector("#table tbody");
tbody.innerHTML = "";
let row = `
<tr>
<td>${p.id}</td>
<td>${p.name}</td>
<td>${p.price}</td>
</tr>
`;
tbody.innerHTML += row;
} catch (e) {
document.querySelector("#table tbody").innerHTML = `<tr><td colspan='3' class='text-danger'>加载失败</td></tr>`;
}
document.getElementById("spinner").style.display = "none";
}
</script>
下一篇预告
下一篇总结:
完整 App Web Shell 架构
关联项目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社区 QQ群:645660665
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。
转载声明
本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://www.cnblogs.com/densen2014/p/19722970
AlexChow
今日头条 | 博客园 | 知乎 | Gitee | GitHub


浙公网安备 33010602011771号