PicoServer
Source Code URL:
https://github.com/densen2014/MauiPicoAdmin
In the previous articles of this series, we have gradually built a complete PicoServer local Web Admin system with the following components:
- MAUI Embedded Web Server
- REST API Architecture
- Web Admin Management Backend
- WebSocket Real-Time Communication
- Automatic Controller Discovery and Plug-in Support
Currently, the system has the following workflow:
Web UI
↓
REST API
↓
MAUI Local Services
↓
SQLite / Device
However, one question remains:
Can the system still run when the browser is offline?
The answer is: Yes.
With PWA (Progressive Web App) + Service Worker, we can enable the Web Admin to have the following capabilities:
- Offline Operation
- Local Caching
- Background Sync
- Automatic Updates
Ultimately achieving:
PicoServer + PWA = Local Web Application Platform
I. Overall System Architecture
The complete architecture is as follows:
Browser / WebView
│
Service Worker
│
┌───────────┴───────────┐
│ │
Cache Storage IndexedDB
│ │
│ │
└───────────┬───────────┘
│
PicoServer
│
REST API
│
MAUI
│
SQLite / Device
Explanation:
Cache Storage
Used for caching:
- HTML
- JS
- CSS
- Images
IndexedDB
Used for caching:
- Product Lists
- Orders
- Offline Documents
- Sync Queues
Service Worker
Responsible for:
- Request Interception
- Offline Caching
- Background Updates
II. Basic PWA Components
PWA mainly consists of three parts.
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"
}
]
}
Functions:
- Allows web pages to be installed as apps
- Defines app icons
- Defines launch modes
2. Register Service Worker
Add the following code in the frontend entry file:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
console.log("SW registered");
});
}
III. Cache Strategy Design
In offline systems, different resources require different caching strategies.
| Resource Type | Strategy |
|---|---|
| HTML | Network First |
| JS / CSS | Cache First |
| Images | Cache First |
| Product API | Stale While Revalidate |
Explanation:
Cache First
Prioritize reading from cache:
cache → network
Suitable for:
- JS
- CSS
- Images
Network First
Prioritize accessing the network:
network → cache
Suitable for:
- HTML
- API
Stale While Revalidate
Return cached data first, then update in the background:
cache → network update
Suitable for:
- Product lists
- Configuration data
IV. Complete Service Worker Implementation
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))
);
})
);
});
V. Request Interception
Service Worker can intercept all requests.
fetch → Service Worker
→ Cache
→ Network
Implementation code:
self.addEventListener("fetch", event => {
const url = new URL(event.request.url);
// Product API
if (url.pathname.startsWith("/api/products")) {
event.respondWith(cacheProductApi(event.request));
return;
}
// Static resources
event.respondWith(cacheFirst(event.request));
});
VI. Product API Caching Example
Product API endpoint:
GET /api/products
Caching strategy: Stale While Revalidate
Code implementation:
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;
}
Explanation:
Workflow:
- Return cached product data immediately
- Fetch the latest product data in the background
- Update the cache with new data
Users won't perceive any delay.
VII. Static Resource Caching
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;
}
Suitable for:
- js
- css
- images
VIII. Periodic Product List Refresh
You can periodically update the product cache in the frontend page.
For example:
setInterval(async () => {
try {
await fetch("/api/products");
console.log("products refreshed");
} catch (e) {
console.log("offline");
}
}, 300000);
This refreshes the product list every 5 minutes.
The workflow:
Cache Products
↓
Background Update
↓
Available Offline
IX. Offline Data Architecture
Browser database: IndexedDB
Typical structure:
IndexedDB
├─ products
├─ orders
├─ syncQueue
Functions:
- products: Product caching
- orders: Offline order storage
- syncQueue: Sync queue for pending operations
Sync workflow:
Place Order Offline
↓
Store in IndexedDB
↓
Network Restored
↓
POST /api/orders
↓
Sync Completed
X. Advantages of PicoServer + PWA
Regular PWA Architecture:
Browser
↓
Internet
↓
Remote Server
PicoServer Architecture:
Browser
↓
PicoServer (Local)
↓
MAUI
↓
SQLite
Advantages:
Local API
API endpoint:http://127.0.0.1/api
Latency: < 1ms-
Fully Offline
All system components run locally:- UI
- API
- DB
-
Cross-Platform
MAUI supports multiple platforms:- Windows
- Android
- iOS
- Mac
XI. Final System Structure
The complete system architecture:
PWA Web Admin
│
Service Worker
│
┌────────┴────────┐
│ │
Cache Storage IndexedDB
│ │
└────────┬────────┘
│
PicoServer
│
API
│
MAUI
│
SQLite
One-sentence summary:
PicoServer + PWA = Local Web Application Platform
XII. Additional Code
Due to space limitations, some code is not fully displayed. Please refer to the synchronized source code in the project: https://github.com/densen2014/MauiPicoAdmin
Partial code from MauiPicoAdmin\Resources\Raw\wwwroot\search.html:
<input id="productIdInput" type="text" class="form-control form-control-sm" style="width:120px;" placeholder="Enter Product 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'>Failed to load</td></tr>`;
}
document.getElementById("spinner").style.display = "none";
}
</script>
Next Article Preview
Next article summary:
Complete App Web Shell Architecture
Top comments (0)