Lädt...

🔧 Building Offline-First Web Apps with Zero Dependencies: A SRVRA-SYNC Tutorial


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

In today's web development landscape, offline capability isn't just a nice-to-have feature—it's becoming essential. Users expect applications to work seamlessly regardless of network conditions.
However, implementing robust offline functionality typically requires complex libraries and frameworks with numerous dependencies, complicating your build process and increasing your bundle size.

Enter SRVRA-SYNC: a pure JavaScript state management and synchronization library that requires zero dependencies and works without Node.js. This tutorial will guide you through building an offline-first web application that synchronizes data intelligently when connectivity returns.

Why Offline-First Matters
Before diving in, let's clarify why offline-first architecture is crucial:

Improved user experience in unreliable network conditions
Faster perceived performance with instant local data access
Resilience against network interruptions
Better battery life by reducing constant connection attempts

Getting Started with SRVRA-SYNC

Let's start by adding SRVRA-SYNC to our project. Since it has zero dependencies, you can include it directly via a script tag:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Offline-First Todo App</title>
    <!-- No need for npm install or complex build processes -->
    <script src="https://cdn.example.com/srvra-sync.min.js"></script>
</head>
<body>
    <div id="app">
        <h1>My Todos</h1>
        <div id="connection-status"></div>
        <form id="todo-form">
            <input type="text" id="todo-input" placeholder="Add a new todo">
            <button type="submit">Add</button>
        </form>
        <ul id="todo-list"></ul>
    </div>

    <script src="app.js"></script>
</body>
</html>

Now, let's create our application logic in app.js:

// Initialize the core components

const eventBus = new SrvraEventBus();
const stateManager = new SrvraStateManager();
const dataSync = new SrvraDataSync({
    // Adjust sync interval based on your app's needs
    syncInterval: 5000,
    // Optimize for offline usage
    retryAttempts: 5,
    enableDeltaUpdates: true
});

// Set up our initial state

stateManager.setState('todos', []);
stateManager.setState('connectionStatus', 'online');

// UI Elements

const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const connectionStatus = document.getElementById('connection-status');

// Track network status

window.addEventListener('online', () => {
    stateManager.setState('connectionStatus', 'online');
    connectionStatus.textContent = 'Online - Syncing...';
    // Trigger sync when connection returns
    dataSync.sync();
});

window.addEventListener('offline', () => {
    stateManager.setState('connectionStatus', 'offline');
    connectionStatus.textContent = 'Offline - Changes saved locally';
});

// Initialize the connection display

connectionStatus.textContent = navigator.onLine ? 'Online' : 'Offline - Changes saved locally';

// Handle adding new todos

todoForm.addEventListener('submit', (e) => {
    e.preventDefault();

    const todoText = todoInput.value.trim();
    if (!todoText) return;

    const todos = stateManager.getState('todos') || [];
    const newTodo = {
        id: Date.now().toString(),
        text: todoText,
        completed: false,
        createdAt: Date.now(),
        synced: false
    };

// Update local state immediately
    stateManager.setState('todos', [...todos, newTodo]);

// Publish event for sync handling
eventBus.publish('data-change', {
        key: 'todos',
        value: [...todos, newTodo],
        timestamp: Date.now()
    });

    todoInput.value = '';
});

// Subscribe to state changes to update the UI

stateManager.subscribe('todos', renderTodos);

// Render the todo list

function renderTodos(todos) {
    todoList.innerHTML = '';

    if (!todos || !todos.length) {
        todoList.innerHTML = '<li class="empty">No todos yet. Add one above!</li>';
        return;
    }

    todos.forEach(todo => {
        const li = document.createElement('li');
        li.dataset.id = todo.id;
        li.className = todo.completed ? 'completed' : '';

        // Add sync status indicator
        const syncStatus = todo.synced ? '?' : '?';

        li.innerHTML = `
            <span class="todo-text">${todo.text}</span>
            <span class="sync-status" title="${todo.synced ? 'Synced' : 'Pending sync'}">${syncStatus}</span>
            <button class="toggle-btn">${todo.completed ? 'Undo' : 'Complete'}</button>
            <button class="delete-btn">Delete</button>
        `;

        // Add event listeners for toggle and delete
        li.querySelector('.toggle-btn').addEventListener('click', () => toggleTodo(todo.id));
        li.querySelector('.delete-btn').addEventListener('click', () => deleteTodo(todo.id));

        todoList.appendChild(li);
    });
}

// Toggle todo completion status

function toggleTodo(id) {
    const todos = stateManager.getState('todos');
    const updatedTodos = todos.map(todo => {
        if (todo.id === id) {
            return { ...todo, completed: !todo.completed, synced: false };
        }
        return todo;
    });

    stateManager.setState('todos', updatedTodos);

    // Publish event for sync
    eventBus.publish('data-change', {
        key: 'todos',
        value: updatedTodos,
        timestamp: Date.now()
    });
}

// Delete a todo

function deleteTodo(id) {
    const todos = stateManager.getState('todos');
    const updatedTodos = todos.filter(todo => todo.id !== id);

    stateManager.setState('todos', updatedTodos);

    // Publish event for sync
    eventBus.publish('data-change', {
        key: 'todos',
        value: updatedTodos,
        timestamp: Date.now()
    });
}

// Handle synchronization

eventBus.subscribe('sync-complete', (data) => {
    const todos = stateManager.getState('todos');

    // Mark successfully synced todos
    const updatedTodos = todos.map(todo => {
        if (!todo.synced) {
            return { ...todo, synced: true };
        }
        return todo;
    });

    stateManager.setState('todos', updatedTodos);
    connectionStatus.textContent = 'Online - Synced';

    // After a moment, simplify the display
    setTimeout(() => {
        if (stateManager.getState('connectionStatus') === 'online') {
            connectionStatus.textContent = 'Online';
        }
    }, 2000);
});

// Listen for sync errors

eventBus.subscribe('sync-error', (data) => {
    console.error('Sync error:', data.error);
    connectionStatus.textContent = 'Sync failed - Will retry';
});

// Initialize with any stored data

function initializeFromStorage() {
    const storedTodos = localStorage.getItem('todos');
    if (storedTodos) {
        try {
            const todos = JSON.parse(storedTodos);
            stateManager.setState('todos', todos);
        } catch (e) {
            console.error('Error loading stored todos:', e);
        }
    }
}

// Persist state changes to localStorage

stateManager.subscribe('todos', (todos) => {
    localStorage.setItem('todos', JSON.stringify(todos));
});

// Initialize

initializeFromStorage();

Setting Up the Server Sync
For a complete offline-first application, we need to set up server synchronization. Here's how to handle the server-side integration:

// This would be added to your app.js

function setupServerSync() {
    // Configure sync endpoint
    const syncEndpoint = 'https://api.example.com/sync';

    // Override the default sendToServer method in SrvraDataSync
    dataSync.sendToServer = async function(batch) {
        try {
            // Only attempt to send if online
            if (!navigator.onLine) {
                return { success: [], conflicts: [], errors: [] };
            }

            const response = await fetch(syncEndpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(batch)
            });

            if (!response.ok) {
                throw new Error(`Server responded with ${response.status}`);
            }

            return await response.json();
        } catch (error) {
            console.error('Sync error:', error);
            // Return empty results but don't fail - we'll retry later
            return { success: [], conflicts: [], errors: [error] };
        }
    };

// Handle conflict resolution
eventBus.subscribe('conflict', (conflict) => {
        // For this example, we'll use a "client wins" strategy for conflicts
        return dataSync.handleConflict({
            ...conflict,
            forcedStrategy: 'client-wins'
        });
    });
}

setupServerSync();

Advanced Offline-First Patterns with SRVRA-SYNC
Now let's implement some advanced offline-first patterns:

1. Optimistic UI Updates
// Add this to app.js to enable optimistic UI updates

function optimisticallyUpdateTodo(todoId, changes) {
    const todos = stateManager.getState('todos');

    // Immediately update the UI
    const optimisticTodos = todos.map(todo => {
        if (todo.id === todoId) {
            return { ...todo, ...changes, synced: false };
        }
        return todo;
    });

    // Update state immediately
    stateManager.setState('todos', optimisticTodos);

    // Then attempt to sync
    eventBus.publish('data-change', {
        key: 'todos',
        value: optimisticTodos,
        timestamp: Date.now()
    });
}

// Example usage for a priority change

function updateTodoPriority(todoId, priority) {
    optimisticallyUpdateTodo(todoId, { priority });
}

2. Conflict Resolution Strategies
// Advanced conflict resolution based on data types

dataSync.conflictResolver.registerCustomStrategy('smart-todo-merge', (conflict) => {
    const serverTodo = conflict.serverValue;
    const clientTodo = conflict.clientValue;

    // If completed status differs, prefer the completed version
    const completed = serverTodo.completed || clientTodo.completed;

    // For text content, use the most recently edited version
    const text = serverTodo.updatedAt > clientTodo.updatedAt 
        ? serverTodo.text 
        : clientTodo.text;

    return {
        value: {
            ...clientTodo,
            text,
            completed,
            synced: true
        },
        source: 'smart-merge',
        metadata: {
            mergedAt: Date.now(),
            conflictResolution: 'smart-todo-merge'
        }
    };
});

// Configure todos to use our custom strategy

dataSync.conflictResolver.registerMergeRule('todos', 
    dataSync.conflictResolver.customStrategies.get('smart-todo-merge'));

3. Storage Quota Management
// Add this to manage localStorage quotas

async function checkStorageQuota() {
    if ('storage' in navigator && 'estimate' in navigator.storage) {
        const estimate = await navigator.storage.estimate();
        const percentUsed = (estimate.usage / estimate.quota) * 100;

        if (percentUsed > 80) {
            // Alert user or clean up old data
            const todos = stateManager.getState('todos');

            // Clean up completed and synced todos older than 30 days
            const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
            const cleanedTodos = todos.filter(todo => {
                return !(todo.completed && todo.synced && todo.createdAt < thirtyDaysAgo);
            });

            if (cleanedTodos.length < todos.length) {
                stateManager.setState('todos', cleanedTodos);
                console.log(`Cleaned up ${todos.length - cleanedTodos.length} old todos to save space.`);
            }
        }
    }
}

// Check quota weekly

setInterval(checkStorageQuota, 7 * 24 * 60 * 60 * 1000);

// Also check on startup

checkStorageQuota();

Handling Network Reconnection Intelligently
An important aspect of offline-first applications is managing reconnection smoothly:

// Add sophisticated reconnection handling

let reconnectionAttempts = 0;
const MAX_BACKOFF = 60000; // Maximum 1 minute between retries

function handleReconnection() {
    // Reset if we're online
    if (navigator.onLine) {
        reconnectionAttempts = 0;
        dataSync.sync(); // Sync immediately when we reconnect
        return;
    }

    // Exponential backoff for reconnection attempts
    reconnectionAttempts++;
    const backoff = Math.min(
        1000 * Math.pow(2, reconnectionAttempts), 
        MAX_BACKOFF
    );

    connectionStatus.textContent = `Offline - Retrying in ${backoff/1000}s`;

    setTimeout(() => {
        // Check if we're back online
        if (navigator.onLine) {
            stateManager.setState('connectionStatus', 'online');
            connectionStatus.textContent = 'Online - Syncing...';
            dataSync.sync();
        } else {
            handleReconnection(); // Try again with increased backoff
        }
    }, backoff);
}

// Initialize network handling

if (!navigator.onLine) {
    handleReconnection();
}

// Listen for network changes
window.addEventListener('online', () => {
    stateManager.setState('connectionStatus', 'online');
    connectionStatus.textContent = 'Online - Syncing...';
    dataSync.sync();
});

GitHub Repository:
Srvra-Sync https://github.com/SINHASantos/srvra-sync

...

🔧 Your dependencies have dependencies: new features to assess risk


📈 29.1 Punkte
🔧 Programmierung

🎥 How To Use META AI (Complete Tutorial) Beginner Tutorial (LLAMA 3 Tutorial)


📈 23.76 Punkte
🎥 Video | Youtube

🔧 Building a web server with no dependencies in Java


📈 22.96 Punkte
🔧 Programmierung

🔧 Mastering JavaScript Service Workers: A Complete Guide to Building Offline-Ready Web Apps


📈 20.1 Punkte
🔧 Programmierung

🔧 Exploring Progressive Web Apps (PWAs): Building Offline-Ready Experiences


📈 20.1 Punkte
🔧 Programmierung

🔧 Learn Node.js by building a backend framework with 0 dependencies


📈 19.82 Punkte
🔧 Programmierung

🔧 Building NPM packages for CommonJS with ESM dependencies


📈 19.82 Punkte
🔧 Programmierung

🔧 🚀 SPOT.BOX: Create Stunning Interactive Buttons with Zero Dependencies


📈 19.68 Punkte
🔧 Programmierung

🔧 Open-Source TailwindCSS React Color Picker - Zero Dependencies! Perfect for Next.js Projects!


📈 19.68 Punkte
🔧 Programmierung

🔧 “Zero dependencies” is overrated, build it like Theseus


📈 19.68 Punkte
🔧 Programmierung

🔧 How to create navigation menu with HTML CSS step by step | web design tutorial | HTML CSS tutorial


📈 18.98 Punkte
🔧 Programmierung

🔧 Building and deploying web apps with Static Web Apps | The DevOps Lab


📈 18.59 Punkte
🔧 Programmierung

🎥 Building Progressive Web Apps to extend existing web apps in Windows and Microsoft - BRK3078


📈 18.59 Punkte
🎥 Video | Youtube

📰 You Can Now Package Your Apps as Snaps without Bundling Their Dependencies


📈 18.07 Punkte
📰 IT Security

📰 You Can Now Package Your Apps as Snaps without Bundling Their Dependencies


📈 18.07 Punkte
📰 IT Security

🔧 Optimize Core Web Vitals - FCP and LCP: Remove not need dependencies


📈 17.69 Punkte
🔧 Programmierung

🐧 Web cam white/black board in single executable with all dependencies (experimental)


📈 17.69 Punkte
🐧 Linux Tipps

🎥 Power Apps | New Capabilities Building Zero-To-Low-Code Apps


📈 17.43 Punkte
🎥 Video | Youtube

🔧 Building Pure Python Web Apps With Reflex Part 1 | Building the Frontend


📈 17.2 Punkte
🔧 Programmierung

🔧 From Zero to Hero: A Beginner's Guide to Building Web Apps with the MERN Stack


📈 17.06 Punkte
🔧 Programmierung

🔧 The Ultimate Guide to Building Offline Angular Apps with Service Workers


📈 16.95 Punkte
🔧 Programmierung

🔧 The Ultimate Guide to Building Offline Angular Apps with Service Workers


📈 16.95 Punkte
🔧 Programmierung

🔧 Tutorial 11: Introduction to SwiftUI: Building Modern Apps


📈 16.71 Punkte
🔧 Programmierung

🔧 Building an Offline-Enabled To-Do List Web App 🚀


📈 16.58 Punkte
🔧 Programmierung

🎥 Building web applications in Java with Spring Boot 3 – Tutorial


📈 16.34 Punkte
🎥 Video | Youtube

🪟 Pinter Offline [FIX] – Why Is My Printer Offline in Windows 10?


📈 16.33 Punkte
🪟 Windows Tipps

📰 Pinter Offline [FIX] – Why Is My Printer Offline in Windows 10?


📈 16.33 Punkte
🖥️ Betriebssysteme

📰 Offline-Karten: Neue Version von Google Maps ermöglicht Offline-Download ...


📈 16.33 Punkte
📰 IT Nachrichten

matomo