我们最近用上了 WXT 开发框架,方便于在浏览器插件中开发。但是在使用 Pglite 的时候遇到了一个问题,就是 wxt 自己的 build 任务是无法将 @electric-sql/pglite 包中必须依赖的 postgres.wasm
和 postgres.data
文件打包到插件中的,使用 PGlite 的时候,在浏览器中就会报错:
Uncaught (in promise) TypeError: Failed to fetch
...
1253 var Ge = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string", ie;
1254 async function Er() {
1255 if (Ge || ie) return;
1256 let e = new URL("./postgres.wasm", self.location.href);
1257 ie = fetch(e);
1258 }
1259 var V;
...
以及:
Uncaught (in promise) TypeError: Failed to fetch
...
1272 async function Pr() {
1273 let e = new URL("./postgres.data", self.location.href);
1274 return Ge ? (await (await Promise.resolve().then(() => __viteBrowserExternal$1)).readFile(e)).buffer : (await fetch(e)).arrayBuffer();
1275 }
1276 x$2();
...
从错误堆栈可以看到是加载 pstgres.wasm
和 postgres.data
文件时,浏览器无法找到这两个文件,导致加载失败。那么我们可以通过查阅 PGlite API 的时候,发现这两个文件是可以外部化的,通过远程地址加载,这样不但可以解决 PGlite 在浏览器插件环境中的使用问题,还可以大大减小插件打包容量。
这里我们使用一个 initdb()
函数初始化 PGlite 实例,并返回一个 db
对象,供后续使用。
import { PGlite } from "@electric-sql/pglite";
// 前端在加载这段代码的时候,建议加上 loading 或 initialing 的 UI 效果
// 因为这两个文件较大,加起来十几MB,避免在网络较慢的情况下阻塞用户体验
async function initdb() {
const postgresWasmURL =
"https://unpkg.com/@electric-sql/pglite/dist/postgres.wasm";
const postgresDataURL =
"https://unpkg.com/@electric-sql/pglite/dist/postgres.data";
const postgresWasmPromise = WebAssembly.compileStreaming(
fetch(postgresWasmURL)
);
const postgresDataPromise = fetch(postgresDataURL).then((res) => res.blob());
// 持久化到 IndexedDB,实际路径为 `/pglite/my-data`
return PGlite.create("idb://my-data", {
wasmModule: await postgresWasmPromise,
fsBundle: await postgresDataPromise,
});
}
使用示例:
async function main() {
const db = await initdb();
console.log("initdb success");
await db.exec(`
CREATE TABLE IF NOT EXISTS todo (
id SERIAL PRIMARY KEY,
task TEXT,
done BOOLEAN DEFAULT false
);
INSERT INTO todo (task, done) VALUES ('Install PGlite from NPM', true);
INSERT INTO todo (task, done) VALUES ('Load PGlite', true);
INSERT INTO todo (task, done) VALUES ('Create a table', true);
INSERT INTO todo (task, done) VALUES ('Insert some data', true);
INSERT INTO todo (task) VALUES ('Update a task');
`);
const ret = await db.query(`
SELECT * from todo;
`);
console.log(ret.rows);
}
NOTE: 这里我们用了
unpkg.com
的 CDN 地址,加载的是最新版本的 PGlite 中的postgres.wasm
和postgres.data
文件,这个地址会经过 302 跳转到当前最新版本的地址上。建议固定自己当前使用的版本,如https://unpkg.com/@electric-sql/pglite@0.2.12/dist/postgres.wasm
。只有第一次初始化的时候,浏览器会下载这个几个文件,后续加载的时候,浏览器会直接从命中缓存,不会重复下载,后续初始化就会很快。
同样也可以使用其他 CDN 或自建 CDN 代替,如:
cdn.jsdelivr.net
通常的,其他 PGlite 插件也没有打包到浏览器插件中,也需要从远程地址加载,否则一样会出现上述错误。
我们可以参考 PGlite 插件对应的源码,重新实现它通过 URL
方式加载插件的逻辑,即可实现远程地址加载,这里我们以 pgvector 插件为例:
import { PGlite } from "@electric-sql/pglite";
import { vector } from "@electric-sql/pglite/vector";
async function initdb() {
const postgresWasmURL =
"https://unpkg.com/@electric-sql/pglite/dist/postgres.wasm";
const postgresDataURL =
"https://unpkg.com/@electric-sql/pglite/dist/postgres.data";
const vectorExtensionURL =
"https://unpkg.com/@electric-sql/pglite/dist/vector.tar.gz";
const postgresWasmPromise = WebAssembly.compileStreaming(
fetch(postgresWasmURL)
);
const postgresDataPromise = fetch(postgresDataURL).then((res) => res.blob());
vector.setup = async (_pg, emscriptenOpts: any) => {
return {
emscriptenOpts,
bundlePath: new URL(vectorExtensionURL, import.meta.url),
};
};
return PGlite.create("idb://my-data", {
wasmModule: await postgresWasmPromise,
fsBundle: await postgresDataPromise,
extensions: { vector },
});
}
使用插件:
async function main() {
const db = await initdb();
await db.exec(`
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS items (id bigserial PRIMARY KEY, embedding vector(3));
INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]');
`);
const result = await db.exec(
"SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;"
);
console.log(result);
}
这里我们稍加改动了 vector.setup
函数,实现使用远程 URL 加载插件的逻辑。
我们在浏览器插件这种受限的打包环境中,可以实现远程加载的方式成功使用 PGlite。
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章