在 GitHub Pages 上使用 sql.js-httpvfs 查询 SQLite

发布: (2026年3月9日 GMT+8 04:18)
9 分钟阅读
原文: Dev.to

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-httpvfssql.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,
  },
}
  1. 将数据库和静态资源放入仓库(或使用 Git LFS),然后推送。
    GitHub 的静态文件服务器默认支持 Range Requests——无需额外配置。
  2. S3Cloudflare PagesNetlify 也支持 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 OPFSwa‑sqlite
  • 无缓存驱逐 – 会话期间下载的页面会缓存在 Worker 内存中,且该缓存不会收缩。大量查询可能导致内存增长。
  • 实验性 – 作者在 README 中将其描述为演示级代码;不建议在高可靠性生产环境中使用。

进一步阅读

  • Getting Started with sql.js – 浏览器中的 SQLite(基础)。
  • Offline Web Apps with sql.js and IndexedDB – 完整的离线写入实现。
  • Browser Storage Solutions Compared – 浏览器存储选项的更广泛比较。
0 浏览
Back to Blog

相关文章

阅读更多 »

开发者角色,重新定义

Developer = Product + Architect + QA。 那就是在 AI 代理带来的新现实下,今天软件工程师的角色。如果那个公式...