uniapp移动端生成分享海报并下载的实现及遇到的坑

需求及分析

需求是这样的,当用户进入页面的时候,会根据用户的邀请码生成一个二维码海报,然后用户点击下载,可以将这张海报保存到用户手机的相册。

这是一个很普通的需求,但是没想到让我遇到了一个大坑(后面细说),卡了我整整一天的进度。

一开始想着用HBX的 海报插件 来实现,然后发现这些海报插件,要么是不适配vue3,要么是可定制性不够高,只能按照它写好的模板进行替换,再或者就是要去其他网站用在线海报编辑器编辑好了之后,再生成配置文件,有点麻烦。简单的说就是,要么用不了,要么不好用。

于是便想着通过html2canvas来自己实现。

静态页面构建

新建一个组件 .vue 文件,然后在里面搭建页面结构,引入几张图片,得到下图。

页面结构如下:

vue 复制代码
<!-- 海报容器 -->
<view class="post-container" id="htmlCanvas">
	<!-- 海报背景图 -->
	<image src="@/static/bg.png" class="post" mode="aspectFit"></image>
	<!-- 海报上的艺术字 -->
	<image src="@/static/art-text.png" class="art-text" mode="aspectFit"></image>
	<!-- 邀请码背景 -->
	<image src="@/static/rounded-rect.png" class="rounded-rect" mode="aspectFit"></image>
	<!-- 邀请码文字 -->
	<view class="invite-text">
		<text>邀请码:{{inviteCode}}</text>
		<text>一起开黑组队启航</text>
	</view>
</view>
<!-- 分享按钮 -->
<view class="share-btn-group">
	<view class="share-item">
		<text>微信好友</text>
	</view>
	<view class="share-item">
		<text>微信朋友圈</text>
	</view>
	<view class="share-item" @click="canvas.toImage">
		<text>下载图片</text>
	</view>
</view>

html2canvas实现需求

首先是安装:

shell 复制代码
npm i html2canvas --save

因为html2canvas要操作DOM元素,因此这里我们要用到renderjs,根据uni官方文档的说明,我们在文件中新增一对 script 标签,如下:

vue 复制代码
<script module="canvas" lang="renderjs">
	// ... 代码
</script>

其中module表示模块名,可以任意起,但是不能和vue的data、computed等里面的属性起冲突。 lang属性则是固定为renderjs,表示启用renderjs。

此时我们组件中存在两个script标签,如下:

vue 复制代码
<script>
	export default {
		onLoad() {},
		methods: {},
	}
</script>
<script module="canvas" lang="renderjs">
	export default {
		mounted() {},
		methods: {},
	}
</script>

根据uniapp文档,我们将上面的script标签称为逻辑层 ,而下面使用renderjs的则称为视图层 ,在视图层中我们可以使用 documentwindow 等对象,进而可以操作DOM元素。

除此之外,renderjs还有挺多注意事项的,建议大家遇到问题了先去官方文档中查看这些注意事项,比如说:

  1. APP 端可以使用 dom、bom API,不可直接访问逻辑层数据,不可以使用 uni 相关接口(如:uni.request);
  2. vue3 项目不支持 setup script 用法;
  3. 可以使用 vue 组件的生命周期(不支持 beforeDestroy、destroyed、beforeUnmount、unmounted),不可以使用 App、Page 的生命周期;
  4. 目前仅支持内联使用;

toImage方法的实现

我们在视图层引入html2canvas,然后在其methods属性中定义一个 toImage 异步方法,该方法代码如下:

js 复制代码
async toImage() {
	try {
		const timeout = setTimeout(async () => {
			const htmlCanvas = document.getElementById('htmlCanvas')
			const canvas = await html2canvas(htmlCanvas, {
				backgroundColor: null,
				allowTaint: true,
				useCORS: true
			})
			const base64Data = canvas.toDataURL('image/png')
			this.$ownerInstance.callMethod('creates', base64Data)
			clearTimeout(timeout)
		}, 500);
	} catch (e) {
		console.log(e);
	}
}

图片跨域

toImage的定时器一定要加上的,不然的话会有问题,然后就是获取DOM,使用html2canvas,然后通过 toDataURL 获取图片的base64数据,一切都好像很顺利,直到我运行的时候,控制台出现这行红字:

shell 复制代码
Uncaught (in promise) SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported. at app-renderjs.js:7906

简单的说,就是canvas引入的图片跨域了,导致画布被污染,所以不能使用 toDataURL 这个方法导出数据了,于是我赶紧查看html2canvas的文档,发现有两个配置项与这个有关:

  • allowTaint:允许画布被污染;
  • useCORS:使用跨域;

我将这俩配置项都设为true之后,再运行,哦豁,还是同样的错误。

赶紧上网咨询广大网友,得到的方法是:

  1. 换成网络图片,然后让后端设置成允许跨域;
  2. 给img标签加上一个跨域的属性 crossorigin ,然后设为 anonymous

首先第一个方法,不是很ok,因为后端比较忙,而且就3张图片,我也实在是懒得叫。 然后第二个方法,好像可以,但是又瞬间清醒,我用的是image标签,不是img标签,在移动端使用img标签会不显示图片的,最后抱着希望跑去uni官网看看image标签会不会有这个属性:

很可惜,没有。

难道真的要去麻烦后端了吗!不不不,要不咋说网友是万能的呢,翻了十几页搜索结果之后,终于在一篇帖子中找到了,这位仁兄也遇到了同样的困境,canvas跨域,然后没有后端帮忙,他给出的解决方案如下:

二话不说,我直接就跑去把用到的图片转成了base64,然后封装成组件:

然后重写改写模板结构:

vue 复制代码
<view class="post-container" id="htmlCanvas">
	<Background class="post" />
	<ArtText class="art-text" />
	<RoundedSquare class="invite-code" />
	<view class="invite-text">
		<text>邀请码:{{inviteCode}}</text>
		<text>一起开黑组队启航</text>
	</view>
</view>

再去控制台一看,诶嘿,妥了,获取到图片的base64数据了,接下来就到去逻辑层实现 creates 方法了,因为我们将base64数据传给这个方法了。

creates方法的实现

这个方法的逻辑很简单,将base64数据转换成tempFilePath,然后调用 uni.saveImageToPhotosAlbum 将图片保存到用户相册即可。

base64ToPath方法的实现

如何将base64数据转换成tempFilePath呢,网上教的都是转成blob,但是这在uniapp的移动端中不适用啊,网上搜不到,我就去插件库中抄一下,看看它们是怎么实现的,嘿,当我打开LimeEchart的目录的时候,很快就让我在一个 utils.js 文件中翻到了一个名为 base64ToPath 的工具函数,啥也不说,直接复制:

js 复制代码
/**
 * base64转filePath
 * @param {Object} base64
 */
export function base64ToPath(base64) {
	return new Promise((resolve, reject) => {
		const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
		const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
		bitmap.loadBase64Data(base64, () => {
			if (!format) {
				reject(new Error('ERROR_BASE64SRC_PARSE'))
			}
			const time = new Date().getTime();
			const filePath = `_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方法了:

js 复制代码
creates(base64Data) {
	uni.showLoading({
		title: '下载中',
	})
	base64ToPath(base64Data).then(filePath => {
		uni.saveImageToPhotosAlbum({
			filePath,
			success: () => {
				uni.hideLoading()
				uni.showToast({
					title: '图片下载成功',
					mask: false,
					duration: 1500
				});
			}
		});
	})
}

到这里,基本上没有问题了,然后我就开了模拟器,进行了一波真机运行,确实很ok。

正式项目中遇到的坑

本次遇到最大的坑来了,上面是我在一个demo项目中实现的(编译快),然后我将其移动到正式项目中去。

然后发现,当我点击下载图片的时候,死活触发不了 toImage 方法,但是逻辑层的方法却不受影响,第一时间想到的是,难道是层级问题?毕竟这个页面用了挺多绝对定位和 z-index 的,但是跑了一下H5,控制台看了一下,没有问题啊,有层级问题的话,旁边分享到微信的两个按钮也应该用不了啊,咋就视图层的方法触发不了呢?

于是乎,我给下载图片按钮绑定了一个逻辑层的方法,然后想这个方法中调用视图层的方法:

vue 复制代码
<!-- <view class="share-item" @click="canvas.toImage"> -->
<view class="share-item" @click="downloadImage">
	<text>下载图片</text>
</view>
<script>
// ... 省略其他
downloadImage(){
	console.log('执行')
	this.canvas.toImage()
}
</script>

然后点击下载图片,控制台中输出了"执行"俩字,接着就是报错,说 canvas.toImage is not a function ,不是吧啊sir,我打印this出来,上面确实有canvas属性的喔,然后canvas里面也确实有toImage的喔,而且这个toImage也确实是function类型的喔,后来想想,我那是在H5看的this,跟移动端不一样,毕竟文档也写了这么两句话:

  • H5 端逻辑层和视图层实际运行在同一个环境中,相当于使用 mixin 方式,可以直接访问逻辑层数据;
  • uni-app的app端逻辑层和视图层是分离的; 好吧,看来想解决这个问题,得先弄清楚到底是什么原因导致的它不响应这个点击事件。

插槽中的元素无法触发视图层的点击事件

为了弄明白到底是什么原因导致的点击事件触发,我决定使用排除法。

首先我直接把demo项目中的这个功能直接整个复制到了正式项目中去(覆盖掉该模块的其他代码),然后运行,发现是可以下载图片的,也就是说,功能确实是没有问题的。

然后我开始恢复页面样式(既然功能没问题,那我把页面也恢复,那这个需求不就解决了吗),恢复的过程中,我发现猫腻了,因为这个项目中很多地方都用到了天空和海平面作为背景图,然后这俩还是分开的,于是我之前封装了一个页面容器组件pageContainer,然后这个组件的插槽部分就是页面内容了,这样就不用我每个页面都重新写背景了。

当我使用pageContainer包裹页面内容的时候,视图层的方法就无法被触发

vue 复制代码
<!-- 触发 -->
<view class="page-container">
	<view class="share-btn-group">
		<view class="share-item" @click="canvas.toImage">
			<text>下载图片</text>
		</view>
	</view>
</view>
<!-- 不触发 -->
<PageContainer>
	<view class="page-container">
		<view class="share-btn-group">
			<view class="share-item" @click="canvas.toImage">
				<text>下载图片</text>
			</view>
		</view>
	</view>
</PageContainer>

找到了问题所在,那解决起来就简单了,我直接不用pageContaienr组件了,这个页面直接写背景图,最后一试,果然没问题了。

写在最后

真的是麻了,本来挺简单的一个功能,结果一开始卡在base64转filePath上面,找了很多方法都没用,然后又因为跨域的问题,我不得已转变思路,改成了当用户点击下载的时候,调用系统的截图功能,将当前页面截图下来保存,后来截图会把按钮那些也截下来,用户体验不好,而且后面分享到微信及朋友圈也需要图片,因此这个方法行不通。

又换回了html2canvas,结果又卡在了视图层事件不触发这上面,关键是,它不报错,点击之后啥反应都没有,人家逻辑层起码还抛一个 not a function 出来,然后文档中没有提到过这个(可能是那句只支持内联?),社区中也没有,网上还找不到。

相关推荐
Heyuan_Xie3 小时前
uni-app总结5-UTS插件开发
uni-app
debug time13 小时前
uniapp 对接deepseek
uni-app
java_强哥13 小时前
uniapp实现聊天中的接发消息自动滚动、消息定位和回到底部
javascript·vue.js·uni-app
xw517 小时前
uni-app项目process is not defined
前端·uni-app
從南走到北17 小时前
物业收费管理小程序ThinkPHP+UniApp
微信小程序·小程序·uni-app·微信公众平台
vvilkim18 小时前
Uniapp App端原生插件深度解析:从使用到开发全指南
uni-app
!win !18 小时前
uni-app项目process is not defined
前端·uni-app
weixin_ab18 小时前
Uniapp 中根据不同离开页面方式处理 `onHide` 的方法
javascript·uni-app
德莱厄斯19 小时前
简单聊聊小程序、uniapp及其生态圈
前端·微信小程序·uni-app
小倪有点菜20 小时前
微信原生小程序转uniapp过程及错误总结
微信·小程序·uni-app