DEV Community

ben
ben

Posted on

Practical Combat of MAUI Embedded Web Architecture (9) PicoServer + PWA Offline System: Building a True Local Web App

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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");
        });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Suitable for:

  • JS
  • CSS
  • Images

Network First

Prioritize accessing the network:

network → cache
Enter fullscreen mode Exit fullscreen mode

Suitable for:

  • HTML
  • API

Stale While Revalidate

Return cached data first, then update in the background:

cache → network update
Enter fullscreen mode Exit fullscreen mode

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))
            );
        })
    );
});
Enter fullscreen mode Exit fullscreen mode

V. Request Interception

Service Worker can intercept all requests.

fetch → Service Worker
      → Cache
      → Network
Enter fullscreen mode Exit fullscreen mode

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));
});
Enter fullscreen mode Exit fullscreen mode

VI. Product API Caching Example

Product API endpoint:

GET /api/products
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

Workflow:

  1. Return cached product data immediately
  2. Fetch the latest product data in the background
  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

This refreshes the product list every 5 minutes.

The workflow:

Cache Products
↓
Background Update
↓
Available Offline
Enter fullscreen mode Exit fullscreen mode

IX. Offline Data Architecture

Browser database: IndexedDB

Typical structure:

IndexedDB
 ├─ products
 ├─ orders
 ├─ syncQueue
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

X. Advantages of PicoServer + PWA

Regular PWA Architecture:

Browser
↓
Internet
↓
Remote Server
Enter fullscreen mode Exit fullscreen mode

PicoServer Architecture:

Browser
↓
PicoServer (Local)
↓
MAUI
↓
SQLite
Enter fullscreen mode Exit fullscreen mode

Advantages:

  1. Local API
    API endpoint: http://127.0.0.1/api
    Latency: < 1ms

  2. Fully Offline
    All system components run locally:

    • UI
    • API
    • DB
  3. 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Next Article Preview

Next article summary:
Complete App Web Shell Architecture

Top comments (0)