多组件Canvas ID冲突解决方案
场景说明
该图片上传组件会自动给上传图片加水印。页面中只调用一次该组件则无任何异常。但同一页面中多次调用该组件,则会随机出现生成图片不完整现象。
代码展示
页面代码:
<template><view>//第一次调用组件<image-upload @media-updated="handleMediaUpdated1" />//第二次调用组件<image-upload @media-updated="handleMediaUpdated2" /></view>
</template>
<script>
import imageUpload from "@/components/imageUpload.vue";
export default {components: {imageUpload},data() {return {img1: [],img2: [],}},methods: {handleMediaUpdated1(mediaList) {this.img1 = mediaList},handleMediaUpdated2(mediaList) {this.img2 = mediaList},},
}
</script>
imageUpload组件代码:
<template><view><WatermarkComponent ref="watermark" /></view>
</template>
<script>import WatermarkComponent from '@/components/watermark.vue';export default {components: {WatermarkComponent},data() {return {};},methods: {chooseImages() {uni.chooseImage({count: 10,sourceType: ["album"],success: (res) => {const tempFilePaths = res.tempFilePaths;this.uploadImagesSequentially(tempFilePaths); // 依次上传图片},fail: (err) => {console.error("选择图片失败: ", err);},});},uploadImagesSequentially(tempFilePaths) {const uploadImage = (path) => {return new Promise((resolve, reject) => {// #ifdef APP-PLUSuni.compressImage({src: path,quality: 50,success: (res) => {this.addWaterMarkAndUpload(res.tempFilePath).then(resolve).catch(reject);},fail: () => {console.log("压缩失败");reject(new Error("压缩失败"));},});// #endif// #ifdef H5this.addWaterMarkAndUpload(path).then(resolve).catch(reject);// #endif});};tempFilePaths.reduce((promiseChain, currentFilePath) => {return promiseChain.then(() => uploadImage(currentFilePath));}, Promise.resolve()).then(() => {this.$refs.popupComponent.close(); // 所有图片上传完成后关闭弹出层}).catch((error) => {console.error('Error uploading images:', error);});},addWaterMarkAndUpload(filePath) {return this.$refs.watermark.callAddWaterMark(filePath).then((watermarkedFilePath) => {return new Promise((resolve, reject) => {// 调用上传文件的函数,传入加了水印的文件路径this.uploadFiles(watermarkedFilePath).then(resolve).catch(reject);});});},uploadFiles(files) {},},};
</script>
WatermarkComponent组件代码(修改前):
<template><div><view id="canvas"><canvas :style="{ width: canvasWidth, height: canvasHeight }" type="2d" canvas-id="myCanvas"></canvas></view></div>
</template><script>export default {data() {return {canvasWidth: '',canvasHeight: '',};},methods: {callAddWaterMark(tempFilePath) {uni.showLoading({title: '图片处理中'});return new Promise((resolve, reject) => {this.getCanvasSize(tempFilePath, (width, height) => {this.addWaterMark(tempFilePath, width, height, (watermarkedFilePath) => {uni.hideLoading();resolve(watermarkedFilePath);});});});},getCanvasSize(tempFilePath, callback) {uni.getImageInfo({src: tempFilePath,success: (res) => {const width = res.width;const height = res.height;this.canvasWidth = width / 3 + 'px';this.canvasHeight = height / 3 + 'px';callback(width, height);},});},addWaterMark(tempFilePath, width, height, callback) {// 获取当前年月日和时间var now = new Date();var year = now.getFullYear(); //得到年份var month = now.getMonth() + 1; //得到月份var date = now.getDate(); //得到日期var hours = now.getHours(); //得到小时var minutes = now.getMinutes(); //得到分钟var seconds = now.getSeconds(); //得到秒// 格式化日期和时间var curDate = `${year}/${month.toString().padStart(2, '0')}/${date.toString().padStart(2, '0')} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;setTimeout(() => {var ctx = uni.createCanvasContext('myCanvas');ctx.fillRect(0, 0, width / 3, height / 3);ctx.beginPath();ctx.drawImage(tempFilePath, 0, 0, width / 3, height / 3); // 设置绘制图片的宽高为图片的实际宽高let sizeBaseline = width > height ? width : height;let fontSize = sizeBaseline >= 1000 ? sizeBaseline * 0.01 : 10;ctx.font = `${fontSize}px sans-serif`; // 按比例设置字体ctx.setFillStyle('rgba(0,0,0,0.3)');ctx.fillRect(0, height / 3 - 30, 420, 40);ctx.setFillStyle('#ffffff');ctx.fillText('日期: ' + curDate, 0, height / 3 - 10, width / 3);ctx.draw(false, () => {uni.canvasToTempFilePath({canvasId: 'myCanvas',destWidth: width,destHeight: height,fileType: 'jpg',success: (res) => {callback(res.tempFilePath);},});});}, 1000);},},};
</script>
<style>#canvas {visibility: hidden;position: absolute;left: -9999px;}
</style>
问题原因分析
根据上述代码,问题在于多个水印组件实例共享了同一个Canvas ID,导致图片水印处理冲突。以下是详细分析和解决方案:
Canvas ID冲突:
在
WatermarkComponent
中,canvas-id
固定为"myCanvas"
当页面同时存在两个
imageUpload
组件时,会创建两个WatermarkComponent
实例两个实例尝试使用相同的Canvas ID,导致水印处理时互相覆盖
DOM操作冲突:
水印处理依赖隐藏的Canvas元素
多个同ID的Canvas同时存在时,水印操作会随机选择一个Canvas执行
结果:一个组件的图片可能被处理成另一个组件的水印样式
解决方案
修改 WatermarkComponent
,使其支持动态Canvas ID:
<template><div><view id="canvas"><!-- 关键修改:动态绑定canvas-id --><canvas :canvas-id="canvasId" :style="{ width: canvasWidth, height: canvasHeight }" type="2d"></canvas></view></div>
</template><script>
export default {data() {return {canvasId: '', // 动态IDcanvasWidth: '',canvasHeight: '',};},mounted() {// 生成唯一Canvas ID(组件挂载时创建)this.canvasId = `watermark_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`;},methods: {callAddWaterMark(tempFilePath) {// ...原有逻辑不变,但修改以下两处...},addWaterMark(tempFilePath, width, height, callback) {setTimeout(() => {// 1. 创建上下文时使用动态IDvar ctx = uni.createCanvasContext(this.canvasId, this); // 添加this参数// ...中间绘制逻辑不变...ctx.draw(false, () => {uni.canvasToTempFilePath({// 2. 导出时使用动态IDcanvasId: this.canvasId, // 修改这里destWidth: width,destHeight: height,fileType: 'jpg',success: (res) => {callback(res.tempFilePath);},}, this); // 添加this参数});}, 1000);}}
};
</script>
<style>/* 确保每个水印组件独立定位 */#canvas {position: fixed;top: -10000px; /* 替代left:-9999px避免滚动条问题 */z-index: -1;opacity: 0;}
</style>
为什么单实例正常?
单个组件实例 => 唯一Canvas ID => 水印处理正常
多个实例 => Canvas ID冲突 => 水印处理随机覆盖