导读:本期聚焦于香港程序员创作的《如何用SQLite和Service Worker打造离线可用的Web应用缓存方案?》,敬请观看详情。网页断网就白屏,数据一刷新就丢,这是不少前端项目上线后被吐槽最多的问题。本文围绕SQLite与Service Worker的组合展开实战讲解,先分析IndexedDB在复杂查询场景下的短板,说明为什么sql.js这类SQLite编译方案更适合结构化离线数据;再一步步演示如何在页面中初始化SQLite数据库、执行建表与增删改查,并把数据库文件持久化到浏览器存储;最后重点讲解Service Worker的缓存拦截策略,让静态资源与动态接口各自走不同缓存通道,实现真正的离线访问。文中附带完整代码示例与常见踩坑点,适合想提升Web应用可用性的开发者参考。

做Web应用最尴尬的场景莫过于用户在地铁里打开页面,网络一断,整个应用直接白屏,之前填了一半的数据也全部丢失。传统的解决方案要么依赖localStorage存几个键值对,要么用IndexedDB硬扛复杂查询,但一旦数据结构变复杂、查询条件变多,代码就会迅速失控。把SQLite搬进浏览器,配合Service Worker做资源与接口缓存,是目前比较成熟的离线方案。本文将完整演示这套组合的实现过程。

如何用SQLite和Service Worker打造离线可用的Web应用缓存方案?

一、为什么选择SQLite而不是原生IndexedDB

浏览器原生的本地存储方案里,localStorage只有几MB容量且只能存字符串,IndexedDB虽然容量大、支持索引,但它的API是面向对象的键值存储模型,没有JOIN、没有GROUP BY,也没有标准的SQL语法。当你的离线数据涉及多张关联表,比如订单表关联用户表、用户表关联地址表时,用IndexedDB手写查询逻辑的工作量会成倍增加。

SQLite通过Emscripten编译成WebAssembly后可以在浏览器中直接运行,最流行的封装库是sql.js。它提供了完整的SQL执行能力,建表、事务、联表查询统统支持,对于熟悉SQL的开发者几乎没有学习成本。数据库整体以一个文件的形式存在,导出就是一个Uint8Array,可以随时上传到服务器备份或持久化到IndexedDB。

两者的核心差异可以用一个表格概括:

对比维度IndexedDBSQLite (sql.js)
查询能力键值与索引,无SQL完整SQL语法,支持JOIN、事务
学习成本API繁琐,回调复杂会SQL即可上手
数据导出需逐仓库遍历export导出单个文件
性能大数据量下各有胜负复杂查询明显占优

二、在页面中初始化SQLite并执行CRUD

首先通过npm或CDN引入sql.js,同时需要加载对应的wasm文件。初始化的关键在于指定locateFile,让程序知道wasm资源的位置,否则会报找不到文件的错误。

import initSqlJs from 'sql.js';

const SQL = await initSqlJs({
  // 告诉sql.js去哪里找wasm文件
  locateFile: file => `/wasm/${file}`
});

// 尝试从IndexedDB恢复上次的数据库文件
const saved = await loadDbFromIndexedDB();
const db = saved ? new SQL.Database(saved) : new SQL.Database();

// 首次使用则建表
db.run(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    updated_at INTEGER DEFAULT (strftime('%s','now'))
  )
`);

建表之后就可以执行常规的增删改查。sql.js提供了两种执行方式:db.run适合执行写操作,db.exec会返回结构化的结果集,适合查询。下面是插入和查询的示例:

// 插入数据,务必使用预处理语句防注入
db.run('INSERT INTO notes (title, content) VALUES (?, ?)', ['购物清单', '牛奶、面包']);

// 查询并取出结果
const result = db.exec('SELECT * FROM notes ORDER BY updated_at DESC');
const rows = result[0]?.values ?? [];

// 关键步骤:把内存中的数据库写回IndexedDB持久化
await saveDbToIndexedDB(db.export());

这里有一个非常容易踩的坑:sql.js的数据库是完整加载进内存的,任何写操作都只改了内存中的数据,如果不调用db.export()把二进制内容写回IndexedDB,刷新页面后数据就会丢失。因此建议把持久化封装成防抖函数,在每次写操作后延迟几百毫秒统一保存,避免频繁序列化带来的性能开销。

三、用Service Worker拦截请求实现离线访问

SQLite解决的是数据离线,Service Worker解决的是资源和接口离线。它是一个运行在浏览器后台的独立线程,能够拦截页面发出的所有网络请求,根据策略决定从缓存返回还是从网络获取。注册方式如下:

// 在主页面中注册,注意SW文件必须位于根目录才能拦截全站请求
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('注册成功', reg.scope))
    .catch(err => console.error('注册失败', err));
}

sw.js内部通常采用缓存优先策略处理静态资源:首次访问时把CSS、JS、字体等文件放进Cache Storage,之后即使断网,页面骨架也能完整渲染。下面是一个经典的缓存优先实现:

const CACHE_NAME = 'app-cache-v1';
const ASSETS = ['/', '/index.html', '/app.js', '/style.css'];

self.addEventListener('install', event => {
  event.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ASSETS)));
});

self.addEventListener('fetch', event => {
  const { request } = event;
  // HTML页面使用网络优先,失败后回退缓存
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request).catch(() => caches.match('/index.html'))
    );
    return;
  }
  // 其余资源缓存优先
  event.respondWith(
    caches.match(request).then(hit => hit || fetch(request))
  );
});

对于动态接口,盲目缓存会导致数据过期。更好的做法是采用Network First策略:优先请求网络,成功后更新缓存副本,失败时才返回缓存数据。这样用户在线时永远看到最新数据,断网时仍能拿到上一次的结果。

四、两个模块如何协同工作

整套方案的分工非常清晰:Service Worker负责让应用的外壳在离线时照常加载,页面本身能够打开;SQLite负责让业务数据在离线时可以读写。用户在线时,接口数据同步写入本地SQLite库;离线时,页面照常从SQLite读取数据渲染,用户的写操作先落库并打上未同步标记,网络恢复后再批量上传。

// 监听网络恢复,同步离线期间的数据
window.addEventListener('online', async () => {
  const pending = db.exec('SELECT * FROM notes WHERE synced = 0');
  for (const row of pending[0]?.values ?? []) {
    await fetch('/api/notes', {
      method: 'POST',
      body: JSON.stringify({ title: row[1], content: row[2] })
    });
  }
  db.run('UPDATE notes SET synced = 1 WHERE synced = 0');
  await saveDbToIndexedDB(db.export());
});

最后提醒几个上线前必须注意的点:一是SW文件更新后旧缓存不会自动失效,要通过修改CACHE_NAME版本号并在activate事件中清理旧缓存;二是sql.js的wasm文件体积约1MB,务必让Service Worker把它也缓存下来,否则首次离线时初始化会直接失败;三是数据库文件随数据增长会变大,定期执行VACUUM可以压缩体积。把这套方案落地后,你的Web应用就能真正做到断网可用、数据不丢。

SQLiteService Worker离线缓存修改时间:2026-09-03 00:17:25

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260903/49226.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。