code学习

【uniapp实战笔记】聊天APP的开发踩坑记录

最近工作重心转移到了 uniapp,有一说一,这个框架跨端确实优秀,一套代码能一次编译到多端使用。

但随之而来的是层出不穷的兼容性问题,同样地在面临APP底层的改动也显得力不从心。同时,uniapp 的性能问题也是一直被人所诟病的,在这方面上,一还是要提高自己本身的编码能力,二还是得依靠 dcloud 团队持续优化框架。

本期是给大家分享使用 uniapp 开发一个聊天APP的踩坑问题。

一、输入框吞字,光标闪动问题

uniapp 中使用输入框,无论是 input 组件还是 textarea 组件,都存在一个问题。

如果组件绑定 v-model 的话,会有两个BUG:

①输入时,在苹果手机或者使用部分特殊有待选区域的输入法,会存在输入框吞字;

②从文字中间输入时,光标会闪动到最后。

该问题的解决方案,最终只能使用 :value 去绑定输入框,为输入框分配两个变量,一个是真实的 value 值,一个是临时的 tempValue 值。

临时的 tempValue 值用于在输入触发的 @input 事件内实时接收,真实的 value 值只在第一次由无值到有值的时候接收一次,后面只需在发送时设置真实 value 值为空即可清空输入框。

<template>
    <textarea :value="value" @input="handleInput"></textarea>
    <button @click="handleClickSend">发送</button>
</template>

export default {
    data: {
        value: '',
        tempValue: ''
    }
    handleInput(event) {
        const value = event.detail.value
        if(!this.value) {
            // 第一次值为空时赋给真实值
            this.value = value
        }
        this.tempValue = value // 临时值用于实时接收
    }
    asynchandleClickSend() {
        if(!this.value) {
            return
        }
        // 发送时,临时值存储的为当前输入框内的值
        // 随后将真实value值设置为空实现清空输入框
        // 模拟发送请求
        const res = await this.$axios.post({
            url: xxxx,
            text: this.tempValue 
        })
        this.value = '' // 清空输入框
    }
}      

二、组件key值问题

博主封装了一个消息气泡渲染组件,只需要将必要的参数传递进去即可渲染各种消息,但是在开发过程中,发现了气泡消息抛出来长按事件所带的参数错乱的问题,后经研究为组件加上了 key 值才解决。

<template>
    <viewclass="chat-warp"v-for="item of msgList":key="item.id">
        <!-- 注意,需要给组件也给key值 -->
        <chat-item:item="item":key="item.id"></chat-item>
    </view></template>      

可能是 vue 中对组件的更新机制不同,所以 v-for 循环中的组件也需要给 key 值。

三、消息定位问题

项目中的聊天页,使用的是 scroll-view 配合封装好了的富文本组件来渲染各种消息,由于下拉加载更多消息时,总会有屏幕闪动的现象出现,所以最后是两层 scroll-view 来配合使用,一层是真消息,一层是假消息。

加载更多时,假消息显示、真消息隐藏,等到消息完全渲染定位完毕后再隐藏假消息、显示真消息。

(1)滚动不到最底部的问题

在开发过程中,常常会遇到发送消息或者进入聊天页时,滚不到最底部的情况。

针对这个问题,一开始是不断地瞄点滚动到底部,一进入页面就会触发七八次 goBottom 函数。

后面经过优化,整理了思路,调整为“判断消息是否完全渲染完毕后,再执行goBottom 函数”。

而判断消息是否完全渲染完毕,则需要用到 uni.createSelectorQuery() 这个 API。

// 在onReady钩子中调用goBottom
onReady() {
    this.$nextTick(() => {
        this.goBottom()
    })
}
// 滚动到底部函数
goBottom() {
    this.scrollView = '' // scrollview瞄点置空
    this.$nextTick(async () => {
        const res = await this.checkMsgIsRender('btm') // 检测最底部的消息是否完全渲染完毕
        if(res) {
            this.scrollView = 'bottom' // 瞄点至底部
        } else {
            this.goBottom()
        }
    })
}
// 检测消息是否渲染完毕
checkMsgIsRender(position) {
    let msgID = ''
    if(position === 'btm') {
        // 底部
        // 找到最底部的消息ID
        msgID = xxxx
    } else {
        // 顶部
        // 找到最顶部的消息ID
        msgID = yyyy
    }
    // 返回一个Promise
    return new Promise((resolve) => {
        const query = uni.createSelectorQuery().in(this)
        query.select('#id').boundingClientRect(data => {
            // 存在data,且存在宽和高,视为渲染完毕
            if(data && data.width && data.height) {
                resolve(true)
            } else {
                resolve(false)
            }
        }).exec();
    })
}      

(2)下拉加载更多消息瞄点定位不准的问题

同样地,“下拉加载更多消息”也是存在瞄点不准确的问题,而一开始的解决方案,是简单粗暴地延时两秒钟,这直接导致了用户出现等待时间过长的问题,而且还不一定能准确定位到。

参照滚动到底部的做法,我们举一反三,通过判断消息是否完全渲染完毕来决定瞄点的时机,以确保瞄点定位准确。

// 加载更多
async loadMore() {
    await this.getMoreMsg() // 向服务器获取更多消息或者展示本地消息,该函数不展开
    let location = async () => {
        const res = await this.checkMsgIsRender('top') // 检测最顶部的消息是否完全渲染完毕
        if(res) {
            // 这里还需要找到需要瞄点过去的消息ID,zzzz
            this.scrollView = zzzz // 瞄点到消息zzzz
        } else {
            location ()
        }
    }
    this.scrollView = '' // scrollview瞄点置空
    this.$nextTick(() => {
        location ()
    })
}      

总结

虽然网上对 uniapp 一直存有质疑的声音,但是讲句道理,能够以一套代码运行到多端的技术还是挺厉害的,尽管目前来说这项技术还不是很成熟(性能问题、兼容问题)。

uniapp 能带领我们的项目走向多远,还是可以期待的,当然不能光指望 dcloud 团队,个人代码的写法也很关键,一起继续努力,Keep learning…