DTeam 技术日志

Doer、Delivery、Dream

PGlite 在浏览器插件环境中加载远程 WASM 的方案

冯宇 Posted at — Oct 23, 2024 阅读

背景

我们最近用上了 WXT 开发框架,方便于在浏览器插件中开发。但是在使用 Pglite 的时候遇到了一个问题,就是 wxt 自己的 build 任务是无法将 @electric-sql/pglite 包中必须依赖的 postgres.wasmpostgres.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.wasmpostgres.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.wasmpostgres.data 文件,这个地址会经过 302 跳转到当前最新版本的地址上。建议固定自己当前使用的版本,如 https://unpkg.com/@electric-sql/pglite@0.2.12/dist/postgres.wasm

只有第一次初始化的时候,浏览器会下载这个几个文件,后续加载的时候,浏览器会直接从命中缓存,不会重复下载,后续初始化就会很快。

同样也可以使用其他 CDN 或自建 CDN 代替,如: cdn.jsdelivr.net

引用 PGlite 插件

通常的,其他 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。

觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :

付费文章

友情链接


相关文章