在 GitHub Pages 上使用 sql.js-httpvfs 查询 SQLite
I’m happy to translate the article for you, but I need the full text of the article (the part you’d like translated) in order to do so. Could you please paste the content you want translated here?
The Problem
I once had a 670 MB SQLite database and a simple requirement: put it on a static site so users could search it by keyword.
- I didn’t want a backend – the whole project is static.
- Uploading the raw DB and letting the browser download it would make users wait for a 670 MB transfer.
Source: …
解决方案 – sql.js-httpvfs
sql.js-httpvfs 在 sql.js 的基础上,添加了基于 HTTP Range‑Request 的虚拟文件系统,使浏览器只获取查询实际需要的 SQLite 页面。
同样的 670 MB 数据库?一次简单的键查找只会传输大约 1 KB。
SQLite 页面是如何工作的
- SQLite 将数据存储在固定大小的页面中(默认 4096 字节)。
- 每个 B‑Tree 节点、索引条目和行都映射到一个特定的页面号。
- 使用索引的查询只读取沿着 B‑Tree 路径的页面——它永远不会扫描整张表。
sql.js-httpvfs 如何利用这一点
它取代了 Emscripten 的 VFS 层。不是从内存中的 ArrayBuffer 读取,而是发出 HTTP Range 请求:
GET /data.sqlite HTTP/1.1
Range: bytes=4096-8191
服务器只返回这 4096 字节,然后交给 SQLite 引擎处理。
所有工作都在 Web Worker 中运行,主线程保持响应,且每个查询都是 异步 的。
预取
库实现了 三个虚拟读取指针 来跟踪访问模式。
如果读取指针检测到顺序页面访问,它会自动提升预取力度——从一次读取一页到一次请求多页。这对全文搜索尤为关键,因为全文搜索会顺序遍历大量树节点。
索引至关重要
- 好 – 使用索引(覆盖查询,无需读取数据行)。
- 坏 – 全表扫描(下载整张表)。
-- 好:使用索引
EXPLAIN QUERY PLAN
SELECT name, price FROM products WHERE sku = 'ABC123';
-- 输出: SEARCH products USING INDEX idx_sku (sku=?)
-- 坏:全表扫描
EXPLAIN QUERY PLAN
SELECT * FROM products WHERE description LIKE '%keyword%';
-- 输出: SCAN products
安装
npm install sql.js-httpvfs
sql.js-httpvfs 需要另外两个静态资源:sql-wasm.wasm 和一个 Worker JS 文件。这两个文件都已包含在包中,只需将它们复制到你的公共目录即可。
# 使用 Vite(或任何静态文件服务器)
cp node_modules/sql.js-httpvfs/dist/sql-wasm.wasm public/
cp node_modules/sql.js-httpvfs/dist/sqlite.worker.js public/
初始化 Worker
import { createDbWorker } from 'sql.js-httpvfs';
// URLs must point to the static files you just copied
const workerUrl = new URL('/sqlite.worker.js', import.meta.url);
const wasmUrl = new URL('/sql-wasm.wasm', import.meta.url);
const worker = await createDbWorker(
[
{
from: 'url', // load DB from a URL (there's also an inline mode)
config: {
serverMode: 'full', // single‑file mode
url: '/data.sqlite', // path to the database
requestChunkSize: 4096, // Range Request size, aligned to SQLite page size
},
},
],
workerUrl.toString(),
wasmUrl.toString()
);
requestChunkSize 默认值为 4096,与 SQLite 的默认页大小相匹配。如果您使用不同的页大小,请相应地调整此值。
优化 SQLite 文件
数据库的页面大小直接影响传输效率。请在上传文件 之前 执行以下步骤:
-- Smaller page size → finer‑grained Range Requests
PRAGMA page_size = 1024; -- must be set before any tables are created
-- Remove the WAL file (otherwise you’d need to keep two files in sync)
PRAGMA journal_mode = delete;
-- Rebuild the DB to apply the new page size and remove fragmentation
VACUUM;
设计索引
思考你的查询模式,并在可能的情况下使用 覆盖索引。
-- Example: common query is
-- WHERE category = ? ORDER BY created_at DESC LIMIT 20
-- A covering index includes all columns needed by SELECT,
-- so the query never touches the data rows.
CREATE INDEX idx_category_date_cover
ON articles(category, created_at DESC, title, slug);
使用 FTS5 的全文搜索
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
content,
content='articles', -- reference the source table to avoid duplicate storage
content_rowid='id'
);
-- Populate the FTS index on initial data load
INSERT INTO articles_fts(articles_fts) VALUES('rebuild');
查询数据库
一旦 worker 设置完毕,查询方式与普通 sql.js 类似——但所有操作都会返回一个 Promise。
标准查询
const results = await worker.db.query(
`SELECT title, slug, created_at
FROM articles
WHERE category = ?
ORDER BY created_at DESC
LIMIT 20`,
['frontend']
);
// results → { columns: string[], values: any[][] }
console.log(results.columns); // ['title', 'slug', 'created_at']
console.log(results.values); // [['Article title', 'slug-here', '2024-01-01'], ...]
全文搜索
const ftsResults = await worker.db.query(
`SELECT a.title,
a.slug,
snippet(articles_fts, 1, '', '', '...', 20) AS excerpt
FROM articles_fts
JOIN articles a ON articles_fts.rowid = a.id
WHERE articles_fts MATCH ?
ORDER BY rank
LIMIT 10`,
[keyword]
);
测量传输大小
我最喜欢的功能之一:您可以准确看到每个查询获取了多少字节。
// Record stats before the query
const bytesBefore = worker.getStats().totalFetchedBytes;
await worker.db.query('SELECT * FROM articles WHERE id = ?', [42]);
// Compare after
const bytesAfter = worker.getStats().totalFetchedBytes;
console.log(`Query transferred: ${bytesAfter - bytesBefore} bytes`);
getStats() 返回一个对象,其中包含:
totalFetchedBytes– 累计已传输的字节数。totalRequests– 累计已发出的 HTTP Range 请求数量。
TL;DR
sql.js-httpvfs让静态站点在不下载整个文件的情况下查询大型 SQLite 数据库。- 它通过对所需页面发出 HTTP Range Requests 来工作。
- 索引(尤其是覆盖索引)对于保持传输量极小至关重要。
- 该库随附 Web Worker 和 WASM 构建;只需复制这两个静态资源,初始化 worker,并像使用
sql.js那样进行查询。
祝静态站点搜索愉快!
Source: …
浏览器中的分块 SQLite 文件
在开发过程中,我会在屏幕上显示请求计数,以验证索引是否真的在工作。
对于大型数据库,你可以将文件拆分为固定大小的块,这样 CDN 缓存会更加高效。
使用系统 split 命令拆分(Linux/macOS)
split -b 10m data.sqlite data.sqlite.
# 生成:data.sqlite.aa, data.sqlite.ab, …
或者使用 sql.js‑httpvfs 捆绑的工具
npx sql.js-httpvfs-tools split data.sqlite --chunk-size 10485760
# 生成拆分文件以及描述块的 JSON 清单
为 sql.js-httpvfs 配置分块模式
{
from: 'url',
config: {
serverMode: 'chunked',
serverChunkSize: 10 * 1024 * 1024, // 每块 10 MB
urlPrefix: '/db/data.sqlite.', // 所有块文件共享的前缀
urlSuffix: '',
fromCache: false,
requestChunkSize: 4096,
},
}
- 将数据库和静态资源放入仓库(或使用 Git LFS),然后推送。
GitHub 的静态文件服务器默认支持 Range Requests——无需额外配置。 - S3、Cloudflare Pages 和 Netlify 也支持 Range Requests,因而都可以直接使用。
CORS 注意事项
如果前端和数据库位于不同的源,服务器必须返回:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Range
Access-Control-Expose-Headers: Content-Range, Accept-Ranges
在采用此方案前需要了解的事项
- 只读 – HTTP Range Requests 仅用于读取操作;写入需要后端支持。
如需在浏览器中实现读写,请参考官方的 SQLite Wasm with OPFS 或 wa‑sqlite。 - 无缓存驱逐 – 会话期间下载的页面会缓存在 Worker 内存中,且该缓存不会收缩。大量查询可能导致内存增长。
- 实验性 – 作者在 README 中将其描述为演示级代码;不建议在高可靠性生产环境中使用。
进一步阅读
- Getting Started with sql.js – 浏览器中的 SQLite(基础)。
- Offline Web Apps with sql.js and IndexedDB – 完整的离线写入实现。
- Browser Storage Solutions Compared – 浏览器存储选项的更广泛比较。