如果你也在写一个uniapp项目...

4,824 阅读11分钟

人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......

前置准备:

  1. 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
  2. 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
  3. 微信认证。300元
  4. 小程序备案。在前面流程完成之后才能进行小程序的备案

image.png

审核流程

整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话

  1. 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片

文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取

  1. 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标 image.png
  2. 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时

image.png

image.png 5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天

开发过程

  1. 文件上传。以往网页开发中涉及文件上传的业务都是new FormData,然后再append必要的字段。但是,小程序中使用FormData会报错,所以,得使用uniapp自带的uni.uoloadFile
  2. 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按navigateBackuni.showToast,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容
  3. 分享功能。小程序的分享功能需要在onShareAppMessage(分享至好友)或者onShareTimeline(分享至朋友圈)调用。这两个是和onLoad同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app中导入
  4. 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅

先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅

  1. webSocket。小程序中的树洞评论功能我们选用的是webSocket,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制踩坑:用户退出的时候需要关闭,但是移动端中,用户退出的方式有两种:a. 点击顶部导航的退出按钮;b. 滑动页面退出;所以关闭的逻辑可以写到onUnmounted或者onUnload中,不要写到特定的事件回调中(因为你还得分别处理点击回退事件和页面滑动回退事件)

这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义

  1. 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验

image.png

image.png

  1. 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面scroll事件。但是,scroll涉及大量的计算;后面采用Intersection Observer。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver,二者语法差不多
  2. 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。
  3. 有关defineProps父传子的变量修改问题:Vue中通过props传递的变量是只读的,这点老是忘,真需要修改的话可以在子组件创建一个变量保存传递过来的值,不然会有类似如下的警告: Set operation on key "keyword" failed: target is readonly.

大概暂时先能想到这么多,后面有想到再接着补充......

webSocket心跳机制和断线重连

心跳机制

实现思路需要前后端沟通约定发送什么消息即为心跳消息,此时服务端收到该条消息时,无需处理;客户端收到这条返回的心跳,也无需做任何操作,因为心跳的本质只是维护webSocket的连接状态。为了区分开谁是心跳包消息、谁是用户发送的消息,这里采取的方案时是使用一个code字段来区分

取什么不是关键,关键是前后端约定为什么

// webSocket实例
let socketTask = null
// webSocket当前连接状态
const socketStatus = ref(false)
// 心跳包定时器实例
let heartTimer = null
// 延时实例
let waitTimer = null
// 重连定时器实例
let reconnectTimer = null
// 心跳包发送时间间隔
const HEART_TIME = 5 * 1000
// 延时等待服务器响应的时间
const SERVER_TIME = 3 * 1000
// 短线重连相应的时间
const RECONNECT_TIME = 10 * 1000

const connectSocket = () => {
    socketTask = uni.connectSocket({
        url: `wss://sfb.asia/ws/${props.id}`,
        success: (val) => {
                console.log('主评论-连接成功', val)
        }
    })
}

connectSocket()

socketTask.onMessage((data) => {
	console.log('接收的信息', data)
	if (newChats.code === 100) {
            // 第一次进入接收的消息...
	} else if (newChats.code === 102) {
           // 接收心跳包消息...
	} else {
           // 接收新发送的消息...
	}
})

前端使用定时器每隔一段时间(比如3s)发送约定好的心跳消息,发送完之后,先延时一段时间(比如5s),如果你的webSocket还在,那么延时完这段时间之后是能正常接收且走else if (newChats.code === 102)这个分支的逻辑的。在websocket连接成功之后,我们就开始发送心跳

const connectSocket = () => {
    socketTask = uni.connectSocket({
        url: `wss://sfb.asia/ws/${props.id}`,
        success: (val) => {
                console.log('主评论-连接成功', val)
        }
    })
}

connectSocket()
socketTask.onOpen((res) => {
	console.log('连接成功 onOpen')
	// 连接成功时开启心跳
	sendHeartBag()
})

// 发送心跳包
const sendHeartBag = () => {
    // 每隔5s发送一次心跳包维持连接
    heartTimer = setTimeout(() => {
        socketTask.send({
            data: 'HEART_BEAT',
            success: () => {
                    console.log('主评论-发送心跳包成功', new Date())
            },
            fail: () => {
                    console.log('主评论-发送心跳包失败', new Date())
            }
        })
        // 等待服务器推送回来心跳
        waitingServer()
    }, HEART_TIME)
}

// 延时等待服务器响应
const waitingServer = () => {
    socketStatus.value = false
    waitTimer = setTimeout(() => {
        // 如果延时期间能正常接收信息,socketStatus会自动变为true,此时就只需要再发心跳包
        if (socketStatus.value) {
            heartTimer = null
            sendHeartBag()
            return
        }
        console.log('心跳无响应,已断线')
        // 关闭连接
        try {
            socketTask.close({
                success: () => {
                    console.log('关闭成功')
                },
                fail: () => {
                    console.log('关闭失败')
                }
            })
            socketTask = null
        } catch (e) {
                console.log('主评论-webSocket已关闭,无需重复关闭')
        }
        // 重连
        // reconnectWebSocket()
    }, SERVER_TIME)
}

调用sendHeartBag发送心跳包之后调用waitingServer,显式的在waitingServer中将socketStatus改为false,为的就是检测websocket还正常连接。如果是,那么会在onMessage字段再次把状态修改回来

// 接收消息
socketTask.onMessage((data) => {
    const newChats = data.data
    console.log('接收的信息', newChats)
    // 第一次推送(历史消息推送)
    if (newChats.code === 100) {
    } else if (newChats.code === 102) {
        console.log('接收到心跳')
        // 延时服务器接收到心跳,更新状态
        socketStatus.value = true
    } else {
        // newChats.code === 101,此时是新消息的推送
        // 数据追加
        // 如果有发送消息,则不需要再发送心跳包
        clearTimeout(heartTimer)
        sendHeartBag()
    }
})

断线重连

在前面我们已经实现了心跳,通过waitingServer延时来判断是否能再次接收心跳包,进而判断是否已断线,如果确确实实断线了,就需要执行重连操作:

// 延时等待服务器响应
const waitingServer = () => {
    socketStatus.value = false
    waitTimer = setTimeout(() => {
        // ...前面代码省略
        // 重连
        reconnectWebSocket()
    }, SERVER_TIME)
}

// 断线重连
const reconnectWebSocket = () => {
    reconnectTimer = setTimeout(() => {
        // 重置状态
        socketTask = null
        clearTimeout(reconnectTimer)
        clearTimeout(waitTimer)
        clearTimeout(heartTimer)
        // 重新连接
        connectSocket()
    }, RECONNECT_TIME)
}

socketTask.onClose(() => {
	console.log('断开连接,close回调')
})

socketTask.onError(() => {
	console.log('连接失败,error回调')
	// 出错则重连(一直出错就一直重连)
	reconnectWebSocket()
})

在无法接收到心跳包之后,尝试重连,执行reconnectWebSocket。但由于服务器断网或用户断网,会触发socketTask.onError事件,该事件中又会不断触发重连,不断进行计时,直到网络正常

踩坑

webSocket的关闭一定要在onMounted或者onUnload执行,因为用户可能是点击退出按钮退出,也可能是滑动页面退出

onUnmounted(async () => {
	// 由于用户可能是点击回退/滑动页面回退,所以统一在组件卸载的时候处理逻辑
	clearTimeout(reconnectTimer)
	clearTimeout(waitTimer)
	clearTimeout(heartTimer)
	if (socketTask) {
    socketTask.close({
        success: () => {
                console.log('关闭成功')
        },
        fail: () => {
                console.log('关闭失败')
        }
    })
    socketTask = null
	}
})

定时器一定一定一定要清除!!!而且是将所有定时器都清除,而不是只清除一个。不然就会出现:页面退出了,但是心跳包一直在发送

v-if/v-show实现组件缓存

在小程序中有一个很通用的业务场景:Tabs切换不同的栏目

但是根据uniapp官方文档,微信小程序是不支持keepalive缓存的,那么当Tabs进行切换时,往往会有两种不同的处理方案:

  1. v-if控制,但每次切换都要销毁一个Tab、新建一个Tab
  2. v-show控制,但初始化时需要新建所有的Tab

image.png

项目采用的方案是结合v-if/v-show。实现的效果:起初只需要初始化一个Tab,后续Tab切换过程中,没有创建过的则新建,创建过的直接控制显隐,而非销毁

一句话:初始化时不需要创建所有Tabs,减少了初次创建的时间;切换Tab时不用销毁创建,减少了性能开销

<up-tabs :list="list1" @click="click"></up-tabs>

const tabsList = ref([
	{ name: 'Tab1', type: 1, page: 1, hasMore: true, list: [], rendered: true },
	{ name: 'Tab2', type: 2, page: 2, hasMore: true, list: [], rendered: false },
	{ name: 'Tab3', type: 3, page: 2, hasMore: true, list: [], rendered: false }
])
  
<view v-for="(tab, index) in tabsList" :key="tab.type" v-show="tabIndex === index">
  <!-- 传递给子组件对应类别的帖子列表 -->
  <postList :list="tab.list" :type="tab.type" v-if="tab.rendered"></postList>
</view>

const onChange = (e) => {
	// 先更新下标,再更新状态
	tabIndex.value = e.index

	if (!postTabs.value[tabIndex.value].rendered) {
		postTabs.value[tabIndex.value].rendered = true
	}
}



const tabList = ref([
  {type: 1, page: 1, pageSize: 10, list: [], isRender: true },
  {type: 2, page: 1, pageSize: 10, list: [], isRender: false },
  {type: 3, page: 1, pageSize: 10, list: [], isRender: false },
])

<view v-for="(tab, index) in tabList" v-show="index === activeIndex">
  <List :type="tab.type" v-if="tab.inRender" :list="tab.list"/>    
</view>

const onChange = (currentIndex) => {
  activeIndex.value = currentIndex
  if(!tabList.value[currentIndex].isRender) {
    tabList.value[currentIndex].isRender = true
  }
}

方案选择:

  • 如果Tabs的数量本身不多、数据量不大,其实这种方案也没多大必要
  • 组件库的选择,不同的组件库对于Tabs的处理本身就是不同的,比如vant
<van-tabs v-model:active="active">
  <van-tab title="标签 1">内容 1</van-tab>
  <van-tab title="标签 2">内容 2</van-tab>
  <van-tab title="标签 3">内容 3</van-tab>
</van-tabs>

Tabs下本身就需要强依赖于单个的van-tab,那么根本就无需采用上面的方案进行处理。但有的组件库(比如项目中用的uView plus),对于Tabs的处理就是一个简单的文本展示,则可以考虑上述方案

image.png uview plus -> Tabs组件

后记

其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:

  1. 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
  2. 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
  3. ......

然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。

大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见

更新时间:2025.05.07