记录下最近遇到的需求:加载三四百张图片,压缩后再展示给用户。最终方案是用worker多线程进行压缩,这里记录下踩过的坑。
大概最终实现效果:未压缩体积297MB,压缩后体积21MB。
压缩图片方法
// vue
toCompressedPic(url) {
const img = new Image()
// 允许图片资源跨域
img.crossOrigin = 'Anonymous'
img.src = url
img.onload = () => {
// 绘制在canvas上
const canvas = new OffscreenCanvas(img.width, img.height)
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
let quality = 0.8
const maxSize = 100 * 1024
const cb = (blob) => {
quality -= 0.1
if (blob.size > maxSize && quality > 0.1) {
canvas.convertToBlob(cb, 'image/jpeg', quality)
} else {
// 输出结果
this.src = URL.createObjectURL(blob)
}
}
canvas.convertToBlob(cb, 'image/jpeg', quality)
}
}
组件内部压缩
组件加载完图片调用方法压缩,用时约52S,但页面会有略微卡顿。
Vuex单线程压缩
为了方便统一处理,存储在vuex数组中,轮询进行压缩,压缩完成就调用回调函数将数据返回给原组件。
// vue
this.$store.dispatch('toCompressedPic', {
url: this.url,
cb: (url) => {
this.src = url
}
})
用时约74S,无卡顿。
多worker线程池压缩图片:
还是统一存储在Vuex中。但初次使用worker,遇到了一些问题:
worker请求JS文件存在跨域
解决办法:本地Node提供静态资源并设置请求头允许跨域。
// node托管静态文件
var express = require('express');
var fs = require('fs');
var app = express();
app.use('/public',function(req,res) {
fs.readFile('./public/'+req.url , 'utf-8' , function(err,data) {
if (err) {
res.send(err)
}else{
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(data)
}
})
})
app.listen(3000,function () {
console.log("启动成功, http://localhost/3000")
})
worker线程无法使用Document
解决办法:OffscreenCanvas 和 fetch请求图片资源。
图片资源存在跨域问题,无法在主线程和worker线程间传递
解决办法:worker线程自己调用fetch请求图片。
// worker
self.onmessage = async (e) => {
const response = await fetch(e.data);
const blob = await response.blob();
createImageBitmap(blob).then(imageBitmap => {
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height)
let ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0)
let quality = 0.8
const maxSize = 100 * 1024
const cb = (blob) => {
quality -= 0.1
if (blob.size > maxSize && quality > 0.1) {
canvas.convertToBlob({ type: 'image/jpeg', quality: quality }).then(cb)
} else {
self.postMessage(URL.createObjectURL(blob))
}
}
canvas.convertToBlob({ type: 'image/jpeg', quality: quality }).then(cb)
})
}
开启一个8核的线程池
fetch(//远程地址)
.then(response => response.text())
.then(scriptText => {
const blob = new Blob([scriptText], { type: 'application/javascript' })
const blobUrl = URL.createObjectURL(blob)
for (let index = 0; index < 8; index++) {
const worker = new Worker(blobUrl)
pool.push({
workerId: index,
worker,
status: 'free' // free | busy
})
}
})
查询是否有空闲线程
// 获取当前闲置(free)的线程,如果都在busy,则等到100ms再试一次
async function findFreePool() {
while (true) {
const poolItem = pool.find(item => item.status === 'free') // 找到一个可用线程
if (poolItem) {
return poolItem
} else {
await timeOut(100)
}
}
}
function timeOut(s) {
return new Promise(r => setTimeout(r, s))
}
最终效果:用时约26S,无卡顿。