有人可能会问:既然浏览器里又内置得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 脚本文件
html
<script src="sqlite3-worker1-promiser.js"></script>
现在我们封装一个 TypeScript 类
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 版本的方法,其他老方法一股脑删掉。
TypeScript
const dbFactory = globalThis.sqlite3Worker1Promiser.v2;
delete globalThis.sqlite3Worker1Promiser;
接着创建一个数据库访问方法:dbFunc,然后使用这个方法创建数据库:db.sqlite3
TypeScript
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 属性,表示查询结束。
下面这段代码用来根据列名构造数据对象:
javascript
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" 的文件或子目录。