MAUI PicoServer PWA离线系统,如何构建本地Web App?

摘要:MAUI 嵌入式 Web 架构实战(九) PicoServer + PWA 离线系统:构建真正的本地 Web App 源码地址: https:github.comdensen2014MauiPicoAdmin 在前
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 架构