在浏览器中使用SQLite(官方sqlite3.wasm)

发布于:2025-07-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

有人可能会问:既然浏览器里又内置得IndexedDB,而且在IndexedDB里存数据,关了浏览器数据也不会丢,为什么还要在浏览器里用SQLite?

实际上,当 IndexedDB 内的数据量增多,数据和数据之间的关系变得复杂,IndexedDB 的劣势就凸显出来了。我们在这方面踩过很多坑,这里就不细说了。

好了,言归正传:

我建议你不要使用这个工具:https://github.com/sqlite/sqlite-wasm,用起来麻烦的很,而且类型定义还有问题(看看 issue 就知道了)。

还是自己使用原始的、官方提供的 js 文件比较好。

首先到  SQLite Download Page 下载 WebAssembly & JavaScript 版本的SQLite。

下载解压后你将得到:

我们需要的文件都在 jswasm 目录中。

把 jswasm 目录中的如下文件拷贝到你的根目录下,(可以通过 https://domain.com/sqlite3.js 访问)

接着,在你的页面引入 sqlite3-worker1-promiser.js 脚本文件

<script src="sqlite3-worker1-promiser.js"></script>

现在我们封装一个 TypeScript 类

class Db {
  dbId: string;
  dbFunc: (cmd: string, param: object) => Promise<{ dbId: string; messageId: string; type: string; result: any }>;
  constructor() {}
  exec(sql: string): Promise<any[]> {
    return new Promise((resolve, reject) => {
      let rows = [];
      this.dbFunc("exec", {
        dbId: this.dbId,
        sql,
        callback: (result: { columnNames: string[]; row: any[]; rowNumber: number; type: string }) => {
          if (result.row) {
            let obj = {};
            for (let i = 0; i < result.columnNames.length; i++) {
              obj[result.columnNames[i]] = result.row[i];
            }
            rows.push(obj);
          } else {
            resolve(rows);
          }
        },
      });
    });
  }
  async open() {
    const dbFactory = globalThis.sqlite3Worker1Promiser.v2;
    delete globalThis.sqlite3Worker1Promiser;
    const config = {
      debug: (...args) => console.debug("db worker debug", ...args),
      onunhandled: (ev) => console.error("Unhandled db worker message:", ev.data),
      onerror: (ev) => console.error("db worker error:", ev),
    };
    this.dbFunc = await dbFactory(config);
    let { dbId } = await this.dbFunc("open", {
      filename: "file:db.sqlite3?vfs=opfs",
      simulateError: 0,
    });
    this.dbId = dbId;
    let rows = await this.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='Job';`);
    if (rows.length <= 0) {
      await this.exec(`CREATE TABLE Job(Id VARCHAR2(36) NOT NULL PRIMARY KEY, JobInfo TEXT, RepeatType INT, StartTime BIGINT, EndTime BIGINT, ColorIndex INT);
CREATE INDEX JobInfo_Index ON Job(JobInfo);
CREATE TABLE Setting(ViewDefault INT DEFAULT 0, ViewVal INT, LangDefault INT DEFAULT 0, SkinDefault INT DEFAULT 0, AlertBefore INT);
INSERT INTO Setting (ViewDefault, ViewVal, LangDefault, SkinDefault, AlertBefore) VALUES (0, 0, 0, 0, 5);`);
    }
    let data = await this.exec(`select * from Setting;`);
    console.log(data);
  }
  async delDb() {
    const opfsRoot = await navigator.storage.getDirectory();
    await opfsRoot.removeEntry("db.sqlite3");
  }
}
export let db = new Db();

先来看打开数据库方法: open 。

我们使用 SQLite3 的 V2 版本的方法,其他老方法一股脑删掉。

const dbFactory = globalThis.sqlite3Worker1Promiser.v2;
delete globalThis.sqlite3Worker1Promiser;

接着创建一个数据库访问方法:dbFunc,然后使用这个方法创建数据库:db.sqlite3

this.dbFunc = await dbFactory(config);
let { dbId } = await this.dbFunc("open", {
  filename: "file:db.sqlite3?vfs=opfs",
  simulateError: 0,
});

注意,filename的路径和参数,我们让数据库保存在浏览器的OPFS文件系统中。

OPFS 是浏览器提供的一种私有文件系统,属于 File System Access API 的一部分。它的特点包括:

私有性:每个网站拥有独立的文件系统,其他网站无法访问。

高性能:支持原地读写和同步访问,适合数据库等对性能要求高的场景。

持久化:数据不会因刷新或关闭浏览器而丢失。

 数据文件创建成功后,将得到数据库id:dbId,这是个字符串。

接下来,我们检查数据库中是否存在指定的表,没有的话,就给数据库建表。

此时就用到了SQL指令执行方法:exec

执行 SQL 指令,也是用 dbFunc 方法完成的。

如果执行的 SQL 语句本身不返回数据(例如 INSERT, UPDATE, DELETE, 或查询结果为空的 SELECT),回调方法 callback 只会被执行一次。callback 的传入参数 result 没有 row 属性。

如果执行的 SQL 语句本身返回多行数据,那么 callback 方法会被执行多次,每次都可能返回多行查询结果,查询结果被存储在 result 的 row 属性里,最后一次 callback 方法被执行时, result 没有 row 属性,表示查询结束。

下面这段代码用来根据列名构造数据对象:

if (result.row) {
  let obj = {};
  for (let i = 0; i < result.columnNames.length; i++) {
      obj[result.columnNames[i]] = result.row[i];
  }
  rows.push(obj);
} else {
  resolve(rows);
}

如果你想删除数据库,可以使用 delDb 方法

const opfsRoot = await navigator.storage.getDirectory() 用于获取 OPFS 文件系统的根目录实例

opfsRoot.removeEntry(name) 会从当前目录中删除名为 "db.sqlite3" 的文件或子目录。


网站公告

今日签到

点亮在社区的每一天
去签到