From fa905519f629ec014791f19a752186951df62a3b Mon Sep 17 00:00:00 2001 From: liangxu Date: Tue, 28 Apr 2026 18:35:46 +0800 Subject: [PATCH] fix(admin-web/image-webp): bundle jsquash wasm via vite resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite 在生产构建里没法解析 @jsquash/webp 自带的相对 wasm 路径,导致 WebP 编码 首次调用就失败。改成直接 import encode.js + ?url 的 wasm 资源,并在 vite.config 里把 specifier 显式 alias 到 node_modules 的真实文件,运行前再调用 init({locateFile}) 告诉 wasm 加载器去拿打包后的 URL。 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/admin-web/src/lib/image-webp.ts | 29 ++++++++++++++- apps/admin-web/src/types/jsquash-webp.d.ts | 42 ++++++++++++++++++++++ apps/admin-web/vite.config.ts | 6 ++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 apps/admin-web/src/types/jsquash-webp.d.ts diff --git a/apps/admin-web/src/lib/image-webp.ts b/apps/admin-web/src/lib/image-webp.ts index 415afab..80c4a9a 100644 --- a/apps/admin-web/src/lib/image-webp.ts +++ b/apps/admin-web/src/lib/image-webp.ts @@ -1,4 +1,6 @@ -import { encode } from "@jsquash/webp"; +import encode, { init as initWebpEncoder } from "@jsquash/webp/encode.js"; +import webpEncoderWasmUrl from "@jsquash/webp/codec/enc/webp_enc.wasm?url"; +import webpEncoderSimdWasmUrl from "@jsquash/webp/codec/enc/webp_enc_simd.wasm?url"; import { reactive, readonly } from "vue"; // upload limit set to 10MB @@ -54,6 +56,30 @@ const imageUploadProgressState = reactive({ }); let activeProgressToken = 0; +let webpEncoderReady: Promise | null = null; + +function ensureWebpEncoderReady(): Promise { + if (webpEncoderReady) { + return webpEncoderReady; + } + + const ready = initWebpEncoder({ + locateFile(path: string) { + if (path === "webp_enc_simd.wasm") { + return webpEncoderSimdWasmUrl; + } + if (path === "webp_enc.wasm") { + return webpEncoderWasmUrl; + } + return path; + }, + }).catch((error: unknown) => { + webpEncoderReady = null; + throw error; + }); + webpEncoderReady = ready; + return ready; +} function ensureWebpFileName(fileName: string, fallbackBaseName = "image"): string { const trimmed = fileName.trim(); @@ -200,6 +226,7 @@ export async function convertImageFileToWebp(file: File): Promise { const imageData = await createImageDataFromBlob(file, token); updateImageUploadProgress(token, { percent: 72, stage: "encoding" }); + await ensureWebpEncoderReady(); const encoded = await encode(imageData, { quality: 82 }); updateImageUploadProgress(token, { percent: 88, stage: "finishing" }); diff --git a/apps/admin-web/src/types/jsquash-webp.d.ts b/apps/admin-web/src/types/jsquash-webp.d.ts new file mode 100644 index 0000000..db05642 --- /dev/null +++ b/apps/admin-web/src/types/jsquash-webp.d.ts @@ -0,0 +1,42 @@ +declare module "@jsquash/webp/encode.js" { + type WebpEncodeOptions = { + quality: number; + target_size: number; + target_PSNR: number; + method: number; + sns_strength: number; + filter_strength: number; + filter_sharpness: number; + filter_type: number; + partitions: number; + segments: number; + pass: number; + show_compressed: number; + preprocessing: number; + autofilter: number; + partition_limit: number; + alpha_compression: number; + alpha_filtering: number; + alpha_quality: number; + lossless: number; + exact: number; + image_hint: number; + emulate_jpeg_size: number; + thread_level: number; + low_memory: number; + near_lossless: number; + use_delta_palette: number; + use_sharp_yuv: number; + }; + + type WebpEncoderInitOptions = { + locateFile?: (path: string, scriptDirectory: string) => string; + }; + + export function init(moduleOptionOverrides?: WebpEncoderInitOptions): Promise; + + export default function encode( + data: ImageData, + options?: Partial, + ): Promise; +} diff --git a/apps/admin-web/vite.config.ts b/apps/admin-web/vite.config.ts index edd27de..e7b1db7 100644 --- a/apps/admin-web/vite.config.ts +++ b/apps/admin-web/vite.config.ts @@ -1,3 +1,4 @@ +import { createRequire } from "node:module"; import { fileURLToPath, URL } from "node:url"; import vue from "@vitejs/plugin-vue"; @@ -6,6 +7,8 @@ import { defineConfig, loadEnv, type PluginOption } from "vite"; import { compression } from "vite-plugin-compression2"; const r = (path: string) => fileURLToPath(new URL(path, import.meta.url)); +const nodeRequire = createRequire(import.meta.url); +const resolveNodeModule = (specifier: string) => nodeRequire.resolve(specifier); export default defineConfig(({ mode }) => { const env = loadEnv(mode, ".", ""); @@ -42,6 +45,9 @@ export default defineConfig(({ mode }) => { "@": r("./src"), "@geo/shared-types": r("../../packages/shared-types/src/index.ts"), "@geo/http-client": r("../../packages/http-client/src/index.ts"), + "@jsquash/webp/encode.js": resolveNodeModule("@jsquash/webp/encode.js"), + "@jsquash/webp/codec/enc/webp_enc.wasm": resolveNodeModule("@jsquash/webp/codec/enc/webp_enc.wasm"), + "@jsquash/webp/codec/enc/webp_enc_simd.wasm": resolveNodeModule("@jsquash/webp/codec/enc/webp_enc_simd.wasm"), }, }, server: {