这是一个很普通的需求,但是没想到让我遇到了一个大坑(后面细说),卡了我整整一天的进度。
一开始想着用HBX的海报插件来实现,然后发现这些海报插件,要么是不适配vue3,要么是可定制性不够高,只能按照它写好的模板进行替换,再或者就是要去其他网站用在线海报编辑器编辑好了之后,再生成配置文件,有点麻烦。简单的说就是,要么用不了,要么不好用。
于是便想着通过html2canvas来自己实现。
新建一个组件.vue文件,然后在里面搭建页面结构,引入几张图片,得到下图。
页面结构如下:
npmihtml2canvas--save因为html2canvas要操作DOM元素,因此这里我们要用到renderjs,根据uni官方文档的说明,我们在文件中新增一对script标签,如下:
此时我们组件中存在两个script标签,如下:
除此之外,renderjs还有挺多注意事项的,建议大家遇到问题了先去官方文档中查看这些注意事项,比如说:
我们在视图层引入html2canvas,然后在其methods属性中定义一个toImage异步方法,该方法代码如下:
asynctoImage(){ try{ consttimeout=setTimeout(async()=>{ consthtmlCanvas=document.getElementById('htmlCanvas') constcanvas=awaithtml2canvas(htmlCanvas,{ backgroundColor:null, allowTaint:true, useCORS:true }) constbase64Data=canvas.toDataURL('image/png') this.$ownerInstance.callMethod('creates',base64Data) clearTimeout(timeout) },500); }catch(e){ console.log(e); }}图片跨域toImage的定时器一定要加上的,不然的话会有问题,然后就是获取DOM,使用html2canvas,然后通过toDataURL获取图片的base64数据,一切都好像很顺利,直到我运行的时候,控制台出现这行红字:
Uncaught(inpromise)SecurityError:Failedtoexecute'toDataURL'on'HTMLCanvasElement':Taintedcanvasesmaynotbeexported.atapp-renderjs.js:7906简单的说,就是canvas引入的图片跨域了,导致画布被污染,所以不能使用toDataURL这个方法导出数据了,于是我赶紧查看html2canvas的文档,发现有两个配置项与这个有关:
我将这俩配置项都设为true之后,再运行,哦豁,还是同样的错误。
赶紧上网咨询广大网友,得到的方法是:
首先第一个方法,不是很ok,因为后端比较忙,而且就3张图片,我也实在是懒得叫。然后第二个方法,好像可以,但是又瞬间清醒,我用的是image标签,不是img标签,在移动端使用img标签会不显示图片的,最后抱着希望跑去uni官网看看image标签会不会有这个属性:
很可惜,没有。
难道真的要去麻烦后端了吗!不不不,要不咋说网友是万能的呢,翻了十几页搜索结果之后,终于在一篇帖子中找到了,这位仁兄也遇到了同样的困境,canvas跨域,然后没有后端帮忙,他给出的解决方案如下:
二话不说,我直接就跑去把用到的图片转成了base64,然后封装成组件:
然后重写改写模板结构:
这个方法的逻辑很简单,将base64数据转换成tempFilePath,然后调用uni.saveImageToPhotosAlbum将图片保存到用户相册即可。
如何将base64数据转换成tempFilePath呢,网上教的都是转成blob,但是这在uniapp的移动端中不适用啊,网上搜不到,我就去插件库中抄一下,看看它们是怎么实现的,嘿,当我打开LimeEchart的目录的时候,很快就让我在一个utils.js文件中翻到了一个名为base64ToPath的工具函数,啥也不说,直接复制:
/***base64转filePath*@param{Object}base64*/exportfunctionbase64ToPath(base64){ returnnewPromise((resolve,reject)=>{ const[,format,bodyData]=/data:image\/(\w+);base64,(.*)/.exec(base64)||[]; constbitmap=newplus.nativeObj.Bitmap('bitmap'+Date.now()) bitmap.loadBase64Data(base64,()=>{ if(!format){ reject(newError('ERROR_BASE64SRC_PARSE')) } consttime=newDate().getTime(); constfilePath=`_doc/uniapp_temp/${time}.${format}` bitmap.save(filePath,{}, ()=>{ bitmap.clear() resolve(filePath) }, (error)=>{ bitmap.clear() console.error(`${JSON.stringify(error)}`) reject(error) }) },(error)=>{ bitmap.clear() console.error(`${JSON.stringify(error)}`) reject(error) }) })}然后就可以接着实现creates方法了:
creates(base64Data){ uni.showLoading({ title:'下载中', }) base64ToPath(base64Data).then(filePath=>{ uni.saveImageToPhotosAlbum({ filePath, success:()=>{ uni.hideLoading() uni.showToast({ title:'图片下载成功', mask:false, duration:1500 }); } }); })}到这里,基本上没有问题了,然后我就开了模拟器,进行了一波真机运行,确实很ok。
本次遇到最大的坑来了,上面是我在一个demo项目中实现的(编译快),然后我将其移动到正式项目中去。
于是乎,我给下载图片按钮绑定了一个逻辑层的方法,然后想这个方法中调用视图层的方法:
为了弄明白到底是什么原因导致的点击事件触发,我决定使用排除法。
首先我直接把demo项目中的这个功能直接整个复制到了正式项目中去(覆盖掉该模块的其他代码),然后运行,发现是可以下载图片的,也就是说,功能确实是没有问题的。
然后我开始恢复页面样式(既然功能没问题,那我把页面也恢复,那这个需求不就解决了吗),恢复的过程中,我发现猫腻了,因为这个项目中很多地方都用到了天空和海平面作为背景图,然后这俩还是分开的,于是我之前封装了一个页面容器组件pageContainer,然后这个组件的插槽部分就是页面内容了,这样就不用我每个页面都重新写背景了。
当我使用pageContainer包裹页面内容的时候,视图层的方法就无法被触发。
又换回了html2canvas,结果又卡在了视图层事件不触发这上面,关键是,它不报错,点击之后啥反应都没有,人家逻辑层起码还抛一个notafunction出来,然后文档中没有提到过这个(可能是那句只支持内联?),社区中也没有,网上还找不到。