sqlite3_simple - Web
May 30, 2026 · View on GitHub
在线演示:Simple 分词器 示例 。代码请参考 example/lib/data/impl_web 。
快速开始
1. 添加 sqlite3 、sqlite3_web 依赖
在 Web 上使用 Simple 分词器,需要 sqlite3: ^3.3.0 或以上版本(原因见此处),sqlite3_web 理论上不受限制但还是建议使用最新版本:
dependencies:
sqlite3: ^3.3.0
sqlite3_web: ^0.7.0
📝 为什么需要 sqlite3_web ?
SQLite 是基于文件的嵌入式数据库,但在 Web 环境中无法直接访问文件系统,因此需要 sqlite3_web 提供的基于 Web Worker(属于后台线程,支持文件系统 API)管理数据库的异步 API 。
sqlite3 提供的同步 API 不满足 Web Worker 的异步通信需求,已有项目增加 Web 支持,可能涉及大量同步异步代码的调整。一种可以复用同步 API 的方法是通过 squadron 或其他框架自行管理 Web Worker,例如示例中的 impl_web/sqlite3_wasm 就复用了 Sqlite3Dao 。不过此类通用框架首屏加载速度会稍慢,需要根据实际项目需求选择。
2. 添加本库,下载 sqlite3.wasm
flutter pub add sqlite3_simple
随后到本仓库的 Release 中下载 sqlite3.wasm 或 sqlite3mc.wasm (内置 SQLite3 Multiple Ciphers 扩展) ,置于项目的 web 或者资源等可访问目录下,以便后续加载使用。
Tip
sqlite3 使用 WASI SDK 通过 sqlite3_wasm_build 将 SQLite 编译为 WebAssembly 文件以在 Web 上使用。
由于目前 SQLite 不支持在 Web 上加载运行时扩展 ,本项目基于 sqlite3_wasm_build 编译了内置 Simple 分词器的 sqlite3.wasm 和 sqlite3mc.wasm ,编译代码由 simple-native 分支 维护,若需自行编译请参考 src/web 。
3. 编写在 Web Worker 中的数据库管理逻辑
根据 sqlite3_web 的说明 进行编写,例如 impl_web/sqlite3_web/sqlite3_web_worker.dart 。
Web Worker 启动入口
void main() {
WebSqlite.workerEntrypoint(controller: Sqlite3WebDbController());
}
Wasm 模块和数据库加载
final class Sqlite3WebDbController extends DatabaseController {
final DefaultSimpleWasmModuleLoader _loader = DefaultSimpleWasmModuleLoader();
@override
Future<WasmSqlite3> loadWasmModule(
Uri uri, {
Map<String, String>? headers,
}) async {
return WasmSqlite3.loadFromUrl(uri, headers: headers, loader: _loader);
}
@override
Future<WorkerDatabase> openDatabase(
WasmSqlite3 sqlite3,
String path,
String vfs,
JSAny? additionalData,
) async {
return Sqlite3WebDb(sqlite3.open(path, vfs: vfs), _loader);
}
@override
Future<JSAny?> handleCustomRequest(
ClientConnection connection,
CustomClientRequest request,
) async => null;
}
其中 DefaultSimpleWasmModuleLoader 是支撑 Simple 扩展在 Web 上正常使用的核心组件,主要职责如下:
- 在
WebAssembly.instantiateStreaming(source, importObject)模块实例化时,将所需的宿主侧能力通过importObject提供给模块用于运行期调用。 - 在模块实例化后,将模块导出的线性内存交由宿主侧,以供文件读写。
Tip
用于自定义 Wasm 模块加载的 WasmModuleLoader 在 sqlite3: 3.3.0 才被引入,因此 3.3.0 是在 Web 上使用 Simple 的最低版本要求。
📝 DefaultSimpleWasmModuleLoader 实现
由于 Simple 涉及文件读写(fopen、read、write ...)等在 Web 环境中不可用的系统调用,WASI 会将其统一为抽象为 wasi_snapshot_preview1 模块的 API(path_open、fd_read、fd_write 等)并要求宿主侧提供实现。DefaultSimpleWasmModuleLoader 通过 wasd 实现了这些 API,为 Simple 提供了 Web 环境支持。
如果默认实现不满足要求,可以自行继承 SimpleBridgeCallbacks 和 WasmModuleLoader 进行扩展。
通过主线程传递自定义消息,加载结巴分词字典文件
本库内置了结巴分词所需的字典文件,可通过 JiebaDictAssets.loadPaths() 获取其路径。由于涉及对 Flutter 资源的访问,该方法需要在主线程中调用。 因此,使用结巴分词,需要自定义消息将路径传递到 Worker 中,然后使用 JS 的 fetch 函数进行加载,例如:
final class Sqlite3WebDb extends WorkerDatabase {
@override
final CommonDatabase database;
final DefaultSimpleWasmModuleLoader _loader;
Sqlite3WebDb(this.database, this._loader);
@override
Future<JSAny?> handleCustomRequest(
ClientConnection connection,
CustomClientDatabaseRequest request,
) async {
// 将内置的结巴分词文件传递给 Wasm 模块以供运行时读取,不需要结巴分词直接 `return null` 即可
try {
final requestJO = request.request as JSObject;
final type = (requestJO['type'] as JSString).toDart;
switch(type) {
case 'updateJiebaDict': // 自定义消息 { 'type': 'updateJiebaDict', 'path2url': 虚拟路径 → 文件内容 }
final path2url = (requestJO['path2url'].dartify() as Map).cast<String, String>();
final jiebaDictFiles = {
for (final e in path2url.entries)
e.key: await fetchFromBase(e.value),
};
_loader.updateFiles((it) => it..addAll(jiebaDictFiles)); // 更新结巴分词文件,其他已有文件继续保留
}
} catch (e) {
rethrow;
}
return null;
}
}
fetchFromBase 来自 impl_web/fetch.dart:
import 'dart:js_interop';
import 'dart:typed_data';
import 'package:web/web.dart';
@JS("fetch")
external JSPromise<Response> jsFetch(URL resource, [RequestInit? options]);
Future<Uint8List> fetch(String url, [String? base]) async {
final jsURL = base != null ? URL(url, base) : URL(url);
final response = await jsFetch(jsURL).toDart;
return (await response.bytes().toDart).toDart;
}
Future<Uint8List> fetchFromBase(String url) => fetch(url, Uri.base.toString());
_loader.updateFiles 用于更新提供给 Wasm 模块读取的文件。在 DefaultSimpleWasmModuleLoader 的实现中,文件读写通过内存中的「虚拟路径 → 文件内容 Uint8List」映射关系来模拟实现。
编译为 JavaScript
上述代码通过 dart compile js 编译为 JavaScript 后才能使用。参考示例代码在 example 目录下执行如下命令,即可编译成 sqlite3_web_worker.dart.js 供后续使用:
dart compile js lib\data\impl_web\sqlite3_web\sqlite3_web_worker.dart -o web\sqlite3_web_worker.dart.js -O4
Important
Flutter 相关的依赖不支持编译为 JS,注意不要被直接或间接依赖。
调试阶段建议省略 -O4 优化选项,以保留更清晰的错误信息和调用栈,方便问题定位。
4. 打开数据库
根据 sqlite3_web 的说明,以及前面下载的 sqlite3.wasm 和编译的 sqlite3_web_worker.dart.js 的位置,打开数据库:
final webSqlite3 = WebSqlite.open(
workers: WorkerConnector.defaultWorkers(
Uri.parse('sqlite3_web_worker.dart.js'),
),
wasmModule: Uri.parse('sqlite3.wasm'),
);
final db = await webSqlite3.connectToRecommended('database');
如需启用结巴分词,根据前面自定义的消息,调用 db.customRequest() 进行传递:
final Map<JiebaDictType, String> jiebaDictPaths = await JiebaDictAssets.loadPaths();
const jiebaDictDir = '.dict';
await db.customRequest(
{
'type': 'updateJiebaDict',
'path2url': {
for (final e in jiebaDictPaths.entries)
'$jiebaDictDir/${e.key.filename}': e.value,
}
}.jsify(),
);
await db.execute("SELECT jieba_dict(?)", parameters: [jiebaDictDir]);
final init = await db.select( // 推荐在正式查询前执行一次查询,提前加载
"SELECT jieba_query('Jieba分词初始化(提前加载避免后续等待)')",
);
if (kDebugMode) print(init.result);
JiebaDictType 枚举定义了结巴分词所需的所有字典类型,如 jiebaDict("jieba.dict.utf8") 等。
5. 创建主表、FTS5 虚表和触发器
根据 SQLite FTS5 Extension 的说明和实际项目需求创表,例如 impl_web/sqlite3_web/sqlite3_web_impl.dart :
// final fts5Tokenizer = "simple 0"; // 关闭拼音搜索
const fts5Tokenizer = "simple";
const mainTable = "custom";
const id = "id",
title = "title",
content = "content",
insertDate = "insert_date";
const fts5Table = "t1";
@override
@protected
Future<void> createMainAndFts5(Database db) async {
/// 主表
await db.execute('''
CREATE TABLE $mainTable (
$id INTEGER PRIMARY KEY AUTOINCREMENT,
$title TEXT,
$content TEXT,
$insertDate INTEGER
);
''');
/// FTS5虚表
await db.execute('''
CREATE VIRTUAL TABLE $fts5Table USING fts5(
$title, $content, $insertDate UNINDEXED,
tokenize = '$fts5Tokenizer',
content = '$mainTable',
content_rowid = '$id'
);
''');
const newInsert = '''
INSERT INTO $fts5Table(rowid, $title, $content, $insertDate)
VALUES (new.$id, new.$title, new.$content, new.$insertDate);
''';
const deleteInsert = '''
INSERT INTO $fts5Table($fts5Table, rowid, $title, $content, $insertDate)
VALUES ('delete', old.$id, old.$title, old.$content, old.$insertDate);
''';
await db.execute('''
CREATE TRIGGER ${mainTable}_insert AFTER INSERT ON $mainTable BEGIN
$newInsert
END;
''');
await db.execute('''
CREATE TRIGGER ${mainTable}_delete AFTER DELETE ON $mainTable BEGIN
$deleteInsert
END;
''');
await db.execute('''
CREATE TRIGGER ${mainTable}_update AFTER UPDATE ON $mainTable BEGIN
$deleteInsert
$newInsert
END;
''');
}
6. 查询
参阅 Simple 的说明,根据需要调用 jieba_query、simple_query、highlight、 simple_highlight 进行查询,例如 impl_web/sqlite3_web/sqlite3_web_impl.dart :
@override
Future<List<MainTableRow>> search(String value, Tokenizer tokenizer) async {
const wrapperSql = "'${ZeroWidth.start}', '${ZeroWidth.end}'";
final resultSet = await db.select(
'''
SELECT
rowid AS $id,
simple_highlight($fts5Table, 0, $wrapperSql) AS $title,
simple_highlight($fts5Table, 1, $wrapperSql) AS $content,
$insertDate
FROM $fts5Table
WHERE $fts5Table MATCH ${tokenizer.name}_query(?);
''',
parameters: [value],
);
return _toMainTableRows(resultSet.result);
}