最近数字人很火,公司是做保险行业有关的,最近一直在抢占市场做数字人相关的项目,vue3开发的h5页面,内嵌小程序使用。本人主要负责前端开发,记录一下开发过程中所遇问题和知识点。
1. canvas绘制图片问题
1.1 canvas转base64图片时透明区域变成黑色背景
描述:canvas绘制带透明背景的图片和文字后,转成base64图片,透明背景变成黑色。
原因分析:背景图是png格式,toDataURL(type, quality)转换的时候转成jpeg格式的了,应该统一png:
cannvas.toDataURL("image/png");
1.2 *canvas清除画布内容后重新绘制透明底图片,边边会带阴影
问题定位发现第一次不会,clearRect()清除后重新绘制就会有,一开始以为逻辑问题:画布没清干净图片绘制了两次的原因,实际并未如此,猜想是不是文字影响,注释后发现果然如此!应该是画布内容清除了,但是文字携带的阴影还是存在没清除干净,重绘时清除画布的同时顺便清除阴影就可以了!
clearCanvas(){
this.ctx.clearRect(0, 0, this.$canvas.width, this.$canvas.height)
this.ctx.shadowColor = 'rgba(0, 0, 0, 0)';
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.shadowBlur = 0;
}
1.3 小程序回退内嵌h5有绘制canvas页面出现白屏
一进页面会调用接口获取详情页信息,拿到返回的图片再进行canvas绘制,直接写在<setup>里面调用,相当于是在created生命周期中使用。
但是:canvas应该在mounted的生命周期中初始化,在created和updated中都是无效的。
所以应该写在onMounted()生命周期中,或加个定时器。
<setup>
const getDetail = async () => {
let res = await getTmplDetailApi({ id: tmpId });
...
drawCanvas();
}
getDetail();
</setup>
2. 关于上传的问题
2.1 使用axios上传base64编码图片到七牛云,指定目录且带后缀
原代码返回的路径在根目录下且没带格式后缀,代码如下:
可在url后面设置key的值来指定存放目录和文件后缀。官网
canvas绘制后生成的图片base64 url为data:image/jpeg;base64,/9j/4AAQSkZJRg...,修改后的代码:
import { Base64 } from 'js-base64';
const qiniuImageHead = "https://static.xxxxx.com";
export const uploadBase64ToQiniu = async(file) => {
if(!file) {
toast("未获取到资源文件");
return
}
try {
const token = await getQiniuToken();
// 解析 Data URI,获取文件信息
const dataUriParts = file.split(';');
const mimeType = dataUriParts[0].split(':')[1]; // 获取 MIME 类型
const base64Data = dataUriParts[1].replace('base64,', ''); // 获取 Base64 编码的图像数据
// 生成一个唯一的文件名,例如使用时间戳
const timestamp = new Date().getTime();
const userInfo = await getUserInfo();
let userId = userInfo.user_id || 0;
const fileName = `/digitalman/image_${userId}_${timestamp}.${mimeType.split('/')[1]}`;
// 安全的 base64编码
const destination_file_name = Base64.encode(fileName).replace('+', '-').replace('/', '_');
// 输出文件信息
// console.log('文件名:', fileName);
// console.log('文件类型:', mimeType);
let url = 'https://upload.qiniup.com/putb64/-1/key/' + destination_file_name;
const qiniuRes = await ajax.post(url, base64Data, {
headers: {
"Content-Type": "application/octet-stream",
"Authorization": "UpToken "+token,
"Accept": "*/*",
},
processData: false
});
let theLongPicurl = qiniuImageHead + "/" + qiniuRes.data.key;
console.log('theLongPicurl: ',theLongPicurl);
return qiniuImageHead + "/" + qiniuRes.data.key;
}catch(err) {
toastError(err);
}
}
2.2 使用vant4的Uploader组件上传图片后,获取图片的尺寸宽高
/*
** 通过上传的图片file对象,获取图片的尺寸(宽 * 高)
*/
export const getImageSize = (file) => {
return new Promise((resolve, reject)=> {
try{
var arr = file.content.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
var f = new Blob([u8arr], { type: mime });
var reader = new FileReader();
reader.onload = function (e) {
var data = e.target.result;
//加载图片获取图片真实宽度和高度
var image = new Image();
image.onload=function(){
var width = image.width;
var height = image.height;
resolve({ width, height })
};
image.src= data;
};
reader.readAsDataURL(f);
}catch(err) {
reject(err)
}
})
}
2.3 *微信浏览器h5 ios监听loadedmetadata获取视频时长不成功没反应
使用loadedmetadata,当指定的音频/视频的元数据已加载时,会发生loadedmetadata事件
由于ios对audio和video的一系列限制,包括是提前load和自动播放等。所以只有当audio的play触发事件,才可以监听到duration,所以我们需默认触发play事件,播放音频。ios播放时默认会打开全屏,可以设置 playsinline 属性禁止全屏播放。
export const getVideoWidthHeight = (file) => {
return new Promise((resolve, reject)=> {
try{
// 创建一个video元素
const video = document.createElement('video');
videoElement.setAttribute("src", file.objectUrl);
// 设置 playsinline 属性
videoElement.setAttribute("playsinline", "true");
videoElement.setAttribute("webkit-playsinline", "true");
// 在视频加载完成后获取视频的宽度和高度
video.addEventListener('loadedmetadata', function() {
const width = video.videoWidth;
const height = video.videoHeight;
const duration = video.duration;
resolve({ width, height, duration })
console.log('视频宽度:', width);
console.log('视频高度:', height);
console.log("时长:", duration)
});
// console.log("ios----",isIos())
if (isIos()) {
showDialog({
title: "读取成功,开始上传",
confirmButtonText: "确定",
showConfirmButton: true,
})
.then(() => {
console.log("play1");
videoElement.play().then(() => videoElement.pause());
})
.catch(() => {
console.log("cancle");
});
}
}catch(err) {
reject(err)
}
})
}
3. 运行问题
3.1 (mac)运行vscode脚本node版本不一致
使用nvm来管理node版本,默认版本号14.15.0,因为有用到pnmp包管理器,需node版本号在16.14以上,但明明已经切换到18.15.0了,运行脚本还是提示在14.15.0,且总是变为14.15.0,运行报错提示版本低。
解决方法:使用命令行nvm alias default <version>将默认版本改成18.15.0
3.2 node版本过低报错,运行项目报错
原因:Vite 需要 Node.js 版本 14.18+,16+。且有些模板需要依赖更高的 Node 版本才能正常运行。
3.3 报错TypeError: Assignment to constant variable.
原因:使用const定义的常量被改动,将const改成let
3.4 按需引入使用Toast组件,引用 showToast 时出现编译报错的解决方案
解决办法:参考
4. 关于小程序
4.1 小程序内嵌h5使用wx.miniProgram.postMessage发送信息获取失败
需求:H5内嵌小程序实现视频保存到相册
// =====H5代码====
const testSave = () => {
wx.miniProgram.postMessage({type:'saveVideoRequest', videoUrl:'https://duobao-1256871399.cos.ap-guangzhou.myqcloud.com/15-202306/168767589938893.mp4'})
}
//====小程序代码====
<web-view bindmessage="bindGetMsg"></web-view>
// 获取h5传参
bindGetMsg: function (e) {
let articleData;
console.log("李哈哈---",e)
if (e.detail.data && e.detail.data.length > 0) {
articleData = e.detail.data.pop();
}
if(articleData.type == "saveVideoRequest"){
// 视频下载
if (articleData.videoUrl) {
let self = this;
wx.downloadFile({
url: articleData.videoUrl,
success(res) {
wx.hideLoading();
// console.log(res);
if (res.statusCode === 200) {
let path = res.tempFilePath;
wx.saveVideoToPhotosAlbum({
filePath: path,
success(res) {
self.setData({
showSucess: true
});
},
fail(res) {
util.toast('save视频保存失败');
}
})
}
},
fail(res) {
wx.hideLoading();
console.log('视频保存失败', res);
util.toast('视频保存失败');
}
})
}
}
解决办法:传递过去的数据必须写在data对象里面,修改后可以获取成功,但是仍然报错{errMsg: "invokeMiniProgramAPI"},且小程序那边没反应。
分析原因是网页向小程序postMessage时,只有在特定时机触发并接收到消息。
最终修改代码如下,最终成功将视频保存到手机相册。
// =====H5代码====
const testSave = () => {
wx.miniProgram.navigateBack({delta: 1}); //注:必须在前,否则触发不了接收不到信息
wx.miniProgram.postMessage({
data: {type:'saveVideoRequest', videoUrl:'https://duobao-1256871399.cos.ap-guangzhou.myqcloud.com/15-202306/168767589938893.mp4'}
})
}
4.2 移动端页面可以手动放大
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" 修改如下:/> -->
<meta name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover" />
4.3 微信小程序开发AudioContext与InnerAudioContext的区别
微信官方文档:
音频。1.6.0版本开始,该组件不再维护。建议使用能力更强的 wx.createInnerAudioContext 接口
AudioContext 通过 id 跟一个 audio 组件绑定,操作对应的 audio 组件。
所以说你不能用audio了,而audiocontext需要和audio绑定的,你都没有audio了,那也就不能用AudioContext了。
4.4 小程序打开授权页没看到麦克风授权
解决办法:在小程序中使用敏感权限(如麦克风、摄像头等)时.json进行相应的配置
"permission": {
"scope.record": {
"desc": "你的录音功能将用于小程序语音输入"
}
},
5. 数据处理
5.1 *签名验签(通信安全问题)
数字签名:在客户端和服务端的通信过程中,会遇到很多的安全问题,无法确认收到的是不是被修改过,这个时候数字签名就发挥作用了。
验签:顾名思义,就是一个对数字进行验证的操作。
步骤:
- 运营需求:动态生成小程序二维码,扫码进入到h5页面弹出免费领取弹窗,以生成的时间为准,超过24小时二维码失效。
- 思路:在跳转链接后面加个参数(生成二维码的时间戳),页面获取参数判断是否已过24小时。问题是如果有人直接修改时间戳也不能判断。于是对参数进行数字签名+验签。
- 处理:传2个参数:时间戳 + 验证码(定义一个只有自己知道的固定key字符串,将时间戳和key连接的字符串进行MD5加密得到一长字符串,截取10位作为验证码参数)。页面获取参数,将获取到参数时间戳和key连接进行MD5加密并截取10位,与获取到的验证码参数进行比较,就可判断链接参数有没有被修改过。
代码如下:
生成二维码页参数设置:
let createdAt = moment().unix();
let key = "realmanlmx";
let signStr = MD5(createdAt + key);
createdAt = createdAt + 'sign' + signStr.substring(0, 10);
获取的链接:http://localhost:3000/digital/invite?ctime=1704367048signb89c0c4666
弹窗也获取参数进行验证:
const isExpired = ref(true); //小程序码是否过期(超24h)
const checkQrcodeAuth = () => {
let createAt = route.query.ctime || ""; //小程序码创建的时间戳(秒)
if(!createAt) {
pageStart();
return
}
let signArr = createAt ? createAt.split("sign") : ""; //['时间戳', '验证码']
let key = "realmanlmx";
let signStr = MD5(signArr[0] + key); //校验码
if(signStr.substring(0, 10)== signArr[1]){
if(parseInt(nowTimestamp)< parseInt(signArr[0]) + 24*3600){
isExpired.value = false;
}
}
}