社交应用实战 #

一、项目概述 #

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