首页
统计
友链
关于
Search
1
webRTC与webSocket接入智谱视频通话API避坑指南
89 阅读
2
mysql开发规范
81 阅读
3
关于虚拟dom复用导致的组件渲染不一致问题
79 阅读
4
详谈JavaScript发展史
71 阅读
5
git操作
70 阅读
前端
面试
算法
学习
踩坑日记
登录
Search
标签搜索
git
算法
sql
指针原来是套娃的
累计撰写
22
篇文章
累计收到
0
条评论
首页
栏目
前端
面试
算法
学习
踩坑日记
页面
统计
友链
关于
搜索到
5
篇与
的结果
2024-12-06
关于虚拟dom复用导致的组件渲染不一致问题
今天开发时,有一个页面需要使用自定义的列表组件,共有两个列表,用按钮切换列表展示,但是第二个列表不需要头部的选择框。本来配置selection选项即可,但是切换过来第二个列表无论怎么配置还是带着选择框的样式,无法去掉,相同配置放到其他页面却没有选择框。代码和样式如下:<-- 部分配置已忽略 --> <el-table-wrap v-if="invoiceStatus === 'bill'" :data="dataList" > <-- 配置选择框 --> <el-table-column type="selection" width="40"> </el-table-column> </el-table-wrap> <el-table-wrap v-else :data="customDataList" > <-- 没有配置 --> </el-table-wrap>但是第二个实现出来却是这样的:但是奇怪的是,同样的配置在其它界面就没有选择框,最后经过调试发现把v-if改成v-show显示没有问题了。<-- 部分配置已忽略 --> <el-table-wrap v-show="invoiceStatus === 'bill'" :data="dataList" > <el-table-column type="selection" width="40"> </el-table-column> </el-table-wrap> <el-table-wrap v-show="invoiceStatus === 'custom'" :data="customDataList" > </el-table-wrap>原来是v-if和v-show的区别,v-if会把元素销毁,再创建元素的时候,vue在虚拟dom阶段会复用之前的节点,导致第二个列表携带着第一个列表的配置。v-show只是隐藏显示,两个列表会同时创建,vue会自动标识加上key值。当然给列表组件加上key也可以解决这个问题,给一个加上即可。<-- 部分配置已忽略 --> <el-table-wrap v-if="invoiceStatus === 'bill'" key="bill" :data="dataList" > <el-table-column type="selection" width="40"> </el-table-column> </el-table-wrap> <el-table-wrap v-else :data="customDataList" > </el-table-wrap>有不一致配置时避免使用v-if改为v-show,或者加上key值。复现demo:<template> <div> <el-button type="primary" @click="selection = !selection">切换</el-button> <el-table v-if="selection" :data="tableData"> <el-table-column type="selection" width="55"/> <el-table-column prop="name" label="姓名" width="120"/> <el-table-column prop="address" label="地址" show-overflow-tooltip/> </el-table> <el-table v-else :data="tableData"> <el-table-column prop="name" label="姓名" width="120"/> <el-table-column prop="address" label="地址" show-overflow-tooltip/> </el-table> </div> </template> <script> export default { data() { return { tableData: [ { date: '2016-05-03', name: '王小虎', address: '上海市普陀区金沙江路 1518 弄', }, ], selection: true, }; }, created() {}, methods: {}, }; </script> <style scoped> </style>
2024年12月06日
79 阅读
0 评论
5 点赞
2024-10-08
webRTC与webSocket接入智谱视频通话API避坑指南
先看效果:智谱新上线的音视频通话大模型拥有理解音频和画面的能力,接入该api,需要使用webSocket协议不断传输音频和视频的base64编码。WebRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的技术,webRTC最强大的功能是无需服务器,可以实现浏览器端对端的音视频通话,这里只使用了webRTC实现视频画面显示。这里就详细说一下从开发到跑起这个demo踩的一些不得不说的血泪坑。一技术栈使用vue2,webRTC自然就选用了vue-webRTC,npm地址:https://www.npmjs.com/package/vue-webrtc/v/2.0.0?activeTab=readme通过看它的readme文档发现,最新版是3.0.1,从3.0.0开始支持vue3,vue2使用的是2.0.0版本。跑vue-webRTC这个示例没有出现问题,但是上线的时候却发现一直报错:Permissions policy violation: camera is not allowed in this document.网上有关这些的文档都是说没有授权导致,在这里查资料卡了很久。线上使用的也是https协议,也授权了权限,调试很多次一直报错,终于在一篇文档里发现了问题: 【Web安全】Permissions Policy(权限策略)详解 不知道是谁给线上环境的响应头里设置了不允许使用麦克风和摄像头,也是吐血了。这里需要从Nginx修改配置,于是换了一个线上环境,成功解决。二智谱接入模型需要使用webSocket协议,这很简单,使用原生js new WebSocket(url)即可,提供了.send()方法发送数据,.onopen方法监听建立连接成功 .onmessage方法监听服务器返回。部分代码如下: this.ws = new WebSocket(this.socketURL + `?Authorization=${APIKey}`) this.ws.onopen = () => { console.log('WebSocket 连接已打开') } this.ws.onmessage = event => { let data = JSON.parse(event.data) console.log('收到服务器消息:', data) } // 发送消息 this.ws.send(JSON.stringify(message))但是接入的时候需要传入apiKey进行鉴权,api文档里写的是将key放在Header头里面,这完全就没有考虑浏览器调用情况!浏览器端对写入header头很严格,而且这也不是简单的get post请求,可以用axios工具设置请求头。所以当时有两种方法,1、找一个可以支持websocket协议写入请求头的库 2、使用@fortaine/fetch-event-source发送fetch请求,fetch是websocket的替代方案,可以解决传复杂参数问题。因为有一个已经封装好了的可以用fetch的库,所以当时选择通过fetch协议调用。通过一番操作以后,发post请求报405 发get请求报400,调试到这里真是吐血了!!用文档给的java代码调用就没有问题,这个文档根本就没有考虑浏览器方案。服务器端只支持websocket协议,而websocket或eventsource是get请求,带参只能跟在url地址后面。所以发post会报405请求方法不对,后端又只从请求头里拿鉴权信息,发get带过去的它都不认。经过一番协商,后端改了鉴权方法,从url地址里拿key信息,这下直接用原生webSocket协议都行了。调试半天,后端改一下协议十分钟搞定。三搞定传输以后,剩下的就比较简单了,从音视频流里获取视频和音频数据,再转成base64编码,直接发送即可。特别感谢:https://github.com/2fps/recorder作者整理的关于音频的资料很详细。请求获取音视频流: // 获得音视频数据流 getUserMedia() { if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices .getUserMedia({ audio: true, // 设置为 true 以请求音频流 video: true }) .then(stream => { // 拿到音视频数据流 this.activeStream = stream // 包含视频&&音频 this.videoStream = new MediaStream(stream) // 创建只包含视频轨道的 MediaStream // this.videoStream = new MediaStream() // this.activeStream.getVideoTracks().forEach(track => this.videoStream.addTrack(track)) // 创建只包含音频轨道的 MediaStream this.audioStream = new MediaStream() this.activeStream.getAudioTracks().forEach(track => this.audioStream.addTrack(track)) // 获得音频数据流 this.getAudioBlob() this.getVideoBlob() }) .catch(err => { console.error('获取用户媒体流失败:', err) }) } else { alert('你的浏览器不支持 getUserMedia') } },获得音频流数据 // 获得音频数据流 getAudioBlob() { // 获得pcm格式的音频数据 // 创建 AudioContext const audioContext = new (window.AudioContext || window.webkitAudioContext)() // 创建 MediaStreamSource const source = audioContext.createMediaStreamSource(this.audioStream) // 创建录音节点,指定缓冲区大小和处理音频的输入输出通道数 const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1) // 将音频输入源连接到录音节点,并将录音节点连接到音频上下文的输出 source.connect(scriptProcessor) scriptProcessor.connect(audioContext.destination) // 存储 scriptProcessor 和 audioContext 以便之后可以断开连接 this.scriptProcessor = scriptProcessor this.audioContext = audioContext // 当音频数据可用时执行的回调函数 scriptProcessor.onaudioprocess = audioProcessingEvent => { const inputBuffer = audioProcessingEvent.inputBuffer // 获取第一个(也是唯一一个)输入通道的 Float32Array 数据 leftChannelData 就是 PCM 数据 const leftChannelData = inputBuffer.getChannelData(0) this.inputAudioData.push(new Float32Array(leftChannelData)) this.audioSize += leftChannelData.length } },获得视频流数据// 获得视频数据流 getVideoBlob() { // 设置 MediaRecorder 选项 const videoOptions = { mimeType: 'video/webm;codecs=H264' } // 创建视频 MediaRecorder this.videoMediaRecorder = new MediaRecorder(this.videoStream, videoOptions) // 当stop时触发 this.videoMediaRecorder.ondataavailable = event => { if (event.data && event.data.size > 0) { const data = event.data // 获得音视频的Blob对象 const blobDate = this.getBlob(data) if (this.control.vad_config.server_vad) { // 获得base64数据 this.getBase64(blobDate.videoBlob, blobDate.audioBlob) } else { // 存储blob对象 等待手动发送 this.audioBlobs.push(blobDate.audioBlob) this.videoBlobs.push(blobDate.videoBlob) } } } // 当录制停止时触发 this.videoMediaRecorder.onstop = () => { // 开启vad检测时 触发开启事件 if (this.control.vad_config.server_vad) { this.videoMediaRecorder.start() } } },在这里也要说一点this.videoMediaRecorder.start()开启的时候可以加参数 即多少ms触发一次ondataavailable回调,但是这样在ondataavailable里面拿到的数据只有第一次有头部,后面触发获得的没有头部,需要把所有数据段的拼接起来才能组成完整的视频片段。所有这里采用加一个定时器定时触发stop,stop也会触发ondataavailable,并且监听stop的触发,stop停止时就开启,这样拿到的是完整的视频片段。获得音视频数据后,下面再转成base64即可,下面是互相转换的函数: /** * 将Blob对象转换为数组。 * * @param {Blob} blob - 要转换的Blob对象。 * @returns {Promise<Uint8Array>} 返回一个Promise,该Promise解析为一个包含二进制数据的Uint8Array数组。 */ export function blobToBinaryArray(blob) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = function () { const arrayBuffer = reader.result const bytes = new Uint8Array(arrayBuffer) resolve(bytes) } reader.onerror = function (error) { reject(error) } reader.readAsArrayBuffer(blob) }) } /** * 将Blob对象转换为Base64编码的字符串。 * @param {Blob} blob - 要转换的Blob对象。 * @returns {Promise<string>} 返回一个Promise,解析为Base64编码的字符串。 */ export function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onloadend = function () { const base64String = reader.result.split(',')[1] resolve(base64String) } reader.onerror = reject reader.readAsDataURL(blob) }) } /** * 将Base64编码的字符串转换为Blob对象。 * @param {string} base64 - 需要转换的Base64编码字符串 * @param {string} mimeType - Blob对象的MIME类型,例如'image/jpeg'或'image/png'。 * @returns {Blob} 返回一个Blob对象 */ export function base64ToBlob(base64, mimeType) { // 分割Base64字符串以获取数据部分,通常Base64字符串以"data:image/...,"开头 const byteString = atob(base64) // 创建一个ArrayBuffer,其大小与解码后的字符串长度相同 const ab = new ArrayBuffer(byteString.length) // 创建一个Uint8Array视图,将ArrayBuffer的内容初始化为8位无符号整数值 const ia = new Uint8Array(ab) // 遍历解码后的字符串,并将每个字符的Unicode编码存入Uint8Array数组 for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i) } // 使用ArrayBuffer和MIME类型创建Blob对象 const blob = new Blob([ab], { type: mimeType }) // 返回Blob对象 return blob } /** * 将Base64编码的字符串转换为ArrayBuffer。 * @param {string} base64 - 需要转换的Base64编码字符串。 * @returns {ArrayBuffer} 返回一个ArrayBuffer对象,它包含了从Base64字符串转换而来的字节数据。 */ export function base64ToArrayBuffer(base64) { const binaryString = window.atob(base64) // 使用window.atob()方法将Base64编码的字符串解码成二进制字符串 const len = binaryString.length // 获取二进制字符串的长度 const bytes = new Uint8Array(len) // 创建一个Uint8Array数组,用于存储转换后的字节数据 for (let i = 0; i < len; i++) { // 遍历二进制字符串的每个字符 bytes[i] = binaryString.charCodeAt(i) // 将每个字符的charCodeAt值(即字符的Unicode编码)存储到Uint8Array数组中 } return bytes.buffer // 返回Uint8Array数组的buffer属性,它是一个ArrayBuffer对象 } 四该api会有两种返回方式:1、返回文本 2、默认返回音频这样就比较难受了,不能同时展示,而且模型返回的音频是带感情的,还不能简单的文字转音频。现在常见的音频转文本方案都是调用外部api,或者在浏览器端运行转文本模型(这个试了一下它们的样例,不仅要翻墙,一百多兆的模型转的文本错字也很多)最后放弃了,写了一个文本和语音播放,返回文本就展示文本,返回语音就展示语音。语音返回的是pcm或wav格式的base64字符串,将base64字符串转成blob对象,创建一个资源url,写进audio.src里播放即可,这里比较简单,详见代码: // 播放音频流数据 outAudios(serverMessagesIndex) { let nextAudioMessage = this.serverMessages[serverMessagesIndex] // 设置播放动画 for (let i = 0; i < this.serverMessages.length; i++) { this.serverMessages[i].voicePlay = false } this.serverMessages[serverMessagesIndex].voicePlay = true console.log(nextAudioMessage) if (this.serverMessages.length < 1) { return } let audioContent = nextAudioMessage.audioContent let audioBase64 = '' let wavBlobs = [] for (let i = 0; i < audioContent.length; i++) { // 服务端返回的是base64字符串 audioBase64 = audioContent[i] // wav Blob对象 const wavBlob = base64ToBlob(audioBase64, 'audio/mav') wavBlobs.push(wavBlob) } // download(wavBlobs[0], 'wav') // 播放音频 this.playNextAudio(wavBlobs, 0, serverMessagesIndex) }, // 播放音频的函数 playNextAudio(wavBlobs, currentAudioIndex, serverMessagesIndex) { if (currentAudioIndex < wavBlobs.length) { // 创建音频URL let audioSrc = URL.createObjectURL( // new Blob([wavBlobs[currentAudioIndex]], { type: 'audio/wav' }) wavBlobs[currentAudioIndex] ) // 设置音频源并播放 this.audioElement.src = audioSrc this.audioElement.play() // 当音频播放完毕 this.audioElement.onended = () => { console.log(audioSrc, '音频播放完毕:') if ( !isNaN(this.audioElement.duration) && this.serverMessages[serverMessagesIndex].isDuration ) { // 加入长度 this.serverMessages[serverMessagesIndex].audioDuration += this.audioElement.duration this.serverMessages[serverMessagesIndex].audioDuration = +this.serverMessages[serverMessagesIndex].audioDuration.toFixed(0) } // 释放创建的URL对象 URL.revokeObjectURL(audioSrc) // 更新索引以播放下一个音频 currentAudioIndex++ // 播放下一个音频 this.playNextAudio(wavBlobs, currentAudioIndex, serverMessagesIndex) } } else { console.log('所有音频播放完毕') this.serverMessages[serverMessagesIndex].voicePlay = false this.serverMessages[serverMessagesIndex].isDuration = false return -1 } },大功告成!五 参考资料WebRTC本地实现 - WebRTC通信实现(Web端) 【Web安全】Permissions Policy(权限策略)详解 实时音视频WebRTC常见问题汇总 Vue使用js-audio-recorder实现录制,播放与下载音频功能 WebRTC 从实战到未来!
2024年10月08日
89 阅读
0 评论
5 点赞
2024-08-15
令人吐血的电赛备赛踩坑
在今年准备电赛的时候,我们选的是H题,自动驾驶小车,在四天三夜紧张的备赛过程中,踩了很多坑,花了很多宝贵时间找问题,最后的原因真的让人哭笑不得。 印象最深的是小车在寻迹的过程中一直左右摆头,不前进,摇头抽搐。 一开始是以为代码写的有问题,改了很多逻辑,都不行。最后趴到小车身上,看它怎么运动怎么寻迹,终于发现了一点端倪,八路寻迹的六号引脚居然一直是暗的! 也就是说六号引脚一直返回小车右侧有黑线,导致小车右转,右转过去左侧碰到黑线,也告诉小车该左转,所以小车一直左右抽搐。 仔细检查后发现是引脚虚焊!而且最吐血的是,它不是一直暗的,在一开始的时候是正常的,一进入寻迹小车一摆动后才会虚掉,时好时不好。 最后重新焊了一下引脚就可以正常寻迹了,代码逻辑上一点问题没有。 还有一个也很折磨,今年电赛H题要求使用TIMSPM0系列开发板,我们选用了TiMSPM0G3507开发板,在使用G3507控制电机驱动的时候,发现有个轮子一直不听话,别的都能反转,它不可以。当时差点崩溃,小车连正常走都不行,还怎么完成比赛问题。这时候也查了改了很多,无语的来了,原因居然是G3507有几个引脚不能输出高低电平,这是使用手册上没有说明的,把控制引脚换了一个就好了。 还有MPU6050的角度值打印,在OLED中显示为0-295度,居然不是正常的0-360,一度以为要放弃6050方案,后来发现OLED输出函数里面接收的是uint_16值,但是6050函数返回的是float类型,出现了隐式的类型转换,使用sprintf将flaot转成字符串即可,角度是正确的-180到180度。 以上都不是最崩溃的,因为那时候里比赛截止还有很长时间,还有其它方案可以选择,最让人碎掉的是在封箱前三小时,我们已经完美完成了全部问题,就在要多调试继续优化的时候,小车突然抽风了,走直线都会向左偏。这下连第一问都跑不了了,更别提其它问别提优化了,强大的落差感压迫着我们,当时用最快的速度找问题。 是轮子滑扣了吗?转了一下确实很松,又换掉一个紧的,居然还是向左偏。也许是其它的轮子也松了?当时是一个小车多赠送两个轮子,但是在调试的时候跑的次数太多很多轮子都滑扣了,想再换都不行了。试了很久发现有的松的轮子换到另一个电机就紧了,又开始争分夺秒的排列组合,总算组出一个轮子还算可以的小车,放到地上一跑,吐血了,还是向左偏。 看来不是轮子的问题,是代码的问题吗?代码一点问题没有,明明同个代码原先跑的很好的。或者我们要根据这个小车重新调整参数了?这时距离小车封箱还有一小时,很难再调整到之前那么完美了。就在我们举棋不定的时候,我举着小车看着它,感叹时也命也,突然小车右后轮当着我面开始转起来。失落灰心惊愕疑惑不解一下交织起来,很难描述当时的心情。 真的哭笑不得,小车右后轮会不听话的转动,导致右后轮转的比别的快,该停下的时候右后轮不停,所以小车会向左偏。仔细排查以后发现,最大的罪魁祸首是杜邦线!测试次数过多,小车晃动太多,杜邦线松掉了一点,信号传输太虚了,导致电机得不到正常的接收,重新插紧就解决了。 避坑!以后能焊的部分还是尽量焊上。 在评测的时候和之前一样,四问全部测完,第四问47s,河北省二。 可能也有点遗憾吧,没有时间优化了,不过最大的还是感慨,感谢这段电赛经历。熬夜通宵压力崩溃喜悦,这不是一场简单的比赛,是一次磨炼一个挑战。感谢自己突破了自我,这种收获难以言表,不经历就难以体会。 附上我的爱车和秦皇岛美景~{gird column="3" gap="15"}{gird-item}{/gird-item}{gird-item}{/gird-item}{gird-item}{/gird-item}{/gird}
2024年08月15日
22 阅读
0 评论
0 点赞
2024-08-09
P标签内文本莫名多空格
今天又出现了一个奇怪的错误,p标签里面有多个文本,但是文本和a标签之间总是多一个空格。检查model_comparison_desc字段,末尾没有空格,使用$t('api_base.model_comparison_desc').trim()去除首尾空格 页面显示的时候依旧有空格。原因竟然是代码格式化的时候换行导致的!出问题代码:<p> {{ $t('api_base.model_comparison_provide') }} {{ $t('api_base.model_comparison_desc') }} <a href="/dev/howuse/model" target="_blank">{{ $t('api_base.model_comparison') }}</a> {{$t('api_base.model_comparison_choose') }} </p>后来将a标签前后不用换行断开就好了,修改后的代码如下:<p> {{ $t('api_base.model_comparison_provide') }}{{ $t('api_base.model_comparison_desc') }}<a href="/dev/howuse/model" target="_blank">{{ $t('api_base.model_comparison') }}</a>{{ $t('api_base.model_comparison_choose') }} </p>避坑!P标签内换行格式化会被浏览器解析成空格元素
2024年08月09日
27 阅读
0 评论
0 点赞
2024-08-08
项目启动一直报后端接口错误
今天开发的时候,项目需要配置后端地址,需要在hosts中添加对应的ip解析使用的SwitchHosts工具管理hosts解析配置但是一开始没有使用管理员打开,导致软件没有hosts写入权限,项目启动后找不到后端ip,报接口错误后来使用管理员权限打开后,接口还是报错,一直请求不到后端地址。重拉取项目,配置node、npm、pnpm版本后,依旧报错又感觉是vue.config.js中devServer.proxy代理配置问题,但是检查后配置没有错。突然灵机一动,打开浏览器设置清除浏览器缓存,问题完美解决。避坑!以后开发都使用无痕浏览!
2024年08月08日
15 阅读
0 评论
0 点赞