社交应用实战 #
一、项目概述 #
1.1 功能需求 #
| 模块 | 功能 |
|---|---|
| 用户 | 注册、登录、个人主页 |
| 动态 | 发布、浏览、点赞、评论 |
| 消息 | 即时聊天、消息列表 |
| 通知 | 推送通知、消息提醒 |
| 发现 | 搜索、推荐用户 |
1.2 技术栈 #
- 前端框架: React + TypeScript
- 状态管理: Zustand
- 实时通信: Socket.io
- 原生功能: Capacitor
- UI组件: Tailwind CSS
二、项目初始化 #
2.1 创建项目 #
bash
# 创建项目
npm create vite@latest social-app -- --template react-ts
cd social-app
# 安装依赖
npm install
# 安装Capacitor
npm install @capacitor/core @capacitor/cli
npx cap init "社交应用" "com.example.social"
# 安装插件
npm install @capacitor/camera
npm install @capacitor/push-notifications
npm install @capacitor/local-notifications
npm install @capacitor/preferences
npm install @capacitor/keyboard
npm install @capacitor/haptics
# 安装其他依赖
npm install socket.io-client
npm install zustand
npm install react-router-dom
npm install axios
npm install dayjs
2.2 项目结构 #
text
src/
├── api/
│ ├── index.ts
│ ├── auth.ts
│ ├── posts.ts
│ └── messages.ts
├── components/
│ ├── Post/
│ ├── Message/
│ ├── UserCard/
│ └── Notification/
├── hooks/
│ ├── useSocket.ts
│ ├── useNotifications.ts
│ └── useMedia.ts
├── pages/
│ ├── Home/
│ ├── Explore/
│ ├── Messages/
│ ├── Profile/
│ └── Notifications/
├── store/
│ ├── authStore.ts
│ ├── messageStore.ts
│ └── notificationStore.ts
├── types/
│ └── index.ts
├── utils/
│ ├── socket.ts
│ └── media.ts
├── App.tsx
└── main.tsx
三、实时通信 #
3.1 Socket服务 #
typescript
// src/utils/socket.ts
import { io, Socket } from 'socket.io-client';
import { Capacitor } from '@capacitor/core';
class SocketService {
private socket: Socket | null = null;
private url = 'https://api.example.com';
connect(token: string): Promise<Socket> {
return new Promise((resolve, reject) => {
this.socket = io(this.url, {
auth: { token },
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
this.socket.on('connect', () => {
console.log('Socket connected');
resolve(this.socket!);
});
this.socket.on('connect_error', (error) => {
console.error('Socket connection error:', error);
reject(error);
});
this.socket.on('disconnect', () => {
console.log('Socket disconnected');
});
});
}
disconnect(): void {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
}
on(event: string, callback: (...args: any[]) => void): void {
this.socket?.on(event, callback);
}
off(event: string, callback?: (...args: any[]) => void): void {
this.socket?.off(event, callback);
}
emit(event: string, data: any): void {
this.socket?.emit(event, data);
}
getSocket(): Socket | null {
return this.socket;
}
}
export const socketService = new SocketService();
3.2 消息Store #
typescript
// src/store/messageStore.ts
import { create } from 'zustand';
import { socketService } from '../utils/socket';
interface Message {
id: string;
conversationId: string;
senderId: string;
content: string;
type: 'text' | 'image' | 'video';
createdAt: string;
read: boolean;
}
interface Conversation {
id: string;
participants: { id: string; name: string; avatar: string }[];
lastMessage?: Message;
unreadCount: number;
}
interface MessageStore {
conversations: Conversation[];
currentConversation: Conversation | null;
messages: Message[];
setConversations: (conversations: Conversation[]) => void;
setCurrentConversation: (conversation: Conversation | null) => void;
addMessage: (message: Message) => void;
markAsRead: (conversationId: string) => void;
sendMessage: (conversationId: string, content: string, type?: 'text' | 'image') => void;
}
export const useMessageStore = create<MessageStore>((set, get) => ({
conversations: [],
currentConversation: null,
messages: [],
setConversations: (conversations) => set({ conversations }),
setCurrentConversation: (conversation) => set({
currentConversation: conversation,
messages: []
}),
addMessage: (message) => {
const { messages, conversations } = get();
set({
messages: [...messages, message],
conversations: conversations.map(c =>
c.id === message.conversationId
? { ...c, lastMessage: message, unreadCount: c.unreadCount + 1 }
: c
)
});
},
markAsRead: (conversationId) => {
set(state => ({
conversations: state.conversations.map(c =>
c.id === conversationId ? { ...c, unreadCount: 0 } : c
)
}));
},
sendMessage: (conversationId, content, type = 'text') => {
socketService.emit('send_message', {
conversationId,
content,
type
});
}
}));
3.3 使用Socket Hook #
typescript
// src/hooks/useSocket.ts
import { useEffect } from 'react';
import { socketService } from '../utils/socket';
import { useAuthStore } from '../store/authStore';
import { useMessageStore } from '../store/messageStore';
import { useNotificationStore } from '../store/notificationStore';
export function useSocket() {
const { token, isAuthenticated } = useAuthStore();
const { addMessage } = useMessageStore();
const { addNotification } = useNotificationStore();
useEffect(() => {
if (!isAuthenticated || !token) return;
// 连接Socket
socketService.connect(token);
// 监听新消息
socketService.on('new_message', (message) => {
addMessage(message);
});
// 监听通知
socketService.on('notification', (notification) => {
addNotification(notification);
});
return () => {
socketService.disconnect();
};
}, [isAuthenticated, token]);
}
四、动态发布 #
4.1 发布组件 #
tsx
// src/components/Post/CreatePost.tsx
import { useState, useRef } from 'react';
import { Camera, CameraResultType } from '@capacitor/camera';
import { Haptics, ImpactStyle } from '@capacitor/haptics';
import { api } from '../../api';
interface CreatePostProps {
onPostCreated: () => void;
}
export default function CreatePost({ onPostCreated }: CreatePostProps) {
const [content, setContent] = useState('');
const [images, setImages] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
async function pickImage() {
try {
await Haptics.impact({ style: ImpactStyle.Light });
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
source: 'Photos'
});
if (photo.webPath) {
setImages([...images, photo.webPath]);
}
} catch (error) {
console.error('Failed to pick image:', error);
}
}
function removeImage(index: number) {
setImages(images.filter((_, i) => i !== index));
}
async function handleSubmit() {
if (!content.trim() && images.length === 0) return;
setLoading(true);
try {
const formData = new FormData();
formData.append('content', content);
for (const image of images) {
const blob = await fetch(image).then(r => r.blob());
formData.append('images', blob);
}
await api.post('/posts', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
setContent('');
setImages([]);
onPostCreated();
await Haptics.notification();
} catch (error) {
console.error('Failed to create post:', error);
} finally {
setLoading(false);
}
}
return (
<div className="create-post">
<textarea
placeholder="分享你的想法..."
value={content}
onChange={(e) => setContent(e.target.value)}
rows={3}
/>
{images.length > 0 && (
<div className="images-preview">
{images.map((image, index) => (
<div key={index} className="image-item">
<img src={image} alt="" />
<button onClick={() => removeImage(index)}>×</button>
</div>
))}
</div>
)}
<div className="actions">
<button onClick={pickImage} className="icon-btn">
📷 图片
</button>
<button
onClick={handleSubmit}
disabled={loading || (!content.trim() && images.length === 0)}
className="submit-btn"
>
{loading ? '发布中...' : '发布'}
</button>
</div>
</div>
);
}
4.2 动态列表 #
tsx
// src/components/Post/PostList.tsx
import { useState, useEffect } from 'react';
import { api } from '../../api';
import { Haptics, ImpactStyle } from '@capacitor/haptics';
import PostItem from './PostItem';
interface Post {
id: string;
content: string;
images: string[];
author: {
id: string;
name: string;
avatar: string;
};
likes: number;
comments: number;
liked: boolean;
createdAt: string;
}
export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
useEffect(() => {
loadPosts();
}, []);
async function loadPosts() {
try {
const response = await api.get<{ posts: Post[]; hasMore: boolean }>(
`/posts?page=${page}`
);
setPosts([...posts, ...response.data.posts]);
setHasMore(response.data.hasMore);
} catch (error) {
console.error('Failed to load posts:', error);
} finally {
setLoading(false);
}
}
async function handleLike(postId: string) {
await Haptics.impact({ style: ImpactStyle.Light });
setPosts(posts.map(post => {
if (post.id === postId) {
return {
...post,
liked: !post.liked,
likes: post.liked ? post.likes - 1 : post.likes + 1
};
}
return post;
}));
try {
await api.post(`/posts/${postId}/like`);
} catch (error) {
// 回滚
setPosts(posts);
}
}
function loadMore() {
if (hasMore && !loading) {
setPage(page + 1);
loadPosts();
}
}
if (loading && posts.length === 0) {
return <div className="loading">加载中...</div>;
}
return (
<div className="post-list">
{posts.map(post => (
<PostItem
key={post.id}
post={post}
onLike={() => handleLike(post.id)}
/>
))}
{hasMore && (
<button onClick={loadMore} className="load-more">
加载更多
</button>
)}
</div>
);
}
五、推送通知 #
5.1 通知服务 #
typescript
// src/services/notification.service.ts
import { PushNotifications } from '@capacitor/push-notifications';
import { LocalNotifications } from '@capacitor/local-notifications';
import { Capacitor } from '@capacitor/core';
import { useNotificationStore } from '../store/notificationStore';
class NotificationService {
async init(): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
// 请求权限
const result = await PushNotifications.requestPermissions();
if (result.receive === 'granted') {
// 注册推送
await PushNotifications.register();
// 监听事件
await PushNotifications.addListener('registration', (token) => {
console.log('Push token:', token.value);
this.sendTokenToServer(token.value);
});
await PushNotifications.addListener('registrationError', (error) => {
console.error('Push registration error:', error);
});
await PushNotifications.addListener(
'pushNotificationReceived',
this.handlePushReceived.bind(this)
);
await PushNotifications.addListener(
'pushNotificationActionPerformed',
this.handlePushAction.bind(this)
);
}
}
private handlePushReceived(notification: any): void {
console.log('Push received:', notification);
// 更新通知计数
useNotificationStore.getState().incrementUnread();
}
private handlePushAction(action: any): void {
console.log('Push action:', action);
const data = action.notification.data;
// 根据通知类型导航
if (data.type === 'message') {
window.location.href = `/messages/${data.conversationId}`;
} else if (data.type === 'like') {
window.location.href = `/post/${data.postId}`;
}
}
private async sendTokenToServer(token: string): Promise<void> {
// 发送token到服务器
}
async showLocal(title: string, body: string, data?: any): Promise<void> {
await LocalNotifications.schedule({
notifications: [{
title,
body,
id: Date.now(),
extra: data
}]
});
}
}
export const notificationService = new NotificationService();
六、总结 #
6.1 核心功能 #
| 功能 | 技术实现 |
|---|---|
| 即时通讯 | Socket.io |
| 动态发布 | Camera插件 |
| 推送通知 | Push Notifications |
| 触觉反馈 | Haptics插件 |
6.2 下一步 #
了解社交应用后,让我们学习 企业应用!
最后更新:2026-03-28