前言
这又是一次被小程序折磨的故事!距我上一次被小程序折磨还是在上一次(去年9月做微信头像加国旗小程序的时候)。但这是我第二次做小程序,就让我再次遇到这么个大难题,就是在小程序中用 WASM,准确来讲是集成一个第三方 npm 包@jsquash/jpeg
,而这个依赖包是一个基于 WebAssembly ,支持浏览器端运行的第三方 JPEG 编解码库。
业务&背景
这次我开发的小程序是一个图像压缩工具------图像压缩暗房
,它不仅能够完成图像压缩,还能够缩小图像尺寸、进行格式转换。当然,这其中最重要的一点是所有功能完完全全的本地运行。因为没钱买服务器,也才如此困难重重,有钱还是放服务端运行吧。
如果有服务器,可以搭建 NodeJS 环境用shrap
,而如果要在浏览器环境实现则需要自己找其它的第三方依赖了,比如@jsquash/jpeg
,它是jSquash
中的一个包,而jSquash
则是基于谷歌的Squoosh
开源项目而来。因此我的小程序初期就直接尝试用@jsquash/jpeg
来实现功能了。但从一开始就是重重磨难。所遇到的问题搜遍全网无一案例。
第一难:引入@jquash/jpeg
首先来安装@jsquash/jpeg
shell
npm install @jsquash/jpeg
之后在小程序中构建npm
,到这里没有任何问题。
接着根据官方文档的示例引入@jquash/jpeg
javascript
import decode, { init as initJpegDecode } from '@jsquash/jpeg/decode';
import encode, { init as initJpegEncode } from '@jsquash/jpeg/encode';
这时就报错了:模块没有被定义?这看得我很奇怪,根据这个信息,必然是微信在构建 npm 的时候出了差错(当然这是我排查了半天后得出的结论)。

好,来对比一下node_modules
中的源码和miniprogram_npm
,发现是因为encode.js
、decode.js
都没有被编译。(实际这一看就只有2个文件躺在miniprogram_npm
里,匪夷所思)
在编译后的index.js
最底部有注释,即这些文件被认为外部依赖,所以没被编译。
而为什么没有被编译,后面会提到。
javascript
//miniprogram-npm-outsideDeps=["./codec/enc/mozjpeg_enc.js","./codec/dec/mozjpeg_dec.js"]
//# sourceMappingURL=index.js.map
此时需要手动将源码中的相关的文件和整个codec
复制到miniprogram_npm
。
在@jsquash/jpeg
中 REAMDE 也有提到:
Note: You will need to either manually include the wasm files from the codec directory or use a bundler like WebPack or Rollup to include them in your app/server.
第二难:禁用import.meta
接着前面,清除小程序缓存并重新编译,依然报错,这次的错误是不能在模块外使用import.meta
。
这个错误在miniprogram_npm/@jsquash/jpeg/codec/enc/mozjpeg_enc.js
实际上这就是codec
中的 JS 文件没被构建编译的原因。在小程序中不能使用的东西,就直接不给编译了(悄悄地啥都不说,让你自己找)。

看一下源码中import.meta.url
的作用,其实就是动态确定脚本文件路径。
直接把文件中所有的import.meta.url
修改为实际值:
javascript
// var _scriptDir = import.meta.url;
var _scriptDir = '/miniprogram_npm/@jsquash/jpeg/codec/enc/mozjpeg_enc.js';
用不到的就注释:
javascript
/*if(import.meta.url===undefined){import.meta.url="https://localhost"}*/
下面这里没有改成/miniprogram_npm/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm
,而是把.wasm
文件另外放到项目根目录wasm
中。这是因为小程序限制禁止访问miniprogram_npm
,后面会遇到。
javascript
// wasmBinaryFile=new URL("mozjpeg_enc.wasm",import.meta.url).href
wasmBinaryFile="/wasm/mozjpeg_enc.wasm"
之后,miniprogram_npm/@jsquash/jpeg/codec/dec/mozjpeg_dec.js
也一样修改。
第三难:禁用WebAssembly
接着就需要初始化@jsquash/jpeg
,也就是加载 WASM。
javascript
initJpegEncode('/miniprogram_npm/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm')
这时遇到了WebAssembly
没有被定义的报错。

定位一下报错的代码位置,在initEmscriptenModule
方法中:
javascript
export function initEmscriptenModule(moduleFactory, wasmModule, moduleOptionOverrides = {}) {
let instantiateWasm;
if (wasmModule) {
instantiateWasm = (imports, callback) => {
const instance = new WebAssembly.Instance(wasmModule, imports);
callback(instance);
return instance.exports;
};
}
return moduleFactory({
// Just to be safe, don't automatically invoke any wasm functions
noInitialRun: true,
instantiateWasm,
...moduleOptionOverrides,
});
}
经过排查,这是由于微信小程序自己定义了一个WXWebAssembly
(这也要干预?虽然不明白但感觉手脚不是很干净,改了参数但是其他啥也没讲,连个例子都没有)。

首先,这里的重点是WXWebAssembly.instantiate
的path
指向一个代码包内路径的.wasm
文件。也就是前面提到的.wasm
文件要单独放到项目根目录wasm
中。放在miniprogram_npm
是没用的,因为小程序限制禁止访问。
↓你敢尝试就敢给你报错

修改一下初始化方法中的代码,用微信的WXWebAssembly.instantiate
。
javascript
export function initEmscriptenModule(moduleFactory, wasmModule, moduleOptionOverrides = {}) {
let instantiateWasm;
if (wasmModule) {
instantiateWasm = async (imports, callback) => {
const result = await WXWebAssembly.instantiate(wasmModule, imports);
const instance = result.instance;
callback(instance);
return instance.exports;
};
}
return moduleFactory({
// Just to be safe, don't automatically invoke any wasm functions
noInitialRun: true,
instantiateWasm,
...moduleOptionOverrides,
});
}
当然,这还不够。因为胶水文件mozjpeg_enc.js
中还有很多原生使用WebAssembly
对象的地方。我这里直接取巧全局替换了。
javascript
var WebAssembly = WXWebAssembly;
// var _scriptDir = import.meta.url;
var _scriptDir = '/miniprogram_npm/@jsquash/jpeg/codec/enc/mozjpeg_enc.js';
最后,我们的初始化入口方法参数直接传.wasm
文件的代码包内路径。
javascript
initJpegEncode('/wasm/mozjpeg_enc.wasm')
后续
突破三重难关,总算是能够使用@jquash/jpeg
了。然而实际用下来发现,@jquash/jpeg
的图片压缩效率实在是太低了,竟然要几十秒,这与我预想中的相差太远了,也是用户所不能容忍的。我本以为是哪里的参数设置不对,但是在谷歌官方的Squoosh
应用中发现也是如此。因此我直接改用Canvas
实现jpeg
图片的处理了,而其他的gif
、png
格式处理则使用另外的wasm
脚本,后面有机会再另讲一篇。
小程序展示
最后展示一下我这个"图像压缩暗房"小程序,设计都是我自己。