人员搭配和两个舍友,一前两后,从开发到最终上线,耗时两个月,此篇文章记录一下整个开发流程中踩的坑......
前置准备:
- 资质核验。需要企业认证才能发布社交类的小程序。需要上传营业执照、法人信息等
- 类目选择。小程序中有个类目选择,选择自己小程序涉及的类目,但这里就比较抽象了(下文讲)
- 微信认证。300元
- 小程序备案。在前面流程完成之后才能进行小程序的备案
审核流程
整个审核流程给我的感觉就是跟便秘一样,交一次吐一句,交一次吐一句,然后打回来重改 这里记录一下几个需要注意的点,如果你和我一样也是做UGC类的小程序的话
- 微信审核。所有但凡涉及言论的,能展示出你的思想的、个性的,对不起,请通通接入微信审核。包括但不限于用户昵称、头像;发布的帖子内容、图片
文字审核算比较快,图片审核就有点慢,且图片审核不是很精准。为了避免等待检测而让用户在界面等待过久的问题,后面无奈自己搭了个后台管理。以致于流程就变成了:用户发点什么东西 -> 后端调用微信检测接口 -> 检测完甩到后台 -> 后台管理员做个二次审核 -> 通过后在小程序获取
- 不能使用微信icon图标。之前做微信快捷登录,想着搞个微信的icon图标好看点,结果给我审核失败的原因就是不能使用微信图标
- 请预留足够的时间和审核慢慢耗。以上讲到的还只是真正发布之前踩的坑,等所有条件都符合后,算可以真正的发布,然后提示你首次提交一般需要7个自然日,然后就真的是7*24小时
5. 第一次代码真正预上线之后,此后更新代码再发布新的包,一半就只需要1~2天
开发过程
- 文件上传。以往网页开发中涉及文件上传的业务都是
new FormData,然后再append必要的字段。但是,小程序中使用FormData会报错,所以,得使用uniapp自带的uni.uoloadFile - 消息提示。在帖子发布成功之后等场景都会涉及到消息提示,一般涉及消息提示、页面回退,执行顺序请按
navigateBack再uni.showToast,如果二者调用顺序反了的话,只会页面回退,不会显示提示内容 - 分享功能。小程序的分享功能需要在
onShareAppMessage(分享至好友)或者onShareTimeline(分享至朋友圈)调用。这两个是和onLoad同级的,如果你的技术选型也是Vue3的话,可以从@dcloudio/uni-app中导入 - 消息订阅。小程序涉及到一个需求:当用户发布帖子,微信审核+后台审核都通过之后会通知用户,此时就需要进行消息订阅
先在微信公众号平台开通消息订阅模板,拿到模板ID,前端中再调用 uni.requestSubscribeMessage 传入拿到的模板ID就可以实现消息订阅
- webSocket。小程序中的树洞评论功能我们选用的是
webSocket,小程序中我们没有使用三方库,调用的是uniapp的uni.connectSocket,创建一个webSocket实例,然后处理对应的回调。由于用户一段时间内如果不发送消息,服务端也就没东西推送过来,webSocket自己会断掉。所以我们引入了心跳机制和断线重连机制踩坑:用户退出的时候需要关闭,但是移动端中,用户退出的方式有两种:a. 点击顶部导航的退出按钮;b. 滑动页面退出;所以关闭的逻辑可以写到onUnmounted或者onUnload中,不要写到特定的事件回调中(因为你还得分别处理点击回退事件和页面滑动回退事件)
这里再次吐槽微信的内容审核机制。原先选用webSocket的原因就是看中了它的实时推送,但是接入了内容审核就变得很抽象,时而秒通过,时而得等一下,这也搞得失去了选用webSocket的意义
- 请求池。小程序中使用到了tabs组件,当tab在切换过程中,比如tabA切换至tabB,由于需要请求数据,所以页面会有短暂的白屏时间,这里采用的是请求池,在获取第一个tab的列表数据的时候,由请求池顺便把之后的tab的内容也请求回来,此后在进行tab切换时就可以避免白屏,优化用户体验
- 响应式布局。小程序在个人页模仿了小红书个人页的实现形式,也就是随着页面的滚动,页面的布局(主要是用户头像缩小并由透明度为0逐渐变为1)发生变化。一开始采用的是监听页面
scroll事件。但是,scroll涉及大量的计算;后面采用Intersection Observer。但是注意,uniapp不能直接使用这个API,得调用uni.createIntersectionObserver,二者语法差不多 - 防抖节流的使用。页面滚动加载下一页使用防抖,按钮点击进行节流,常规操作。
- 有关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进行切换时,往往会有两种不同的处理方案:
- v-if控制,但每次切换都要销毁一个Tab、新建一个Tab
- v-show控制,但初始化时需要新建所有的Tab
项目采用的方案是结合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的处理就是一个简单的文本展示,则可以考虑上述方案
uview plus -> Tabs组件
后记
其实校园小程序这个题材市面上早已烂大街,说不上有任何的创新。此前决定搞这个的原因有以下几点:
- 很多东西只停留于理论,没有实操。就像webSocket的心跳和断线重连,博客教你怎样怎样,始终不如你自己去亲手实现一下,这个算主要驱动原因
- 这个程序刚好拿去参加学校的比赛,拿奖有钱doge
- ......
然后整一个项目跟下来吧,给我的感觉就是:技术重要吗?重要;但也不太重要,技术的重要性就跟我这句废话一样。因为一个东西,推广不起来,没人用,你就没动力去更新、去维护。当然,有没有人用对我们此次的开发来说还不算是必选项。
大家觉得校园社交类的程序还能集成什么功能,也欢迎提出您的宝贵意见
更新时间:2025.05.07