前言
本教程采用 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、后端部署
-
- 配置 Nginx 反向代理,启用 HTTPS
- 数据库使用云数据库(RDS),开启备份
结语
通过 7 天的学习,你已掌握 uni-app 社交 App 的核心开发能力,包括 WebSocket 即时聊天、朋友圈图文发布、用户关注等功能,同时理解后端接口的设计逻辑。实际开发中可扩展更多功能,如消息通知、私信列表、朋友圈话题等。源码可根据需求调整,建议结合官方文档进一步优化。

被折叠的 条评论
为什么被折叠?



