uni-app 开发社交类 App:即时聊天(WebSocket)+ 朋友圈(图文发布)+ 用户关注(附后端接口设计思路)

前言

本教程采用 Uni-app + Vue3 + Vite + uView UI + Pinia 技术栈,7 天实现社交 App 核心功能,同步讲解前端交互逻辑与后端接口设计,兼顾多端适配与性能优化,源码可直接复用。


第一章 项目架构设计(Day1)

1.1 技术栈选型

技术

作用

优势

Uni-app

跨端框架

一次编写适配 App、小程序、H5 等多端

Vue3 + Vite

前端框架与构建工具

Composition API 灵活,编译速度快

uView UI

组件库

多端兼容的高颜值交互组件

Pinia

状态管理

管理用户登录态、聊天记录等全局数据

WebSocket

即时通信

实现实时聊天与消息推送

JWT

身份认证

无状态接口鉴权,适配多端

1.2 项目目录架构

social-app/

├─ pages/ # 页面目录

│ ├─ chat/ # 即时聊天页面

│ ├─ moments/ # 朋友圈页面(列表+发布)

│ ├─ user/ # 用户页面(关注+详情)

│ └─ login/ # 登录页面

├─ components/ # 公共组件

│ ├─ chat-item.vue # 聊天消息项

│ ├─ moment-card.vue # 朋友圈卡片

│ └─ follow-btn.vue # 关注按钮

├─ store/ # Pinia状态管理

│ ├─ user.js # 用户信息

│ ├─ chat.js # 聊天数据

│ └─ moments.js # 朋友圈数据

├─ hooks/ # 自定义钩子

│ └─ useSocket.js # WebSocket封装

├─ utils/ # 工具函数

│ ├─ request.js # 接口请求封装

│ └─ upload.js # 图片上传工具

├─ api/ # 接口封装

├─ static/ # 静态资源

└─ manifest.json # 多端配置

第二章 即时聊天实现(WebSocket)(Day2-Day3)

2.1 WebSocket 封装(hooks/useSocket.js)

基于 Uni-app 原生 API 封装,支持自动重连与心跳检测:

import { ref, onUnmounted } from 'vue'

export function useSocket(options) {

const { url, maxReconnect = 5, reconnectInterval = 3000, heartBeatInterval = 5000 } = options

const socketTask = ref(null)

const isConnected = ref(false)

const reconnectCount = ref(0)

const messages = ref([])

// 建立连接

const connect = () => {

socketTask.value = uni.connectSocket({ url, success: handleOpen, fail: handleError })

socketTask.value.onOpen(handleOpen)

socketTask.value.onMessage(handleMessage)

socketTask.value.onError(handleError)

socketTask.value.onClose(handleClose)

}

// 连接成功

const handleOpen = () => {

isConnected.value = true

reconnectCount.value = 0

startHeartBeat()

options.onOpen?.()

}

// 接收消息

const handleMessage = (res) => {

const msg = JSON.parse(res.data)

messages.value.push(msg)

options.onMessage?.(msg)

}

// 连接错误/关闭处理

const handleError = () => {

isConnected.value = false

options.onError?.()

reconnect()

}

const handleClose = () => {

isConnected.value = false

options.onClose?.()

reconnect()

}

// 自动重连

const reconnect = () => {

if (reconnectCount.value >= maxReconnect) {

options.onMaxReconnect?.()

return

}

setTimeout(() => {

reconnectCount.value++

connect()

}, reconnectInterval)

}

// 心跳检测

const startHeartBeat = () => {

setInterval(() => {

if (isConnected.value) {

send({ type: 'heartbeat' })

}

}, heartBeatInterval)

}

// 发送消息

const send = (data) => {

if (isConnected.value) {

socketTask.value.send({ data: JSON.stringify(data) })

} else {

uni.showToast({ title: '连接未建立', icon: 'none' })

}

}

// 关闭连接

const disconnect = () => {

if (socketTask.value) {

socketTask.value.close()

}

}

// 组件卸载时清理

onUnmounted(() => {

disconnect()

})

// 自动连接

if (options.autoConnect !== false) connect()

return { isConnected, reconnectCount, messages, send, connect, disconnect }

}

2.2 单聊页面实现(pages/chat/index.vue)

<template>

<view class="chat-page">

<!-- 消息列表 -->

<scroll-view

class="message-list"

scroll-y

:scroll-top="scrollTop"

@scroll="onScroll"

>

<chat-item

v-for="msg in chatStore.messages"

:key="msg.id"

:msg="msg"

:is-self="msg.senderId === userStore.user.id"

></chat-item>

</scroll-view>

<!-- 输入区域 -->

<view class="input-area">

<input

v-model="inputText"

placeholder="输入消息..."

@confirm="handleSend"

/>

<button @click="handleSend">发送</button>

</view>

</view>

</template>

<script setup>

import { ref, watch } from 'vue'

import { useSocket } from '@/hooks/useSocket'

import { useUserStore } from '@/store/user.js'

import { useChatStore } from '@/store/chat.js'

const userStore = useUserStore()

const chatStore = useChatStore()

const inputText = ref('')

const scrollTop = ref(0)

const needScrollToBottom = ref(true)

// 初始化WebSocket连接

const { send, isConnected } = useSocket({

url: `ws://your-server.com/ws?token=${userStore.token}&targetId=${chatStore.targetId}`,

onOpen: () => uni.showToast({ title: '已连接', icon: 'success' }),

onMessage: (msg) => {

chatStore.addMessage(msg)

needScrollToBottom.value = true

},

onMaxReconnect: () => uni.showModal({ title: '连接失败', content: '请检查网络' })

})

// 发送消息

const handleSend = () => {

if (!inputText.value.trim() || !isConnected.value) return

const msg = {

id: Date.now(),

senderId: userStore.user.id,

receiverId: chatStore.targetId,

content: inputText.value,

type: 'text',

timestamp: new Date().getTime()

}

send(msg)

chatStore.addMessage(msg)

inputText.value = ''

needScrollToBottom.value = true

}

// 自动滚动到底部

watch(needScrollToBottom, (val) => {

if (val) {

// 延迟确保DOM更新完成

setTimeout(() => {

scrollTop.value = 999999

needScrollToBottom.value = false

}, 100)

}

})

</script>

2.3 后端 WebSocket 服务(Node.js 示例)

const WebSocket = require('ws')

const jwt = require('jsonwebtoken')

const wss = new WebSocket.Server({ port: 8080 })

// 存储用户连接

const userConnections = new Map()

wss.on('connection', (ws, req) => {

// 解析连接参数

const params = new URLSearchParams(req.url.slice(1))

const token = params.get('token')

const targetId = params.get('targetId')

// 验证Token

try {

const decoded = jwt.verify(token, 'your-secret-key')

const userId = decoded.userId

userConnections.set(userId, ws)

// 监听客户端消息

ws.on('message', (data) => {

const msg = JSON.parse(data)

// 心跳包忽略

if (msg.type === 'heartbeat') return

// 转发消息给目标用户

const targetWs = userConnections.get(targetId)

if (targetWs && targetWs.readyState === WebSocket.OPEN) {

targetWs.send(JSON.stringify({ ...msg, id: Date.now() }))

}

// 存储消息到数据库

saveMessageToDB(msg)

})

// 连接关闭时清理

ws.on('close', () => {

userConnections.delete(userId)

})

} catch (err) {

ws.close()

}

})

// 保存消息到数据库

async function saveMessageToDB(msg) {

// 实现数据库存储逻辑

console.log('保存消息:', msg)

}

第三章 朋友圈功能实现(图文发布)(Day4-Day5)

3.1 图文发布页面(pages/moments/publish.vue)

支持多图上传与预览:

<template>

<view class="publish-page">

<!-- 文本输入 -->

<textarea

v-model="content"

placeholder="分享你的想法..."

class="content-input"

></textarea>

<!-- 图片上传 -->

<view class="image-upload">

<view class="upload-btn" @click="chooseImages" v-if="images.length < 9">

<u-icon name="plus" size="40"></u-icon>

</view>

<view class="image-item" v-for="(img, index) in images" :key="index">

<image :src="img" mode="aspectFill" class="image"></image>

<u-icon name="close" size="24" @click="deleteImage(index)"></u-icon>

</view>

</view>

<!-- 发布按钮 -->

<button class="publish-btn" @click="publishMoment" :loading="publishing">

发布

</button>

</view>

</template>

<script setup>

import { ref } from 'vue'

import { uploadImages } from '@/utils/upload.js'

import { publishMomentAPI } from '@/api/moments.js'

import { useUserStore } from '@/store/user.js'

const userStore = useUserStore()

const content = ref('')

const images = ref([])

const publishing = ref(false)

// 选择图片

const chooseImages = () => {

uni.chooseImage({

count: 9 - images.length,

sizeType: ['compressed'], // 压缩图片

sourceType: ['album', 'camera'],

success: (res) => {

// 预览图片

images.value = [...images.value, ...res.tempFilePaths]

}

})

}

// 删除图片

const deleteImage = (index) => {

images.value.splice(index, 1)

}

// 发布朋友圈

const publishMoment = async () => {

if (!content.value.trim()) {

uni.showToast({ title: '请输入内容', icon: 'none' })

return

}

publishing.value = true

try {

// 上传图片到服务器

const imgUrls = await uploadImages(images.value)

// 提交朋友圈数据

await publishMomentAPI({

content: content.value,

images: imgUrls,

userId: userStore.user.id

})

uni.showToast({ title: '发布成功', icon: 'success' })

// 返回朋友圈列表页并刷新

setTimeout(() => {

uni.navigateBack({ delta: 1 })

uni.$emit('refreshMoments')

}, 1000)

} catch (err) {

uni.showToast({ title: '发布失败', icon: 'none' })

} finally {

publishing.value = false

}

}

</script>

3.2 图片上传工具(utils/upload.js)

/**

* 多图上传工具

* @param {Array} tempFilePaths - 本地图片路径数组

* @returns {Promise<Array>} 服务器图片URL数组

*/

export const uploadImages = (tempFilePaths) => {

return new Promise((resolve, reject) => {

const uploadTasks = tempFilePaths.map((filePath) => {

return new Promise((res, rej) => {

uni.uploadFile({

url: 'https://your-server.com/api/upload/image',

filePath,

name: 'file',

header: {

'token': uni.getStorageSync('token'),

'Content-Type': 'multipart/form-data'

},

success: (res) => {

const data = JSON.parse(res.data)

if (data.code === 200) {

res(data.data.url)

} else {

rej(data.msg)

}

},

fail: (err) => rej(err.errMsg)

})

})

})

Promise.all(uploadTasks)

.then((urls) => resolve(urls))

.catch((err) => {

uni.showToast({ title: `上传失败: ${err}`, icon: 'none' })

reject(err)

})

})

}

3.3 朋友圈列表页面(pages/moments/index.vue)

支持下拉刷新与上拉加载:

<template>

<view class="moments-page">

<scroll-view

class="moments-list"

scroll-y

@scrolltolower="loadMore"

refresher-enabled

@refresherrefresh="refreshMoments"

>

<moment-card

v-for="moment in momentsList"

:key="moment.id"

:moment="moment"

@like="handleLike"

@comment="handleComment"

></moment-card>

<!-- 加载提示 -->

<view class="loading" v-if="loading">加载中...</view>

<view class="no-more" v-if="noMore">没有更多了</view>

</scroll-view>

</view>

</template>

<script setup>

import { ref, onLoad } from 'vue'

import { getMomentsListAPI } from '@/api/moments.js'

import { useMomentsStore } from '@/store/moments.js'

const momentsStore = useMomentsStore()

const momentsList = ref([])

const page = ref(1)

const pageSize = ref(10)

const loading = ref(false)

const noMore = ref(false)

// 获取朋友圈列表

const getMomentsList = async () => {

loading.value = true

try {

const res = await getMomentsListAPI({ page: page.value, pageSize: pageSize.value })

if (page.value === 1) {

momentsList.value = res.data.list

} else {

momentsList.value = [...momentsList.value, ...res.data.list]

}

noMore.value = res.data.list.length < pageSize.value

} catch (err) {

uni.showToast({ title: '加载失败', icon: 'none' })

} finally {

loading.value = false

uni.stopPullDownRefresh()

}

}

// 下拉刷新

const refreshMoments = () => {

page.value = 1

getMomentsList()

}

// 上拉加载更多

const loadMore = () => {

if (!loading.value && !noMore.value) {

page.value++

getMomentsList()

}

}

// 点赞

const handleLike = async (momentId) => {

await momentsStore.likeMoment(momentId)

// 更新本地列表

const moment = momentsList.value.find(item => item.id === momentId)

if (moment) moment.liked = true

}

// 监听发布事件刷新列表

uni.$on('refreshMoments', refreshMoments)

onLoad(() => {

getMomentsList()

})

</script>

第四章 用户关注功能实现(Day6)

4.1 关注按钮组件(components/follow-btn.vue)

<template>

<button

class="follow-btn"

:class="{ followed: isFollowed }"

@click="handleFollow"

:loading="loading"

>

{{ isFollowed ? '已关注' : '关注' }}

</button>

</template>

<script setup>

import { ref, onMounted } from 'vue'

import { followUserAPI, cancelFollowAPI, checkFollowAPI } from '@/api/user.js'

import { useUserStore } from '@/store/user.js'

const userStore = useUserStore()

const props = defineProps({

targetId: {

type: Number,

required: true

}

})

const emit = defineEmits(['followChange'])

const isFollowed = ref(false)

const loading = ref(false)

// 检查关注状态

const checkFollowStatus = async () => {

try {

const res = await checkFollowAPI({ targetId: props.targetId })

isFollowed.value = res.data.followed

} catch (err) {

console.error('检查关注状态失败:', err)

}

}

// 关注/取消关注

const handleFollow = async () => {

if (loading.value) return

loading.value = true

try {

if (isFollowed.value) {

// 取消关注

await cancelFollowAPI({ targetId: props.targetId })

uni.showToast({ title: '已取消关注', icon: 'none' })

} else {

// 关注

await followUserAPI({ targetId: props.targetId })

uni.showToast({ title: '关注成功', icon: 'success' })

}

isFollowed.value = !isFollowed.value

emit('followChange', isFollowed.value)

} catch (err) {

uni.showToast({ title: '操作失败', icon: 'none' })

} finally {

loading.value = false

}

}

onMounted(() => {

checkFollowStatus()

})

</script>

4.2 关注列表页面(pages/user/follow-list.vue)

<template>

<view class="follow-list-page">

<view class="user-item" v-for="user in followList" :key="user.id">

<image :src="user.avatar" class="avatar"></image>

<view class="user-info">

<view class="nickname">{{ user.nickname }}</view>

<view class="desc">{{ user.signature || '暂无简介' }}</view>

</view>

<follow-btn

:targetId="user.id"

@followChange="handleFollowChange(user.id)"

></follow-btn>

</view>

</view>

</template>

<script setup>

import { ref, onLoad } from 'vue'

import { getFollowListAPI } from '@/api/user.js'

const followList = ref([])

// 获取关注列表

const getFollowList = async () => {

try {

const res = await getFollowListAPI({ userId: uni.getStorageSync('userId') })

followList.value = res.data.list

} catch (err) {

uni.showToast({ title: '加载失败', icon: 'none' })

}

}

// 处理关注状态变化

const handleFollowChange = (userId) => {

// 从列表中移除已取消关注的用户

followList.value = followList.value.filter(user => user.id !== userId)

}

onLoad(() => {

getFollowList()

})

</script>

第五章 后端接口设计思路(Day7)

5.1 技术栈与数据模型

后端技术栈
  • 框架:Node.js(Express/NestJS)或 PHP(ThinkPHP)
  • 数据库:MySQL(关系型数据)+ Redis(缓存 / 会话)
  • 存储:阿里云 OSS / 腾讯云 COS(图片存储)
  • 认证:JWT(JSON Web Token)
核心数据模型

1、用户表(users)

CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  nickname VARCHAR(50) NOT NULL,
  avatar VARCHAR(255),
  signature VARCHAR(200),
  phone VARCHAR(20) UNIQUE,
  password VARCHAR(100) NOT NULL,
  followCount INT DEFAULT 0,
  fanCount INT DEFAULT 0,
  createTime DATETIME DEFAULT CURRENT_TIMESTAMP
);

2、关注关系表(follows)

CREATE TABLE follows (
  id INT PRIMARY KEY AUTO_INCREMENT,
  userId INT NOT NULL, -- 关注者ID
  targetId INT NOT NULL, -- 被关注者ID
  createTime DATETIME DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_user_target (userId, targetId) -- 避免重复关注
);

3、朋友圈表(moments)

CREATE TABLE moments (

id INT PRIMARY KEY AUTO_INCREMENT,

userId INT NOT NULL,

content TEXT,

images JSON, -- 图片URL数组

likeCount INT DEFAULT 0,

commentCount INT DEFAULT 0,

createTime DATETIME DEFAULT CURRENT_TIMESTAMP

);

4、聊天消息表(messages)

CREATE TABLE messages (

id INT PRIMARY KEY AUTO_INCREMENT,

senderId INT NOT NULL,

receiverId INT NOT NULL,

content TEXT,

type VARCHAR(20) DEFAULT 'text', -- text/image

status TINYINT DEFAULT 0, -- 0未读/1已读

createTime DATETIME DEFAULT CURRENT_TIMESTAMP

);

5.2 核心接口设计

5.2.1 用户认证接口

接口

方法

参数

描述

/api/auth/login

POST

phone, password

登录,返回 JWT Token

/api/auth/register

POST

nickname, phone, password

注册

/api/auth/info

GET

-

获取当前用户信息(需 Token)

5.2.2 关注接口
// 关注用户

router.post('/follow/:targetId', authMiddleware, async (req, res) => {

const { userId } = req.user // 从Token解析

const { targetId } = req.params

// 检查是否已关注

const existing = await Follow.findOne({ where: { userId, targetId } })

if (existing) return res.json({ code: 400, msg: '已关注该用户' })

// 事务处理:添加关注关系+更新计数

const transaction = await sequelize.transaction()

try {

await Follow.create({ userId, targetId }, { transaction })

await User.increment('followCount', { where: { id: userId }, transaction })

await User.increment('fanCount', { where: { id: targetId }, transaction })

await transaction.commit()

res.json({ code: 200, msg: '关注成功' })

} catch (err) {

await transaction.rollback()

res.json({ code: 500, msg: '服务器错误' })

}

})

// 其他接口:取消关注、检查关注状态、获取关注列表
5.2.3 朋友圈接口
// 发布朋友圈

router.post('/moments', authMiddleware, async (req, res) => {

const { userId } = req.user

const { content, images } = req.body

try {

const moment = await Moment.create({ userId, content, images })

res.json({ code: 200, data: moment })

} catch (err) {

res.json({ code: 500, msg: '发布失败' })

}

})

// 其他接口:获取列表、点赞、评论
5.2.4 图片上传接口
// 图片上传(Node.js + multer + OSS)

const multer = require('multer')

const upload = multer({ dest: 'tmp/' })

const OSS = require('ali-oss')

const client = new OSS({

accessKeyId: 'your-key',

accessKeySecret: 'your-secret',

bucket: 'your-bucket',

region: 'your-region'

})

router.post('/upload/image', authMiddleware, upload.single('file'), async (req, res) => {

try {

// 上传到OSS

const result = await client.put(`moments/${Date.now()}-${req.file.originalname}`, req.file.path)

// 删除本地临时文件

fs.unlinkSync(req.file.path)

res.json({ code: 200, data: { url: result.url } })

} catch (err) {

res.json({ code: 500, msg: '上传失败' })

}

})

5.3 接口安全与性能优化

1、接口安全

  • 所有接口(除登录 / 注册)需 JWT 鉴权
  • 图片上传限制大小(如 5MB 以内)与格式
  • 接口限流:使用 Redis 实现 IP 限流,防止恶意请求

2、性能优化

    • 朋友圈列表添加 Redis 缓存,减轻数据库压力
    • 图片采用 CDN 加速,生成缩略图减少加载时间
    • 聊天消息分页加载,避免一次性返回过多数据

    第六章 多端适配与上线部署

    6.1 多端适配技巧

    1、样式适配

    /* 微信小程序特有样式 */
    
    /* #ifdef MP-WEIXIN */
    
    .nav-bar { padding-top: 20rpx; }
    
    /* #endif */
    
    /* App特有样式 */
    
    /* #ifdef APP-PLUS */
    
    .nav-bar { padding-top: 40rpx; }
    
    /* #endif */
    • 使用 rpx 单位适配不同屏幕宽度
    • 条件编译处理平台差异:

    2、API 适配

    // 发起支付
    
    const pay = async (orderId) => {
    
    const res = await getPayParamsAPI(orderId)
    
    // #ifdef MP-WEIXIN
    
    uni.requestPayment({ timeStamp: res.data.timeStamp, ...res.data })
    
    // #endif
    
    // #ifdef APP-PLUS
    
    // App端支付逻辑
    
    // #endif
    
    }
    • 支付功能多端处理:

    6.2 部署步骤

    1、前端部署

    • App 端:HBuilderX 打包成 APK/IPA,提交应用商店
    • 小程序:发行→微信 / 支付宝小程序,提交平台审核
    • H5 端:打包后上传至 Nginx 服务器

    2、后端部署

      • 接口服务部署至云服务器(阿里云 ECS / 腾讯云 CVM)
      • 配置 Nginx 反向代理,启用 HTTPS
      • 数据库使用云数据库(RDS),开启备份

      结语

      通过 7 天的学习,你已掌握 uni-app 社交 App 的核心开发能力,包括 WebSocket 即时聊天、朋友圈图文发布、用户关注等功能,同时理解后端接口的设计逻辑。实际开发中可扩展更多功能,如消息通知、私信列表、朋友圈话题等。源码可根据需求调整,建议结合官方文档进一步优化。

      评论
      成就一亿技术人!
      拼手气红包6.0元
      还能输入1000个字符
       
      红包 添加红包
      表情包 插入表情
       条评论被折叠 查看
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值