💐 前言
这是继上一篇《全网都找不到!小程序集成第三方 WASM npm 包》后的第二篇。在上一篇中讲述了小程序集成第三方 npm 包@jsquash/jpeg
实现 JPEG 图片压缩,而这一次则是探讨小程序中如何集成并实现 GIF 图片压缩。这一系列的共同之处都是小程序集成使用 WASM。使用 WASM 并不是很难的一件事,因为有很多开源的项目可供参考。真正难的是在小程序这个平台下如何使用,这也是我写这一篇所要分享的内容,这是在全网都找不到的。才疏学浅,也因此希望能够抛砖引玉。
🌈 需求&选型
前段时间我开发了一个图像压缩小程序------图像压缩暗房
,它不仅能够完成图像压缩,还能够缩小图像尺寸、进行格式转换。当然,这其中最重要的一点是所有功能完完全全的本地运行。
↓这是图像压缩暗房
的界面,包括设计也都是我独立完成的。
回到需求背景中来,就是我的这个小程序需要实现 GIF 压缩。
那么先来分析一下,可以实现的路径:
- 服务端,搭建 NodeJS 环境用
shrap
完成 - 客户端,使用支持浏览器运行的 WebAssembly 技术实现
因为我买不起服务器所以只能选择使用本地 WASM 技术实现。
接下来就是要找一个适合的第三方 WASM 包,这个包必须满足几点:
- 能够完成 GIF 压缩、尺寸调整
- 基于 WebAssembly 技术开发
- 支持浏览器环境运行
经过一番调研,我选择了gifsicle-wasm-browser
,之后又从一个shot-easy-website
开源项目中参考如何使用gifsicle-wasm-browser
。
🍭集成 gifsicle wasm 的各种坑
坑1:小程序只允许引入JS文件
首先在项目根目录新建一个wasm
目录,用来放 gifsicle 的.wasm
文件和胶水.js
文件。

接下来在业务需要用到的地方引入GifWasmModule.js
。
javascript
import { gifsicle } from "../wasm/GifWasmModule";
此时会报错。

看一下报错源码位置,这个问题原因在于小程序中只允许引入 JS 文件。
javascript
import gifWasmBinaryFile from "./gif.wasm?url";
export const gifsicle = (function () {
let wasmUrl = gifWasmBinaryFile;
//...
}
找一下用到gifWasmBinaryFile
的地方,直接修改为绝对路径,也就是小程序要求的"代码包内路径"。
javascript
// import gifWasmBinaryFile from "./gif.wasm?url";
export const gifsicle = (function () {
let wasmUrl = "/wasm/gif.wasm";
//...
}
坑2:小程序有自己的WXWebAssembly
继续重新编译,又有新的问题,说是不支持 WASM。

看一下打印这句警告的位置,发现是 WebAssembly 这个对象判断出了问题。

这是因为小程序自己定义了一个WXWebAssembly
。

直接在最顶部声明,让WebAssembly
指向WXWebAssembly
。
javascript
var WebAssembly = WXWebAssembly;
坑3:小程序只能加载本地 wasm
重新编译,还是有问题。
WebAssembly.RuntimeError is not a constructor
,看字面意思是小程序的WXWebAssembly
的RuntimeError
没有提供构造函数。这里不用管因为有实际抛出异常的代码才会走到这里。

从抛出异常的调用栈中定位问题。

报错在getBinary()
方法里,很明显就是获取 WASM 二进制文件的。通常获取 WASM 二进制文件要么通过网络请求获取,要么就加载本地的。
javascript
function getBinary() {
try {
if (wasmBinary) {
return new Uint8Array(wasmBinary);
}
if (readBinary) {
return readBinary(wasmBinaryFile);
} else {
throw "both async and sync fetching of the wasm failed";
}
} catch (err) {
abort(err);
}
}
直接查找WebAssembly.instantiate
的所在方法,因为在小程序中只能通过WebAssembly.instantiate
方法完成加载本地 wasm。
从这里的方法看,结合上面的异常调用栈,可以判断是在getBinaryPromise()
这里面抛出异常,所以没走到WebAssembly.instantiate(binary, info)
。
javascript
function instantiateArrayBuffer(receiver) {
return getBinaryPromise()
.then(function (binary) {
return WebAssembly.instantiate(binary, info);
})
.then(receiver, function (reason) {
err("failed to asynchronously prepare wasm: " + reason);
abort(reason);
});
}
解决方法就是跳过getBinaryPromise()
,直接调用WebAssembly.instantiate
。
因为我们前面声明了var WebAssembly = WXWebAssembly
,所以这里实际就是WXWebAssembly.instantiate
,第一个参数要传代码包内路径的.wasm
文件。
javascript
function instantiateArrayBuffer(receiver) {
// return getBinaryPromise()
// .then(function (binary) {
// return WebAssembly.instantiate(binary, info);
// })
return WXWebAssembly.instantiate('/wasm/gif.wasm', info)
.then(receiver, function (reason) {
err("failed to asynchronously prepare wasm: " + reason);
abort(reason);
});
}
坑3:小程序真机调试才告诉你bug!
这是小程序开发中历史悠久的一个大烂坑!真机调试了才告诉你出了哪些 bug。
这里的报错是说"是个终端"?挺奇怪的。应该说程序判断现在处于一个终端环境,但正确情况下我们是在 Web 环境。

定位到4146
行,由于报错信息有限也没有调用栈,只能看出来是gifsicle_c
函数中报错。

通过调试,最终发现确实是gifsicle_c
函数中环境的判断出现错误。在小程序中是没有window
这个对象的。
javascript
ENVIRONMENT_IS_WEB = typeof window === "object"
修改一下这个判断,让它能够支持小程序环境。
javascript
ENVIRONMENT_IS_WEB = typeof window === "object" || typeof wx === "object";
至此,就成功在小程序中集成 gifsicle wasm 了。
🎄结语
终于,又一次完成了在小程序集成 wasm。通过这几次集成@jsquash/jpeg
,gifsicle-wasm-browser
的历经过程来归纳出一些要点:
- 将
node_modules
源码文件拷贝到miniprogram_npm
- 将源码中的
.wasm
和相应的胶水.js
放到项目根目录的wasm
中 - 胶水文件中不能直接引入
.wasm
、不能使用import.meta.url
动态获取文件路径 - 原生的
WebAssembly
对象要改用WXWebAssembly
- 加载
.wasm
只能使用WXWebAssembly.instantiate
方法 - 检查输入输出的环境判断,在小程序中也属于 Web 环境