uniapp —— 生成带参的小程序码海报分享保存相册并调试(包含后端PHP代码)

生成小程序推广二维码海报,我们在日常的工作当中经常需要用到,因此今天总结了一下开发的过程步骤以及,踩过的一些坑。
温馨提示:完整的代码放在最后面,文章段落分析截取的不是完全的代码。

零、实现效果以及思路

  1. 获取微信 access_token
  2. 根据 access_token 调用接口生成特定的小程序码;
  3. 获取小程序码并绘画成 canvas
  4. 利用 canvas 生成海报图片;
  5. 分享或保存到相册。
    效果

一、后端获取Token并生成带参的小程序码

为什么需要在后端生成?前端生成不可以吗?
其实,前端也可以自己生成二维码,只是鉴于密钥的安全性问题,这个步骤最好还是交给后端处理,避免泄露;
另外,uni.requestwx.request 官方指定不可以直接请求获取 access_token 接口;
最后,前端请求回来 access_token 使用时,会经常性提示 token 过期,用户体验不好。

//把请求发送到微信服务器换取二维码
function httpRequest($url, $data='', $method='GET'){
	$curl = curl_init();  
	curl_setopt($curl, CURLOPT_URL, $url);  
	curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);  
	curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);  
	curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);  
	curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);  
	curl_setopt($curl, CURLOPT_AUTOREFERER, 1);  
	if($method=='POST'){
		curl_setopt($curl, CURLOPT_POST, 1); 
		if ($data != ''){
			curl_setopt($curl, CURLOPT_POSTFIELDS, $data);  
		}
	}
	
	curl_setopt($curl, CURLOPT_TIMEOUT, 30);  
	curl_setopt($curl, CURLOPT_HEADER, 0);  
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);  
	$result = curl_exec($curl);  
	curl_close($curl);  
	return $result;
}

//生成二维码
function get_qrcode($id){
	$APPID = "xxxx"; 
	$APPSECRET =  "xxxxxxxxx"; 
	
	//获取access_token
	$access_token = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$APPID&secret=$APPSECRET";
	
	//缓存access_token
	session_start();
	$_SESSION['access_token'] = "";
	$_SESSION['expires_in'] = 0;
	
	$ACCESS_TOKEN = "";
	if(!isset($_SESSION['access_token']) || (isset($_SESSION['expires_in']) && time() > $_SESSION['expires_in'])){
		$json = httpRequest( $access_token );
		$json = json_decode($json,true); 
		
		$_SESSION['access_token'] = $json['access_token'];
		$_SESSION['expires_in'] = time()+7200;
		$ACCESS_TOKEN = $json["access_token"]; 
	}else{
		$ACCESS_TOKEN = $_SESSION["access_token"]; 
	}
	
	//构建请求二维码参数
	//path是扫描二维码跳转的小程序路径,可以带参数?id=xxx
	//width是二维码宽度
	$qcode ="https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=$ACCESS_TOKEN";
	$param = json_encode(array("path"=>"pages/weixiurenyuan/weixiurenyuan?id=$id","width"=>150));
	
	//POST参数
	$result = httpRequest( $qcode, $param,"POST");
	
	//生成二维码
	// file_put_contents("qrcode.png", $result);
	$base64_image = "data:image/jpeg;base64,".base64_encode( $result );
	
	return $base64_image; //返回base64图片数据
}

二、绘画canvas

这里有必要说一下,我们判断是否开始绘画 canvas 的节点,就是判断用户是否点击保存或分享,以此来规避频繁请求接口和频繁绘画 canvas。所以,这里选择的处理方式是,当用户点击推广按钮时候:

  1. 请求后端获取第一次小程序码,先用 HTML 渲染海报,供用户选择保存或分享;
  2. 生成小程序码之后,多次点击推广按钮,不会再次请求后端获取小程序码;
  3. 当用户选择保存或分享时,再开始绘画 canvas

逻辑代码,我们需要先分析一下绘画的步骤:

  1. 背景图片绘画,该背景图片是网络图片,需将其先转换为本地临时图片,再绘画,否则真机上 canvas 不显示;
  2. 小程序码是 base64 格式,因为 drawImage 方法不支持 base64 图片,因此,需先转换为本地临时图片再绘画,否则真机上 canvas 不显示;
  3. 有了思路之后我们开始执行。
// 引入请求后端二维码接口
import { user_invited } from '@/utils/request/api.js'

export default {
	data() {
		return {
			qrCode: '', // 存储请求回来的base64图片路径
			realShow: false, // 控制poster显示隐藏
			cacheImage: '' // 存储canvas转换而来的图片
		}
	},
	methods: {
		/**
		 * 控制显示海报
		 * 在当前页面中,如果base64图片路径为空,则请求二维码
		 * 否则,只控制HTML显隐,以减少HTTP请求
		*/
		async show() {
			if (this.qrCode == '') {
				await user_invited().then(res => {
					this.qrCode = res.data
				})
			}
			this.realShow = true
		},
		// 关闭海报
		close() {
			this.realShow = false
		},
		// 将网络图片转换成本地临时图片
		handleNetworkImgaeTransferTempImage(url) {
			return new Promise(resolve => {
				if(url.indexOf('http') || url.indexOf('https') === 0) {
					uni.downloadFile({
						url,
						success: res => {
							resolve(res.tempFilePath);
						}
					});
				}
				else {
					resolve(url);
				}
			});
		},
		// 删除本地同名base64图片文件
		removeSave() {
			return new Promise((resolve, reject) => {
				const fsm = uni.getFileSystemManager();
				const FILE_BASE_NAME = 'tmp_base64src';
				const format = 'jpeg';
				const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
				fsm.unlink({
					filePath: filePath,
					success(res) {
						resolve(true);
					},
					fail(err) {
						resolve(true);
					}
				})
			})
		},
		// 将base64图片存储到本地并转换成临时图片
		handleBase64ImageTransferTempImage(base64Image, FILE_BASE_NAME = 'tmp_base64src') {
			const fsm = uni.getFileSystemManager();
			return new Promise((resolve, reject) => {
				//format这个跟base64数据的开头对应
				const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64Image) || [];
				if (!format) {
					reject(new Error('ERROR_BASE64SRC_PARSE'));
				}
				const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
				fsm.writeFile({
					filePath,
					data: bodyData,
					encoding: 'base64',
					success() {
						resolve(filePath);
					},
					fail() {
						reject(new Error('ERROR_BASE64SRC_WRITE'));
					},
				});
			})
		},
		// 生成 canvas 并生成微信缓存图片
		async htmlToCanvas() {
			let that = this
			uni.showLoading({title: '正在生成海报'})
			const ctx = uni.createCanvasContext('poster', this)
			// 像素转换
			const system = uni.getSystemInfoSync(), wpx = system.windowWidth / 750
			// 图片尺寸
			// const imageWidth = 610, imageHeight = 1050
			// 绘画过程,需先将网络图片转为本地临时图片
			const image = 'http://nq34.gzfloat.com/public/user/invite_bg@2x.png'
			// 绘制网络图片 canvas
			ctx.drawImage(await this.handleNetworkImgaeTransferTempImage(image),0,0,610*wpx,950*wpx)
			// 绘画过程,需将base64图片转为本地临时图片,否则canvas不支持
			await this.removeSave()
			const base64CacheImage = await this.handleBase64ImageTransferTempImage(this.qrCode)
			if (base64CacheImage) {
				ctx.drawImage(base64CacheImage,218*wpx,705*wpx,185*wpx,195*wpx)
			}
		}
	}
}

三、生成海报

为什么需要单独拎出来讲呢?因为这里踩过坑(呜呜呜)
使用 draw() 这个方法的时候,记得一定要添加一个定时器,记得一定要添加一个定时器,记得一定要添加一个定时器!否则,canvas 绘画出来永远都是空白的,保存到相册的图片也都是空白的。

setTimeout(() => {
	ctx.draw(true, () => {
		uni.canvasToTempFilePath({
			canvasId: 'poster',
			success(res) {
				that.cacheImage = res.tempFilePath
			},
			fail(error) {
				throw new Error(error)
			}
		}, that)
	})
	uni.hideLoading()
}, 100)

四、分享或保存本地

分享好友就直接调用button里面的open-type="share",因为小程序不支持图片直接转发,因此你可以将生成的海报作为分享的imgUrl封面,然后path携带参数即可(对实现分享不理解的可以阅读我的另一篇文章《传送地址》)。

// 授权相册并保存
authToSave() {
	let that = this
	uni.authorize({
		scope: 'scope.writePhotosAlbum',
		success() {
			uni.showLoading({title: '正在保存海报'})
			if (that.cacheImage != '') {
				uni.getImageInfo({
					src: that.cacheImage, 
					success: function(image) {
						/* 保存图片到手机相册 */
						uni.saveImageToPhotosAlbum({
							filePath: that.cacheImage,
							success: function() {
								that.realShow = false
								uni.$u.toast('图片已成功保存到相册')
							}
						});
					},
					fail: function(image){
						console.log(image);
					}
				});
			} else {
				that.authToSave()
			}
		},
		complete(res) {
			uni.hideLoading()
			/* 判断如果没有授权就打开设置选项让用户重新授权 */
			uni.getSetting({
				success(res) {
					if (!res.authSetting['scope.writePhotosAlbum']) {
						/* 打开设置的方法 */
						uni.showModal({
							content: '没有授权保存图片到相册,点击确定去允许授权',
							success: function(res) {
								if (res.confirm) {
									/* 打开设置的API*/
									uni.openSetting({
										success(res) {}
									});
								} else if (res.cancel) {
									uni.showModal({
									cancelText: '取消',
										confirmText: '重新授权',
										content: '你点击了取消,将无法进行保存操作',
										success: function(res) {
											if (res.confirm) {
												uni.openSetting({
													success(res) {/* 授权成功 */}
												});
											} else if (res.cancel) {
												console.log('用户不授权');
											}
										}
									});
								}
							}
						});
					}
				}
			});
		}
	})
},
// 监控canvas错误函数
canvasIdErrorCallback(e) {
	console.log(e);
},
// 保存本地
savePoster() {
	this.htmlToCanvas()
	this.authToSave()
}

五、调试

  1. 微信开发者工具上传解析你生成的二维码
    添加编译模式
    自动识别参数
  2. 微信开发者工具模拟场景和自定义参数
    手动输入
  3. 页面中获取
onLoad(options) {
	if (options) {
		console.log('tui_id:' + options.tui_id)
	}
}

获取成功
获取成功。

六、最后,完整的父子组件丢出来

父组件HTML:

<template>
	<view>
		<!-- 生成海报 -->
		<poster ref="poster"></poster>
	</view>
</template>

父组件JavaScript:

this.$refs.poster.show();

子组件完整代码:

子组件HTML:

<template>
	<view :class="[{'poster_container_active': realShow},'poster_container']">
		<view class="poster">
			<view @click="close" class="poster_close">
				<u-icon name="close-circle" color="#fff" size="24"></u-icon>
			</view>
			<view class="poster_qrcode">
				<image :src="qrCode" mode="widthFix"></image>
			</view>
			<canvas v-if="realShow" style="width: 100%;height: 100%;opacity: 0;" canvas-id="poster" @error="canvasIdErrorCallback"></canvas>
		</view>
		<view class="poster_tool">
			<view class="poster_tool_item">
				<button open-type="share">
					<u-icon name="weixin-circle-fill" color="green" size="38"></u-icon>
					<text>分享到微信</text>
				</button>
			</view>
			<view class="poster_tool_item" @click="savePoster">
				<u-icon name="download" color="#00aaff" size="38"></u-icon>
				<text style="margin-top: 10rpx;">保存到本地</text>
			</view>
		</view>
	</view>
</template>

子组件JavaScript:

<script>
	import { user_invited } from '@/utils/request/api.js'
	export default {
		data() {
			return {
				qrCode: '',
				realShow: false,
				cacheImage: ''
			}
		},
		methods: {
			// 显示海报
			async show() {
				if (this.qrCode == '') {
					await user_invited().then(res => {
						this.qrCode = res.data
					})
				}
				this.realShow = true
			},
			// 关闭海报
			close() {
				this.realShow = false
			},
			// 将网络图片变成临时图片
			handleNetworkImgaeTransferTempImage(url) {
				return new Promise(resolve => {
					if(url.indexOf('http') === 0) {
						uni.downloadFile({
							url,
							success: res => {
								resolve(res.tempFilePath);
							}
						});
					}
					else {
						resolve(url);
					}
				});
			},
			// 删除本地同名base64图片文件
			removeSave() {
				return new Promise((resolve, reject) => {
					const fsm = uni.getFileSystemManager();
					const FILE_BASE_NAME = 'tmp_base64src';
					const format = 'jpeg';
					const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
					fsm.unlink({
						filePath: filePath,
						success(res) {
							resolve(true);
						},
						fail(err) {
							resolve(true);
						}
					})
				})
			},
			// 将base64图片存储到本地并转换成临时图片
			handleBase64ImageTransferTempImage(base64Image, FILE_BASE_NAME = 'tmp_base64src') {
				const fsm = uni.getFileSystemManager();
				return new Promise((resolve, reject) => {
					//format这个跟base64数据的开头对应
					const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64Image) || [];
					if (!format) {
						reject(new Error('ERROR_BASE64SRC_PARSE'));
					}
					const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
					fsm.writeFile({
						filePath,
						data: bodyData,
						encoding: 'base64',
						success() {
							resolve(filePath);
						},
						fail() {
							reject(new Error('ERROR_BASE64SRC_WRITE'));
						},
					});
				})
			},
			// 生成 canvas 并生成微信缓存图片
			async htmlToCanvas() {
				let that = this
				uni.showLoading({title: '正在生成海报'})
				const ctx = uni.createCanvasContext('poster', this)
				// 像素转换
				const system = uni.getSystemInfoSync(), wpx = system.windowWidth / 750
				// 图片尺寸
				// const imageWidth = 610, imageHeight = 1050
				// 绘画过程,需先将网络图片转为本地临时图片
				const image = 'http://nq34.gzfloat.com/public/user/invite_bg@2x.png'
				// 绘制网络图片 canvas
				ctx.drawImage(await this.handleNetworkImgaeTransferTempImage(image),0,0,610*wpx,950*wpx)
				// 绘画过程,需将base64图片转为本地临时图片,否则canvas不支持
				await this.removeSave()
				const base64CacheImage = await this.handleBase64ImageTransferTempImage(this.qrCode)
				if (base64CacheImage) {
					ctx.drawImage(base64CacheImage,218*wpx,705*wpx,185*wpx,195*wpx)
				}
				setTimeout(() => {
					ctx.draw(true, () => {
						uni.canvasToTempFilePath({
							canvasId: 'poster',
							success(res) {
								that.cacheImage = res.tempFilePath
							},
							fail(error) {
								throw new Error(error)
							}
						}, that)
					})
					uni.hideLoading()
				}, 100)
			},
			// 授权相册并保存
			authToSave() {
				let that = this
				uni.authorize({
					scope: 'scope.writePhotosAlbum',
					success() {
						uni.showLoading({title: '正在保存海报'})
						if (that.cacheImage != '') {
							uni.getImageInfo({
								src: that.cacheImage, 
								success: function(image) {
									/* 保存图片到手机相册 */
									uni.saveImageToPhotosAlbum({
										filePath: that.cacheImage,
										success: function() {
											that.realShow = false
											uni.$u.toast('图片已成功保存到相册')
										}
									});
								},
								fail: function(image){
									console.log(image);
								}
							});
						} else {
							that.authToSave()
						}
					},
					complete(res) {
						uni.hideLoading()
						/* 判断如果没有授权就打开设置选项让用户重新授权 */
						uni.getSetting({
							success(res) {
								if (!res.authSetting['scope.writePhotosAlbum']) {
									/* 打开设置的方法 */
									uni.showModal({
										content: '没有授权保存图片到相册,点击确定去允许授权',
										success: function(res) {
											if (res.confirm) {
												/* 打开设置的API*/
												uni.openSetting({
													success(res) {}
												});
											} else if (res.cancel) {
												uni.showModal({
												cancelText: '取消',
													confirmText: '重新授权',
													content: '你点击了取消,将无法进行保存操作',
													success: function(res) {
														if (res.confirm) {
															uni.openSetting({
																success(res) {/* 授权成功 */}
															});
														} else if (res.cancel) {
															console.log('用户不授权');
														}
													}
												});
											}
										}
									});
								}
							}
						});
					}
				})
			},
			// 监控canvas错误函数
			canvasIdErrorCallback(e) {
				console.log(e);
			},
			// 保存本地
			savePoster() {
				this.htmlToCanvas()
				this.authToSave()
			}
		}
	}
</script>

子组件完整CSS:

<style lang="scss" scoped>
	.poster_container {
		opacity: 0;
		display: block;
		position: fixed;
		z-index: -99;
		top: 0;
		left: 0;
		width: 100vw;
		height: 100vh;
		transition: opacity .5s;
		.poster {
			background: url('http://nq34.gzfloat.com/public/user/invite_bg@2x.png');
			background-size: 100% 100%;
			background-repeat: no-repeat;
			position: absolute;
			width: 610rpx;
			height: 950rpx;
			top: 50%;
			left: 50%;
			transform: translate3d(-50%,-575rpx,0);
			.poster_close {
				position: absolute;
				top: 2%;
				right: 2%;
				z-index: 9999999999999;
			}
			.poster_qrcode {
				position: absolute;
				left: 50%;
				bottom: 40rpx;
				transform: translateX(-50%);
				image {
					width: 175rpx;
				}
			}
		}
		.poster_tool {
			position: absolute;
			left: 0;
			bottom: 0;
			z-index: 99999999999;
			width: 100vw;
			height: 210rpx;
			border-top-left-radius: 30rpx;
			border-top-right-radius: 30rpx;
			background-color: #fff;
			display: flex;
			justify-content: space-around;
			align-items: center;
			padding-bottom: env(safe-area-inset-bottom);
			.poster_tool_item {
				width: 150rpx;
				height: 150rpx;
				background-color: #e6e6e652;
				border-radius: 15rpx;
				display: flex;
				flex-direction: column;
				justify-content: center;
				align-items: center;
				font-size: 23rpx;
				color: #666666b8;
				button {
					width: 150rpx;
					height: 150rpx;
					background-color: #f5f5f552;
					border-radius: 15rpx;
					display: flex;
					flex-direction: column;
					justify-content: center;
					align-items: center;
					font-size: 23rpx;
					color: #666666b8;
					padding: 0;
				}
				button::after{
					  border: none;
					  outline: none;
				}
			}
			.poster_tool_item:hover {
				background-color: #dfdfdf52;
			}
		}
	}
	.poster_container_active {
		opacity: 1;
		z-index: 99999999;
		background-color: rgba(0, 0, 0, .7);
	}
</style>
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐