前情提要:项目采用 vue3 + element-plus 的结构
需求:在背景图上画出红色矩形,圈出重点,格式类似下面
解决卡顿方法:
1.图片懒加载(很重要)
2.图片本地缓存(比较重要)
3.创建web worker子线程(效果不佳)
思路存在,开始写代码
1.创建每一组图片的 ImageGroup.vue
<template>
<div>
<div style="display: flex;">
<div style="flex: 1;">
<el-image
style="width: 100%; height: 107px"
:src="url1"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="srcList"
:initial-index="0"
fit="cover"
lazy
@load="onload"/>
<div>标准图</div>
</div>
<div style="flex: 1;">
<el-image
style="width: 100%; height: 107px"
:src="url2"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="srcList"
:initial-index="1"
fit="cover"
lazy>
<template #error>
<div>加载中</div>
</template>
</el-image>
<div>canvas图</div>
</div>
</div>
</div>
</template>
js代码
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useImageStore } from '@/stores/image' // 图片缓存
const imageStore = useImageStore()
const info = ref({}) // 每一组图片的信息,包含id,图片url,和矩形四个角在图片上的绘制位置等(这里是子组件,应该从父组件获取)
const url1 = ref('') // 标准图的url
const url2 = ref('') // canvas图的url
const srcList = computed(() => { // 图片预览列表
return [url1.value, url2.value]
})
const bgUrl = ref('') // 图二的背景图
const pointList = ref([]) // 红框的点位信息
let worker // web worker子线程
const createWorker = () => { // 创建web worker子线程
if (!worker) {
worker = new Worker('/apple/worker/worker.js') // 这里的路径要注意,和我下面的图对照一下
}
return worker
}
// 1.首先加载标准图
onMounted(() => {
let images = info.value.image
url1.value = images[0].url
})
// 2.(el-image的加载事件调用,这里取个巧,用element的懒加载功能带动canvas的懒加载)标准图加载,用于懒加载实景图
function onload() {
let images = info.value.image
if(info.value.pointList) { // 有标记红框的图片,才需要画图(为了节省性能)
pointList.value = info.value.pointList // 设置点位信息
bgUrl.value = images[1].url // 设置背景图
// 获取图片缓存,有缓存的直接显示,无缓存的则生成base64(为了节省性能)
const img = imageStore.findImage(info.value.id)
if(img) {
url2.value = img.url
} else {
generateBase64()
}
} else { // 没有标记红框的图片,直接显示,不进行画图
url2.value = images[1].url
}
}
// 3.生成图片base64
const generateBase64 = async () => {
const worker = createWorker() // 创建web worker子线程
worker.onmessage = (event) => { // 接收子线程返回的base64数据
if (event.data.type === "toDataURL") {
url2.value = event.data.img
// 缓存图片,主键为id
imageStore.addImageList({id: info.value.id, url: event.data.img})
worker.terminate() // 结束子线程
}
}
worker.onerror = (error) => {
console.log(error)
}
// 将canvas数据发送给Web Worker
worker.postMessage({ type: 'toDataURL', pointList: JSON.stringify(pointList.value), bgUrl: bgUrl.value })
}
</script>
2.创建本地缓存,目的是尽量减少canvas绘画次数
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useImageStore = defineStore('image', () => {
const imageList = ref([])
function addImageList(image) {
if(!imageList.value.filter((item) => item.id === image.id)[0])
imageList.value.push(image)
}
function resetImageList() {
imageList.value = []
}
function findImage(id) {
return imageList.value.filter((item) => item.id === id)[0]
}
return {
imageList,
addImageList,
resetImageList,
findImage
}
})
3.web woker代码(在这里用canvas比较麻烦)
onmessage = async function(event) {
// 接收到消息时开始处理数据
const { type, pointList, bgUrl } = event.data;
const points = JSON.parse(pointList)
// 获取背景图信息
const response = await fetch(bgUrl) // 获取图片
const blob = await response.blob() // 获取blob对象
const image = await createImageBitmap(blob) // 创建一个ImageBitmap对象
if (type !== 'toDataURL') return
const canvas = new OffscreenCanvas(100, 1) // 子线程中获取不到实体canvas,只能用OffscreenCanvas了
const ctx = canvas.getContext('2d')
canvas.width = image.width
canvas.height = image.height
ctx.drawImage(image, 0, 0)
points.forEach(item => {
const [x,y,w,h] = item[0]
ctx.strokeStyle = "rgb(255, 0, 0)"
ctx.lineWidth = 2
ctx.strokeRect(Number(x), Number(y), Number(w), Number(h))
})
const bloba = await canvas.convertToBlob() // OffscreenCanvas没有转换base64的方法,只能先转换成blob
// 然后创建一个文件读取器
let reader = new FileReader()
reader.onload = () => {
let data = reader.result
postMessage({ type, img: data })
}
// 读取blob对象为DataURL
reader.readAsDataURL(bloba)
}
总结:懒加载是很有必要的,本地缓存效果也不错,web woker子线程只是提供了一种优化思路,但是效果聊胜于无,感兴趣的可以试一下。