本文讲介绍如何在wxt框架中使用wasm库。

WXT:Next-gen Web Extension Framework

WXT 是一个基于 Vite 的 Web 浏览器插件框架,旨在为开发者提供一个简单、高效、灵活的开发环境。 大概2023发布第一版,github start 数量已经有5.7k了(截止2025/2/21)。这是我第一次开发浏览器插件,便选择用这个框架试试水。 我开发的时候使用了一个wxt模板,同时支持tailwind,shadcn,多语言。个人感觉不错,欢迎大家尝试。

项目背景

我打算使用开发一个图片编辑插件。由于之前有一定的rust使用经验,我便把眼光又投向了基于rust开发,然后编译为wasm的图片库。想用来提高 性能(逼格)。 我有使用到两个wasm库,分别是:

  • phonton 一个高性能的图片处理库
  • image 一个支持各种图片编码解码,和一定图片处理能力的库

这两个库我都是使用的官方的npm包,而不是自己构建的包。他们都是使用wasm-pack 打包,不过参数有所不同,这也导致了两者的所需要的支持方式有所区别。

wasm-pack 是如何给rust仓库打包的?

wasm-pack 是一个用于将 Rust 编译为 WebAssembly 的工具,类似于Emscripten之于C/C++。 wasmpack 在打包时,有一个可选项是--target 通过这个选项可以指定不同的bundler

OptionUsageDescription
not specified or bundlerBundler输出适合与像Webpack这样的Bundler进行交互的JS代码。您将导入JS代码,并在package.json中指定模块键。默认情况下,sideEffects为false。
nodejsNode.js输出使用CommonJS模块的JS代码,用于与require语句一起使用。package.json中的main键。
webNative in browser输出可以在浏览器中作为ES模块原生导入的JS代码,但必须手动实例化和加载WebAssembly。
no-modulesNative in browser与web相同,除了JS代码被包含在页面上并修改全局状态,不支持像web那样多的wasm-bindgen特性。
denoDeno输出可以在deno中作为ES模块原生导入的JS代码。

在photon中,根据官方文档描述使用wasm-pack build ./crate,没有指定bundler,而image中使用[wasm-pack build --target web]指定了target为web(通过github代码的实例推断)。 这两种打包方式在我实际使用时的区别就是,没有指定target时,wasm默认在import库的时候就会初始化,并且是顶层的异步初始化,而就是这个顶层的异步初始化导致一些列的编译失败ERROR Module format "iife" does not support top-level await. Use the "es" or "system" output formats rather.。而指定target为web时,wasm的初始化是在页面加载完成之后,在代码中使用前手动初始化。

// no target
import {funcA} from 'photon'
funcA(...)

// with target=web
import initWasmMoudle, {funcB} from 'image'
async function init() {
  await initWasmMoudle()
  funcB(...)
}

针对这两种情况如何解决

我在wxt 中,是background脚本需要使用wasm,所以background脚本配置为例,其余的如content script 没尝试过,估计差不多。

没有指定target

没有指定target的情况下需要配置两个地方:

  1. wxt.config.ts中配置两个插件,分别是
    1. vite-plugin-wasm
    2. vite-plugin-top-level-await
        import {defineConfig} from 'wxt';
        import react from '@vitejs/plugin-react';
        import wasm from "vite-plugin-wasm";
        import topLevelAwait from "vite-plugin-top-level-await";
    
        export default defineConfig({
        manifest: {
            // ...
        },
        vite: () => ({
                plugins: [react(), wasm(), topLevelAwait()],
            }),
        });
    
    
  2. 在 background脚本配置 type 类型为 module
    export default defineBackground({
    // Set manifest options
    type: 'module',
    
    //非异步函数
    main() {
        // 你的函数功能实现
    }
        
    });
    

指定target为web

这种情况需要额外增加wxt的module来辅助打包,wxt module 有点类似于打包时提供给用户的hook,方便执行一些你们自己的操作。而我们这里要做的时,把node modules目录下的wasm 文件拷贝到我们wxt的编译输出目录下,这点 官方文档已有说明。下面已我项目为例: 目录结构大概如下

目录组织
import { resolve } from 'node:path';
import { defineWxtModule } from 'wxt/modules';

export default defineWxtModule((wxt) => {
  wxt.hook('build:publicAssets', (_, assets) => {
    assets.push({
      absoluteSrc: resolve(
        'node_modules/@refilelabs/image/image_bg.wasm',
      ),
      relativeDest: 'image_bg.wasm',
    });
  });
});

最后还要把resurce 限制给加上

export default defineConfig({
    manifest: {
        
        web_accessible_resources: [
            {
              // We'll use this matches in the content script as well
              matches: ['*://*/*'],
              // Use the same path as `relativeDest` from the WXT module
              resources: ['/image_bg.wasm'],
            },
          ],
    },
    vite: () => ({
        plugins: [react(), wasm(), topLevelAwait()],
    }),
});

其他非rust编译的wasm库

这些库我没有尝试过,但方法估计也差不多,欢迎评论区留言!