📱 海狸IM 移动端开发指南
🏗️ 技术架构全景
🎨 前端层
Vue 3 + TypeScript
现代化前端框架,类型安全
Pinia 状态管理
响应式状态管理方案
自定义组件库
统一的UI组件设计系统
🚀 框架层
uni-app 3.0+
跨平台应用开发框架
Vite 构建工具
极速的开发体验
WebSocket 通信
实时消息推送
📦 平台层
Android
原生 Android 应用
iOS
原生 iOS 应用
小程序
微信小程序支持
📁 项目结构深度解析
📂 核心目录结构
beaver-mobile/
src/源代码目录
api/接口封装
components/可复用组件
pages/页面组件
store/状态管理
utils/工具函数
static/静态资源
pages.json页面配置
manifest.json应用配置
API 层设计
- 统一的请求封装
- 自动错误处理
- 请求/响应拦截器
- Token 自动刷新
组件化开发
- 可复用的UI组件
- 聊天气泡组件
- 多媒体消息组件
- 通用工具组件
状态管理
- 用户信息管理
- 聊天会话状态
- 消息列表缓存
- 应用配置存储
🚀 环境搭建
1. 克隆项目
bash
git clone https://github.com/wsrh8888/beaver-mobile.git
cd beaver-mobile
2. 安装Node.js
bash
# 使用nvm安装Node.js 20
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
# 验证安装
node --version # 应该显示 v20.x.x
npm --version
3. 安装开发工具
HBuilderX (推荐)
- 下载HBuilderX: https://www.dcloud.io/hbuilderx.html
- 安装uni-app插件
- 配置开发环境
VS Code (可选)
bash
# 安装推荐插件
# - uni-app-schemas
# - uni-app-snippets
# - Vetur 或 Vue Language Features (Volar)
4. 安装项目依赖
bash
# 环境要求:Node.js 20
npm install
# 或者使用yarn
yarn install
⚙️ 项目配置
1. 页面配置 (pages.json)
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "海狸IM"
}
},
{
"path": "pages/chat/chat",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false
}
},
{
"path": "pages/contacts/contacts",
"style": {
"navigationBarTitleText": "联系人"
}
}
],
"tabBar": {
"color": "#8A8A8A",
"selectedColor": "#FF7D45",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/icons/chat.png",
"selectedIconPath": "static/icons/chat-active.png",
"text": "聊天"
},
{
"pagePath": "pages/contacts/contacts",
"iconPath": "static/icons/contacts.png",
"selectedIconPath": "static/icons/contacts-active.png",
"text": "联系人"
},
{
"pagePath": "pages/profile/profile",
"iconPath": "static/icons/profile.png",
"selectedIconPath": "static/icons/profile-active.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "海狸IM",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8"
}
}
2. 应用配置 (manifest.json)
json
{
"name": "beaver-mobile",
"appid": "__UNI__BEAVER_IM",
"description": "海狸IM移动端",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {
"Camera": {},
"Gallery": {},
"Audio": {},
"VideoPlayer": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\" />",
"<uses-permission android:name=\"android.permission.VIBRATE\" />",
"<uses-permission android:name=\"android.permission.READ_LOGS\" />",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />",
"<uses-permission android:name=\"android.permission.CAMERA\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\" />",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />"
]
},
"ios": {
"privacyDescription": {
"NSCameraUsageDescription": "用于拍照发送图片消息",
"NSPhotoLibraryUsageDescription": "用于选择相册图片发送",
"NSMicrophoneUsageDescription": "用于录制语音消息",
"NSLocationWhenInUseUsageDescription": "用于发送位置信息"
}
}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"h5": {
"title": "海狸IM",
"template": "index.html"
}
}
📋 开发流程
1. 启动开发环境
bash
# H5开发
npm run dev:h5
# 微信小程序开发
npm run dev:mp-weixin
# Android开发
npm run dev:app-android
# iOS开发
npm run dev:app-ios
# 或者根据项目实际配置
npm run build_test
2. API接口封装
javascript
// src/api/request.js
import { CONFIG } from '@/config'
class Request {
constructor() {
this.baseURL = CONFIG.API_BASE_URL
this.timeout = 10000
}
request(options) {
return new Promise((resolve, reject) => {
// 获取token
const token = uni.getStorageSync('token')
uni.request({
url: this.baseURL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
timeout: this.timeout,
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 200) {
resolve(res.data)
} else {
uni.showToast({
title: res.data.message || '请求失败',
icon: 'none'
})
reject(res.data)
}
} else {
reject(res)
}
},
fail: (err) => {
uni.showToast({
title: '网络异常',
icon: 'none'
})
reject(err)
}
})
})
}
get(url, data = {}) {
return this.request({ method: 'GET', url, data })
}
post(url, data = {}) {
return this.request({ method: 'POST', url, data })
}
put(url, data = {}) {
return this.request({ method: 'PUT', url, data })
}
delete(url, data = {}) {
return this.request({ method: 'DELETE', url, data })
}
}
export default new Request()
javascript
// src/api/user.js
import request from './request'
export const userApi = {
// 用户登录
login(data) {
return request.post('/api/user/login', data)
},
// 用户注册
register(data) {
return request.post('/api/user/register', data)
},
// 获取用户信息
getUserInfo() {
return request.get('/api/user/info')
},
// 更新用户信息
updateUserInfo(data) {
return request.put('/api/user/info', data)
},
// 上传头像
uploadAvatar(filePath) {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')
uni.uploadFile({
url: CONFIG.API_BASE_URL + '/api/upload/avatar',
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${token}`
},
success: (res) => {
const data = JSON.parse(res.data)
if (data.code === 200) {
resolve(data)
} else {
reject(data)
}
},
fail: reject
})
})
}
}
3. WebSocket连接管理
javascript
// src/utils/websocket.js
class WebSocketManager {
constructor() {
this.ws = null
this.reconnectTimer = null
this.heartbeatTimer = null
this.isConnected = false
this.reconnectCount = 0
this.maxReconnectCount = 5
this.messageHandlers = new Map()
}
connect(url, token) {
if (this.ws) {
this.close()
}
this.ws = uni.connectSocket({
url: `${url}?token=${token}`,
success: () => {
console.log('WebSocket连接成功')
},
fail: (err) => {
console.error('WebSocket连接失败:', err)
}
})
this.ws.onOpen(() => {
console.log('WebSocket已连接')
this.isConnected = true
this.reconnectCount = 0
this.startHeartbeat()
})
this.ws.onMessage((message) => {
try {
const data = JSON.parse(message.data)
this.handleMessage(data)
} catch (error) {
console.error('消息解析失败:', error)
}
})
this.ws.onClose(() => {
console.log('WebSocket连接关闭')
this.isConnected = false
this.stopHeartbeat()
this.reconnect()
})
this.ws.onError((error) => {
console.error('WebSocket错误:', error)
this.isConnected = false
})
}
send(data) {
if (this.isConnected && this.ws) {
this.ws.send({
data: JSON.stringify(data),
success: () => {
console.log('消息发送成功')
},
fail: (err) => {
console.error('消息发送失败:', err)
}
})
}
}
handleMessage(data) {
const { type } = data
const handler = this.messageHandlers.get(type)
if (handler) {
handler(data)
} else {
console.log('未处理的消息类型:', type)
}
}
onMessage(type, handler) {
this.messageHandlers.set(type, handler)
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.send({ type: 'ping' })
}, 30000) // 30秒心跳
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
reconnect() {
if (this.reconnectCount >= this.maxReconnectCount) {
console.log('达到最大重连次数,停止重连')
return
}
this.reconnectTimer = setTimeout(() => {
console.log(`第${this.reconnectCount + 1}次重连...`)
this.reconnectCount++
const token = uni.getStorageSync('token')
if (token) {
this.connect(CONFIG.WS_URL, token)
}
}, 3000 * Math.pow(2, this.reconnectCount)) // 指数退避
}
close() {
if (this.ws) {
this.ws.close()
this.ws = null
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.stopHeartbeat()
this.isConnected = false
}
}
export default new WebSocketManager()
4. 状态管理
javascript
// src/store/index.js
import { createPinia, defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: '',
isLogin: false
}),
getters: {
avatar: (state) => state.userInfo?.avatar || '/static/images/default-avatar.png',
nickname: (state) => state.userInfo?.nickname || '用户'
},
actions: {
async login(loginData) {
try {
const res = await userApi.login(loginData)
this.userInfo = res.data.userInfo
this.token = res.data.token
this.isLogin = true
// 保存到本地存储
uni.setStorageSync('userInfo', this.userInfo)
uni.setStorageSync('token', this.token)
uni.setStorageSync('isLogin', true)
return res
} catch (error) {
throw error
}
},
logout() {
this.userInfo = null
this.token = ''
this.isLogin = false
// 清除本地存储
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
uni.removeStorageSync('isLogin')
// 关闭WebSocket连接
WebSocketManager.close()
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/login'
})
},
loadUserFromStorage() {
const userInfo = uni.getStorageSync('userInfo')
const token = uni.getStorageSync('token')
const isLogin = uni.getStorageSync('isLogin')
if (userInfo && token && isLogin) {
this.userInfo = userInfo
this.token = token
this.isLogin = isLogin
}
}
}
})
export const useChatStore = defineStore('chat', {
state: () => ({
conversationList: [],
currentConversation: null,
messageList: []
}),
actions: {
addMessage(message) {
this.messageList.push(message)
},
updateMessageStatus(messageId, status) {
const message = this.messageList.find(m => m.id === messageId)
if (message) {
message.status = status
}
}
}
})
5. 页面开发示例
vue
<!-- src/pages/chat/chat.vue -->
<template>
<view class="chat-container">
<!-- 消息列表 -->
<scroll-view
class="message-list"
:scroll-top="scrollTop"
scroll-y
@scrolltoupper="loadMoreMessages"
>
<view
v-for="message in messageList"
:key="message.id"
class="message-item"
:class="{ 'own-message': message.senderId === userInfo.id }"
>
<image
class="avatar"
:src="message.senderAvatar || '/static/images/default-avatar.png'"
/>
<view class="message-content">
<text class="message-text">{{ message.content }}</text>
<text class="message-time">{{ formatTime(message.timestamp) }}</text>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="input-area">
<input
v-model="inputText"
class="message-input"
placeholder="输入消息..."
@confirm="sendMessage"
confirm-type="send"
/>
<view class="input-actions">
<button @click="chooseImage" class="action-btn">📷</button>
<button @click="sendMessage" class="send-btn" :disabled="!inputText.trim()">
发送
</button>
</view>
</view>
</view>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useUserStore, useChatStore } from '@/store'
import WebSocketManager from '@/utils/websocket'
export default {
setup() {
const userStore = useUserStore()
const chatStore = useChatStore()
const inputText = ref('')
const scrollTop = ref(0)
const userInfo = computed(() => userStore.userInfo)
const messageList = computed(() => chatStore.messageList)
const sendMessage = () => {
if (!inputText.value.trim()) return
const message = {
id: Date.now(),
content: inputText.value,
senderId: userInfo.value.id,
senderAvatar: userInfo.value.avatar,
timestamp: Date.now(),
status: 'sending'
}
chatStore.addMessage(message)
// 发送WebSocket消息
WebSocketManager.send({
type: 'message',
data: message
})
inputText.value = ''
scrollToBottom()
}
const chooseImage = () => {
uni.chooseImage({
count: 1,
sourceType: ['album', 'camera'],
success: (res) => {
const filePath = res.tempFilePaths[0]
uploadImage(filePath)
}
})
}
const uploadImage = (filePath) => {
uni.uploadFile({
url: CONFIG.API_BASE_URL + '/api/upload/image',
filePath: filePath,
name: 'file',
header: {
'Authorization': `Bearer ${userStore.token}`
},
success: (res) => {
const data = JSON.parse(res.data)
if (data.code === 200) {
const message = {
id: Date.now(),
type: 'image',
content: data.data.url,
senderId: userInfo.value.id,
timestamp: Date.now()
}
chatStore.addMessage(message)
WebSocketManager.send({
type: 'message',
data: message
})
}
}
})
}
const formatTime = (timestamp) => {
const date = new Date(timestamp)
return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`
}
const scrollToBottom = () => {
scrollTop.value = 999999
}
onMounted(() => {
// 监听WebSocket消息
WebSocketManager.onMessage('message', (data) => {
chatStore.addMessage(data.data)
scrollToBottom()
})
})
return {
userInfo,
messageList,
inputText,
scrollTop,
sendMessage,
chooseImage,
formatTime
}
}
}
</script>
<style lang="scss" scoped>
.chat-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.message-list {
flex: 1;
padding: 20rpx;
}
.message-item {
display: flex;
margin-bottom: 30rpx;
&.own-message {
flex-direction: row-reverse;
}
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin: 0 20rpx;
}
.message-content {
max-width: 60%;
background: #f0f0f0;
padding: 20rpx;
border-radius: 20rpx;
}
.own-message .message-content {
background: #FF7D45;
color: white;
}
.input-area {
display: flex;
align-items: center;
padding: 20rpx;
background: white;
border-top: 1px solid #eee;
}
.message-input {
flex: 1;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 30rpx;
margin-right: 20rpx;
}
.input-actions {
display: flex;
gap: 10rpx;
}
.action-btn, .send-btn {
padding: 10rpx 20rpx;
border-radius: 20rpx;
border: none;
background: #FF7D45;
color: white;
}
.send-btn:disabled {
background: #ccc;
}
</style>
📱 平台适配
1. Android适配
javascript
// Android平台特殊处理
// #ifdef APP-PLUS-ANDROID
// Android专用代码
// #endif
2. iOS适配
javascript
// iOS平台特殊处理
// #ifdef APP-PLUS-IOS
// iOS专用代码
// #endif
3. 小程序适配
javascript
// 微信小程序适配
// #ifdef MP-WEIXIN
// 微信小程序专用代码
// #endif
🔧 构建和发布
1. 构建命令
bash
# 构建H5
npm run build:h5
# 构建微信小程序
npm run build:mp-weixin
# 构建App
npm run build_test
2. 发布流程
Android发布:
- 在HBuilderX中选择"发行" -> "原生App-云打包"
- 配置签名证书
- 生成APK文件
iOS发布:
- 配置苹果开发者证书
- 云打包生成ipa文件
- 上传到App Store
小程序发布:
- 在微信开发者工具中上传代码
- 提交审核
- 发布上线
🐛 调试技巧
1. 控制台调试
javascript
// 使用uni.showModal替代alert
uni.showModal({
title: '调试信息',
content: JSON.stringify(data),
showCancel: false
})
// 使用console.log
console.log('调试信息:', data)
2. 真机调试
- Android: 使用ADB连接真机调试
- iOS: 使用Safari远程调试
- 小程序: 微信开发者工具真机预览
📚 相关资源
如果在开发过程中遇到问题,欢迎在GitHub Issues中提问或加入QQ群(1013328597)交流!