Elysia Tools

在一个清晰的工作台里浏览工具、案例、可视化和主题指南。

热门分类

专题

语言
ZHESPTRUFRDEEN

Elysia Tools

导航

Web 技术

Service Workers 离线Web应用

Service Workers 用于离线功能、后台同步和网络拦截

案例

这个案例集合中的条目

代码javascript简单

基础 Service Worker 设置

注册Service Worker并启用离线缓存

难度

3/10

预计时间

15 min

标签

service worker, offline, cache, pwa

前置条件

JavaScript, Basic web development

javascript
// Basic Service Worker Setup
// service-worker.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/icon.png'
];

// Install event - cache files
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - return response
        if (response) {
          return response;
        }

        // Clone the request
        const fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(response => {
          // Check if valid response
          if(!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }

          // Clone the response
          const responseToCache = response.clone();

          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });

          return response;
        });
      })
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            console.log('Deleting old cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// main.js - Register service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);

        // Check for updates
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;

          newWorker.addEventListener('statechange', () => {
            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
              // New content is available, show update notification
              if (confirm('New version available! Reload to update?')) {
                window.location.reload();
              }
            }
          });
        });

      })
      .catch(err => {
        console.log('ServiceWorker registration failed: ', err);
      });
  });

  // Listen for controlling service worker
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    console.log('ServiceWorker controller changed');
    window.location.reload();
  });
}
代码javascript中等

后台同步

网络连接恢复时同步数据

难度

6/10

预计时间

25 min

标签

background sync, offline, pwa, indexeddb

前置条件

JavaScript, Service Workers, IndexedDB

javascript
// Background Sync Service Worker
// service-worker.js
self.addEventListener('sync', event => {
  if (event.tag === 'background-sync') {
    event.waitUntil(syncData());
  }
});

async function syncData() {
  try {
    // Get all pending sync requests from IndexedDB
    const pendingRequests = await getPendingRequests();

    for (const request of pendingRequests) {
      try {
        const response = await fetch(request.url, {
          method: request.method,
          headers: request.headers,
          body: request.body
        });

        if (response.ok) {
          // Remove successful request from pending
          await removePendingRequest(request.id);
          console.log('Sync successful for request:', request.id);
        } else {
          console.error('Sync failed for request:', request.id);
        }
      } catch (error) {
        console.error('Network error during sync:', error);
        throw error; // This will retry the sync
      }
    }

    // Show notification if sync completed
    if (await getPendingRequestsCount() === 0) {
      self.registration.showNotification('Sync Complete', {
        body: 'All your data has been synchronized',
        icon: '/sync-icon.png'
      });
    }
  } catch (error) {
    console.error('Background sync failed:', error);
  }
}

// IndexedDB helpers for pending requests
async function getPendingRequests() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('sync-db', 1);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      const transaction = db.transaction(['requests'], 'readonly');
      const store = transaction.objectStore('requests');
      const getAll = store.getAll();

      getAll.onsuccess = () => resolve(getAll.result);
      getAll.onerror = () => reject(getAll.error);
    };

    request.onupgradeneeded = () => {
      const db = request.result;
      db.createObjectStore('requests', { keyPath: 'id' });
    };
  });
}

async function removePendingRequest(id) {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('sync-db', 1);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => {
      const db = request.result;
      const transaction = db.transaction(['requests'], 'readwrite');
      const store = transaction.objectStore('requests');
      const deleteRequest = store.delete(id);

      deleteRequest.onsuccess = () => resolve();
      deleteRequest.onerror = () => reject(deleteRequest.error);
    };
  });
}

// main.js - Register background sync
class BackgroundSyncManager {
  constructor() {
    this.registration = null;
  }

  async init() {
    this.registration = await navigator.serviceWorker.ready;
  }

  async registerSync() {
    if ('serviceWorker' in navigator &&
        typeof ServiceWorkerRegistration !== 'undefined' &&
        'sync' in ServiceWorkerRegistration.prototype) {
      try {
        await this.registration.sync.register('background-sync');
        console.log('Background sync registered');
        return true;
      } catch (error) {
        console.error('Background sync registration failed:', error);
        return false;
      }
    } else {
      console.warn('Background sync not supported');
      return false;
    }
  }

  async addPendingRequest(requestData) {
    const request = {
      id: Date.now().toString(),
      ...requestData,
      timestamp: Date.now()
    };

    // Save to IndexedDB
    const db = await this.openDB();
    const transaction = db.transaction(['requests'], 'readwrite');
    const store = transaction.objectStore('requests');
    await store.add(request);

    // Try to register background sync
    const syncRegistered = await this.registerSync();

    if (!syncRegistered) {
      // Fallback: try immediate sync
      this.tryImmediateSync(request);
    }

    return request.id;
  }

  async openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('sync-db', 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = () => {
        const db = request.result;
        db.createObjectStore('requests', { keyPath: 'id' });
      };
    });
  }

  async tryImmediateSync(request) {
    try {
      const response = await fetch(request.url, {
        method: request.method,
        headers: request.headers,
        body: request.body
      });

      if (response.ok) {
        await this.removePendingRequest(request.id);
        console.log('Immediate sync successful');
      } else {
        console.log('Immediate sync failed, keeping in pending queue');
      }
    } catch (error) {
      console.log('Network unavailable, keeping in pending queue');
    }
  }
}

// Usage example
const syncManager = new BackgroundSyncManager();

// Initialize when app loads
window.addEventListener('load', async () => {
  await syncManager.init();
});

// Example: Save data with background sync
async function saveDataOffline(data) {
  const requestData = {
    url: '/api/save',
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  };

  const requestId = await syncManager.addPendingRequest(requestData);

  // Show user that data will be synced
  alert('Your data will be synced when you're back online');
}
代码javascript中等

推送通知

接收和处理Service Worker的推送通知

难度

7/10

预计时间

30 min

标签

push notifications, pwa, service worker, messaging

前置条件

JavaScript, Service Workers, HTTPS setup

javascript
// Push Notifications with Service Worker
// service-worker.js
self.addEventListener('push', event => {
  const options = {
    body: event.data ? event.data.text() : 'New notification',
    icon: '/notification-icon.png',
    badge: '/notification-badge.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: 1
    },
    actions: [
      {
        action: 'explore',
        title: 'Explore this new world',
        icon: '/images/checkmark.png'
      },
      {
        action: 'close',
        title: 'Close notification',
        icon: '/images/xmark.png'
      }
    ]
  };

  event.waitUntil(
    self.registration.showNotification('Push Notification', options)
  );
});

// Handle notification clicks
self.addEventListener('notificationclick', event => {
  event.notification.close();

  if (event.action === 'explore') {
    // Open the app to specific page
    event.waitUntil(
      clients.openWindow('/explore')
    );
  } else if (event.action === 'close') {
    // Just close the notification
    console.log('Notification closed');
  } else {
    // Default action - open main app
    event.waitUntil(
      clients.matchAll().then(clientList => {
        for (const client of clientList) {
          if (client.url === '/' && 'focus' in client) {
            return client.focus();
          }
        }
        if (clients.openWindow) {
          return clients.openWindow('/');
        }
      })
    );
  }
});

// Handle notification close
self.addEventListener('notificationclose', event => {
  console.log('Notification closed:', event.notification);
});

// main.js - Push notification subscription
class PushNotificationManager {
  constructor() {
    this.subscription = null;
    this.publicKey = 'YOUR_VAPID_PUBLIC_KEY';
  }

  async init() {
    // Check for service worker support
    if (!('serviceWorker' in navigator)) {
      console.error('Service Worker not supported');
      return false;
    }

    // Check for push notification support
    if (!('PushManager' in window)) {
      console.error('Push notifications not supported');
      return false;
    }

    // Register service worker
    const registration = await navigator.serviceWorker.register('/push-service-worker.js');
    console.log('Service Worker registered');

    this.registration = registration;
    return true;
  }

  async subscribeToPush() {
    try {
      // Subscribe to push notifications
      this.subscription = await this.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: this.urlBase64ToUint8Array(this.publicKey)
      });

      console.log('User is subscribed:', this.subscription);

      // Send subscription to server
      await this.sendSubscriptionToServer(this.subscription);

      return this.subscription;
    } catch (error) {
      console.error('Failed to subscribe to push notifications:', error);
      return null;
    }
  }

  async unsubscribeFromPush() {
    try {
      const result = await this.subscription.unsubscribe();
      console.log('Unsubscribed:', result);

      // Remove subscription from server
      await this.removeSubscriptionFromServer(this.subscription);

      this.subscription = null;
      return result;
    } catch (error) {
      console.error('Failed to unsubscribe:', error);
      return false;
    }
  }

  async sendSubscriptionToServer(subscription) {
    try {
      const response = await fetch('/api/subscribe', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(subscription)
      });

      if (!response.ok) {
        throw new Error('Failed to send subscription to server');
      }

      return await response.json();
    } catch (error) {
      console.error('Error sending subscription to server:', error);
      throw error;
    }
  }

  async removeSubscriptionFromServer(subscription) {
    try {
      const response = await fetch('/api/unsubscribe', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(subscription)
      });

      if (!response.ok) {
        throw new Error('Failed to remove subscription from server');
      }
    } catch (error) {
      console.error('Error removing subscription from server:', error);
      throw error;
    }
  }

  async requestNotificationPermission() {
    const permission = await Notification.requestPermission();

    if (permission === 'granted') {
      console.log('Notification permission granted');
      return true;
    } else {
      console.log('Notification permission denied');
      return false;
    }
  }

  // Helper function to convert VAPID key
  urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }

    return outputArray;
  }

  async checkSubscriptionStatus() {
    this.subscription = await this.registration.pushManager.getSubscription();

    if (this.subscription) {
      console.log('User is already subscribed');
      return this.subscription;
    } else {
      console.log('User is not subscribed');
      return null;
    }
  }
}

// Usage example
const pushManager = new PushNotificationManager();

// Initialize push notifications
async function initPushNotifications() {
  const initialized = await pushManager.init();
  if (!initialized) return;

  // Request permission
  const permissionGranted = await pushManager.requestNotificationPermission();
  if (!permissionGranted) return;

  // Check if already subscribed
  const existingSubscription = await pushManager.checkSubscriptionStatus();

  if (!existingSubscription) {
    // Subscribe to push notifications
    await pushManager.subscribeToPush();
  }
}

// Subscribe button handler
document.getElementById('subscribe-btn').addEventListener('click', async () => {
  await initPushNotifications();
});

// Unsubscribe button handler
document.getElementById('unsubscribe-btn').addEventListener('click', async () => {
  await pushManager.unsubscribeFromPush();
});

// Test local notification
document.getElementById('test-notification').addEventListener('click', async () => {
  if (Notification.permission === 'granted') {
    new Notification('Test Notification', {
      body: 'This is a test notification!',
      icon: '/icon.png'
    });
  } else {
    console.log('Notification permission not granted');
  }
});
代码javascript中等

高级缓存策略

实施不同的缓存策略以获得最佳性能

难度

8/10

预计时间

35 min

标签

caching, performance, offline, pwa, strategies

前置条件

JavaScript, Service Workers, Performance optimization

javascript
// Advanced Service Worker Caching Strategies
// service-worker.js

// Different cache strategies
class CacheStrategies {
  // Cache First - try cache first, fallback to network
  static cacheFirst(request) {
    return caches.match(request)
      .then(response => {
        return response || fetch(request);
      });
  }

  // Network First - try network first, fallback to cache
  static networkFirst(request) {
    return fetch(request)
      .then(response => {
        // Cache successful responses
        if (response.status === 200) {
          const responseClone = response.clone();
          caches.open('dynamic-cache-v1')
            .then(cache => cache.put(request, responseClone));
        }
        return response;
      })
      .catch(() => {
        // Fallback to cache
        return caches.match(request);
      });
  }

  // Stale While Revalidate - serve from cache, update in background
  static staleWhileRevalidate(request) {
    const cachePromise = caches.match(request);
    const networkPromise = fetch(request)
      .then(response => {
        const responseClone = response.clone();
        caches.open('dynamic-cache-v1')
          .then(cache => cache.put(request, responseClone));
        return response;
      });

    return cachePromise
      .then(response => response || networkPromise)
      .catch(() => networkPromise);
  }

  // Network Only - always fetch from network
  static networkOnly(request) {
    return fetch(request);
  }

  // Cache Only - always serve from cache
  static cacheOnly(request) {
    return caches.match(request);
  }
}

// Cache management
class CacheManager {
  constructor() {
    this.caches = {
      static: 'static-cache-v1',
      dynamic: 'dynamic-cache-v1',
      api: 'api-cache-v1',
      images: 'images-cache-v1'
    };

    this.cacheRules = [
      // Static assets - cache first
      {
        match: request =>
          request.destination === 'script' ||
          request.destination === 'style' ||
          request.destination === 'font',
        strategy: 'cacheFirst',
        cacheName: this.caches.static
      },

      // Images - stale while revalidate
      {
        match: request => request.destination === 'image',
        strategy: 'staleWhileRevalidate',
        cacheName: this.caches.images,
        maxEntries: 100,
        maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
      },

      // API calls - network first with timeout
      {
        match: request => request.url.includes('/api/'),
        strategy: 'networkFirst',
        cacheName: this.caches.api,
        timeout: 3000, // 3 seconds
        maxEntries: 50
      },

      // HTML pages - network first
      {
        match: request =>
          request.destination === 'document' ||
          request.mode === 'navigate',
        strategy: 'networkFirst',
        cacheName: this.caches.dynamic
      },

      // External CDN - cache first
      {
        match: request =>
          request.url.includes('cdn.jsdelivr.net') ||
          request.url.includes('cdnjs.cloudflare.com'),
        strategy: 'cacheFirst',
        cacheName: this.caches.static
      }
    ];
  }

  async handleRequest(request) {
    const rule = this.findMatchingRule(request);

    if (!rule) {
      return fetch(request);
    }

    try {
      // Apply strategy with timeout if specified
      if (rule.timeout) {
        return await this.withTimeout(
          CacheStrategies[rule.strategy](request),
          rule.timeout,
          caches.match(request)
        );
      } else {
        const response = await CacheStrategies[rule.strategy](request);

        // Apply cache management rules
        if (response && response.ok && rule.cacheName) {
          await this.manageCache(rule.cacheName, request, rule);
        }

        return response;
      }
    } catch (error) {
      console.error('Cache strategy failed:', error);

      // Fallback to cache if available
      return caches.match(request) || new Response('Offline', {
        status: 503,
        statusText: 'Service Unavailable'
      });
    }
  }

  findMatchingRule(request) {
    return this.cacheRules.find(rule => rule.match(request));
  }

  async withTimeout(promise, timeout, fallback) {
    return Promise.race([
      promise,
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Timeout')), timeout)
      )
    ]).catch(error => {
      if (fallback) return fallback;
      throw error;
    });
  }

  async manageCache(cacheName, request, rule) {
    if (rule.maxEntries || rule.maxAge) {
      const cache = await caches.open(cacheName);
      const requests = await cache.keys();

      // Remove old entries if maxEntries exceeded
      if (rule.maxEntries && requests.length > rule.maxEntries) {
        const toDelete = requests.slice(0, requests.length - rule.maxEntries);
        await Promise.all(toDelete.map(req => cache.delete(req)));
      }

      // Remove old entries based on age
      if (rule.maxAge) {
        const now = Date.now();
        for (const cacheRequest of requests) {
          const response = await cache.match(cacheRequest);
          const dateHeader = response.headers.get('date');
          if (dateHeader && (now - new Date(dateHeader).getTime()) > rule.maxAge) {
            cache.delete(cacheRequest);
          }
        }
      }
    }
  }
}

// Initialize cache manager
const cacheManager = new CacheManager();

// Service worker event handlers
self.addEventListener('fetch', event => {
  event.respondWith(cacheManager.handleRequest(event.request));
});

// Install event - precache critical assets
self.addEventListener('install', event => {
  const staticAssets = [
    '/',
    '/index.html',
    '/offline.html',
    '/styles/main.css',
    '/scripts/app.js',
    '/images/logo.png'
  ];

  event.waitUntil(
    caches.open(cacheManager.caches.static)
      .then(cache => cache.addAll(staticAssets))
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames
          .filter(name => !Object.values(cacheManager.caches).includes(name))
          .map(name => caches.delete(name))
      );
    })
  );
});

// Background sync for offline analytics
self.addEventListener('sync', event => {
  if (event.tag === 'analytics-sync') {
    event.waitUntil(syncAnalytics());
  }
});

async function syncAnalytics() {
  const offlineData = await getOfflineAnalytics();

  for (const data of offlineData) {
    try {
      await fetch('/api/analytics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      await removeOfflineAnalytics(data.id);
    } catch (error) {
      console.error('Failed to sync analytics:', error);
      break;
    }
  }
}

// IndexedDB helpers for offline analytics
async function getOfflineAnalytics() {
  // Implementation for getting analytics from IndexedDB
  return [];
}

async function removeOfflineAnalytics(id) {
  // Implementation for removing analytics from IndexedDB
}

// main.js - Client-side analytics that works offline
class OfflineAnalytics {
  constructor() {
    this.db = null;
  }

  async init() {
    this.db = await this.openDB();
  }

  async openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('analytics-db', 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = () => {
        const db = request.result;
        db.createObjectStore('events', { keyPath: 'id', autoIncrement: true });
      };
    });
  }

  async track(eventName, properties = {}) {
    const event = {
      name: eventName,
      properties: {
        ...properties,
        timestamp: Date.now(),
        url: window.location.href,
        userAgent: navigator.userAgent
      }
    };

    try {
      // Try to send immediately
      const response = await fetch('/api/analytics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event)
      });

      if (!response.ok) throw new Error('Analytics request failed');

    } catch (error) {
      // Store offline
      await this.storeOffline(event);

      // Register background sync if available
      if ('serviceWorker' in navigator &&
          typeof ServiceWorkerRegistration !== 'undefined' &&
          'sync' in ServiceWorkerRegistration.prototype) {
        const registration = await navigator.serviceWorker.ready;
        registration.sync.register('analytics-sync');
      }
    }
  }

  async storeOffline(event) {
    const transaction = this.db.transaction(['events'], 'readwrite');
    const store = transaction.objectStore('events');
    await store.add(event);
  }
}

// Usage
const analytics = new OfflineAnalytics();
analytics.init();

// Track page view
analytics.track('page_view', {
  page: window.location.pathname,
  title: document.title
});

// Track user interaction
document.getElementById('cta-button').addEventListener('click', () => {
  analytics.track('cta_clicked', {
    button_text: 'Get Started',
    location: 'homepage'
  });
});

工具

经常与该案例搭配使用的工具

相关内容

相关案例集合

工具使用指南

帮助搜索与理解工作流的案例说明

标题

Service Workers 离线Web应用

描述

Service Workers 用于离线功能、后台同步和网络拦截

概览

Service Workers 用于离线功能、后台同步和网络拦截

适用场景

  • Use this sample set when you want reference inputs for Service Workers 离线Web应用.
  • Use it to preview output formats and compare related sample variants.

使用方式

  1. 1Open a sample entry to inspect the prepared example payload or asset.
  2. 2Compare it with related tools to understand how the workflow connects.

常见用途

  • 基础 Service Worker 设置
  • 后台同步
  • 推送通知
  • 高级缓存策略

常见问题

What is included in Service Workers 离线Web应用?

This page includes curated sample entries, related tools, and neighboring sample collections for Service Workers 离线Web应用.

首页
工具3197
案例593
可视化461
文件查看器103
专题229
标签333
图像、音频与视频655

图像、音频、视频处理、转换与优化工具

数学、日期与金融644

计算器、数值计算、日期逻辑、统计与金融工具

设计与颜色296

颜色、版式、图形、视觉样式与设计辅助工具

文本与写作209

写作、清洗、格式化、提取与文本分析工具

开发与 Web193

开发者工具、网络处理、Web 调试与自动化辅助工具

转换与编码175

格式、文件、压缩包、单位与编码转换工具

界面截图结构化、人工校对与开发交付
钢琴旋律草稿创作、MIDI 核验与试听样稿交付
复古游戏纹理制作、候选评审与素材交付
操作截图到可复核图文指南与培训交付
网页改版响应式视觉回归审核与证据交付

© 2026 Elysia Tools. 保留所有权利。

文档价格关于我们服务条款隐私政策Cookie 政策联系我们

Media

Chrome网上应用商店截图调整

将图片调整为Chrome网上应用商店截图尺寸,同时保留完整内容和原始宽高比例

Security

ReDoS 正则拒绝服务扫描器

扫描一个或多个正则表达式的灾难性回溯风险,模拟恶意输入并给出更安全的替代建议

Security

TOTP / HOTP 离线动态密码生成器

从 base32 共享密钥生成 RFC 6238 TOTP(基于时间,6/8 位、30/60 秒步长)与 RFC 4226 HOTP(基于计数器)一次性密码,完全离线,支持 HMAC-SHA1/256/512,并导出 otpauth:// URI 以便导入 Google Authenticator / Authy

Format Conversion

Web Push RFC 8030 请求 + VAPID + RFC 8291 负载加密编码器

编码完整的 Web Push 发送流程:RFC 8291 aes128gcm 负载加密、RFC 8292 VAPID ES256 JWT、RFC 8030 头部、APNs/FCM 头部对照与解密往返验证。

Web & Internet

Web 搜索(SearXNG JSON)

通过 s.stdlang.com 的 SearXNG 实例搜索网页,返回 JSON 结果供自动化或后续工具使用。

Design

网页字体配对实验室

并排预览 标题 + 正文 的 Google 字体配对,调节字重与行高,复制即可粘贴的 CSS @import + 变量代码。精选 22 款字体及 7 组经典搭配预设。

Web 技术

渐进式Web应用 (PWA)

构建现代、可靠、引人入胜的渐进式Web应用,具备离线功能

Web 技术

WebRTC 实时通信示例

全面的 WebRTC 示例,用于点对点音视频通信、数据通道、屏幕共享和信令服务器实现