React Social Media App 动态页面部分

143 阅读8分钟

搭建动态页面

初始化

安装依赖 npm i axios

创建 axios.js

// axios.js
import axios from "axios";

export const makeRequest = axios.create({
    baseURL: 'http://localhost:8800/api/',
    withCredentials: true
})

auth

注册

  • 创建 state 保存所有输入数据

    • 创建 input 的事件处理函数,更新 state
    // register.jsx
    export default function Register() {
        const [inputs, setInputs] = useState({
            username: "",
            email: "",
            password: "",
            name: ""
        })
    
        function handleChange(e) {
            setInputs(prev => ({...prev,[e.target.name]: e.target.value}));
        }
    
        return (
     //...............
          <input type="text" placeholder='Username' name='username' onChange={handleChange} />
          <input type="email" placeholder='Email' name='email' onChange={handleChange} />
          <input type="password" placeholder="Password" name='password' onChange={handleChange} />
          <input type="text" placeholder='Name' name='name' onChange={handleChange} />
    //....................
        )
    }
    
  • 提交输入处理逻辑

    • handleClick 逻辑,调用注册接口,提交数据
    • 创建 err state,保存错误信息
    • 如果调用接口失败,则以错误信息更新 err
    const [err,setErr] = useState(null);
    
    async function handleClick(e) {
            e.preventDefault();
            try {
                await axios.post('http://localhost:8800/api/auth/register', inputs);
            } catch(err) {
                setErr(err.response.data);
            }
     }
    
    return (
            <div className="right">
                <h1>Register</h1>
                <form>
                    {err && err}
                    <button onClick={handleClick}>Register</button>
                </form>
            </div>
        )
    

登录

之前静态页面中,将 login 的逻辑交给了 authContextauthContext 只是简单地设置了一个本地数据

  • 完善 authContextlogin 函数
const login = async (inputs) => {
        const res = await axios.post("http://localhost:8800/api/auth/login", inputs, {
            withCredentials: true
        });

        setCurrentUser(res.data);
  }
  • 整理 login.jsx
    • inputs state err stateregister.jsx 那样设置 state 和事件处理函数
const [inputs, setInputs] = useState({
        username: "",
        password: ""
    });
    const [err, setErr] = useState(null);

    function handleChange(e) {
        setInputs(prev => ({...prev,[e.target.name]: e.target.value}));
    }

    async function handleLogin(e) {
        e.preventDefault();
        try {
            await login(inputs);
        } catch (error) {
            setErr(error.response.data);
        }
    }

// ............................................
<div className="right">
    <h1>Login</h1>
    <form>
        <input type="text" placeholder='Username' name='username' onChange={handleChange} />
        <input type="password" placeholder="Password" name='password' onChange={handleChange} />
        {err && err}
        <button onClick={handleLogin}>Login</button>
    </form>
</div>

posts

获取

  • 开始使用 react-query ,安装最新版 npm i @tanstack/react-query
    • 初始化内容就按照文档的来,引入,创建变量,包裹所有标签
**import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query';

const queryClient = new QueryClient()**

const Layout = () => {
    return (
      **<QueryClientProvider client={queryClient}>**
        <div className={`theme-${darkMode ? 'dark' : 'light'}`}>
          <NavBar />
          <div style={{ display: 'flex' }}>
            <LeftBar />
            <div style={{ flex: 6 }}>
              <Outlet />
            </div>
            <RightBar />
          </div>
        </div>
      **</QueryClientProvider>**
    )
  }
  • 请求接口

渲染列表的数据改为 data

import {useQuery} from '@tanstack/react-query';
import {makeRequest} from 'axios.js';

export default function Posts() {
    **const {isLoading, error, data} = useQuery(["posts"], () => {
        return makeRequest('/posts').then((res) => {
            return res.data;
        })
    })**

    return (
        <div className="posts">
            {**data**.map(post => (
                <Post post={post} key={post.id}/>
            ))}
        </div>
    )
}

发帖

  • desc state

  • file state

  • 提交事件处理函数

    • 使用 react-querymutation 函数,目的是在 post 成功后刷新一次页面
    • 上传文件逻辑,判断是否有上传文件,有则调用 upload()
  • 代码

    **const [file,setFile] = useState(null);
    const [desc,setDesc] = useState('');
    
    const queryClient = useQueryClient();
    
    const mutation = useMutation((newPost) => {
        return makeRequest.post('/posts', newPost);
    }, {
        onSuccess: () => {
            queryClient.invalidateQueries(["posts"]);
        }
    })
    
    async function upload() {
            try {
                const formData = new FormData();
                formData.append('file', file);
                const res = await makeRequest.post('/upload', formData);
                return res.data;
            } catch (error) {
                console.log(error);
            }
    }
    
    async function handleClick(e) {
            e.preventDefault();
            let imgUrl = '';
            if(file) imgUrl = await upload()
            mutation.mutate({desc, img: imgUrl});
    }**
    //..............
    return (
    <div className="share">
        <input type="text" placeholder={`What's on your mind ${currentUser.name}?`}
    		 **onChange={(e) => setDesc(e.target.value)}** />
        </div>
        <hr />
        <div className="left">
            <input type="file" id="file" style={{display:"none"}} 
    				**onChange={e => setFile(e.target.files[0])}** />
        </div>
    		<div className="right">
    	      <button onClick={handleClick}>Share</button>
        </div>
    </div>
        );
    
  • 更改之前静态页面的代码,让贴子动态显示发布时间

// post/post.js
<div className="details">
    <Link to={`/profile/${post.useId}`}>
        <span className='name'>{post.name}</span>
    </Link>
    **<span className='date'>{moment(post.createdAt).fromNow()}</span>**
</div>

点赞

  • 获取该帖子的所有点赞用户ID

    • 可以确定点赞数
    • 可以判断当前用户是否点赞
  • 判断是点赞还是取消点赞,然后发送请求

删帖

  • state openMenu
    • 不仅以 openMenu 为依据,还要判断该 post 是属于当前用户,如果不是则不打开
  • 发送请求
    • 使用突变

comments

获取

  • comment 组件接收 prop → postId
  • 像 posts 组件那样创建 query comments
  • 列表渲染 data
  • 动态展示创建时间
export default function Comments({postId}) {
    **const {isLoading, error, data} = useQuery(["comments"], () => {
        return makeRequest.get("/comments?postId=" + postId).then((res) => {
            return res.data;
        })
    })**

    return (
        <div className="comments">
            <div className="write">
                <img src={currentUser.profilePic} alt="" />
                <input type="text" placeholder='write a comment' />
                <button>Send</button>
            </div>
            **{isLoading ? "isLoading"
            :data.map(comment => (
                <div className="comment">
                    <img src={comment.profilePic} alt="" />
                    <div className="comment-info">
                        <span>{comment.name}</span>
                        <p>{comment.desc}</p>
                    </div>
                    <span className='date'>{moment(comment.createdAt).fromNow()}</span>
                </div>
            ))}**
        </div>
    )
}

添加评论

  • desc state
  • 设置突变
  • 点击事件处理函数
    • 调用接口完后更新 desc 为空
const [desc, setDesc] = useState("");

const queryClient = useQueryClient();

    const mutation = useMutation((newComent) => {
        return makeRequest.post('/comments', newComent);
    }, {
        onSuccess: () => {
            queryClient.invalidateQueries(["comments"]);
        }
    })

    async function handleClick(e) {
        e.preventDefault();
        mutation.mutate({desc, postId});
        setDesc('');
    }

return (
        <div className="comments">
            <div className="write">
                <img src={currentUser.profilePic} alt="" />
                <input type="text" placeholder='write a comment'
								 **onChange={e => setDesc(e.target.value)}/>**
                **<button onClick={handleClick}>Send</button>**
            </div>
        </div>
    )

profile page

  • 获取用户信息
    • 通过 react-router 的方法获取路由上的 userId
  • 更新 jsx
    • 将之前静态展示的信息改为动态
    • 判断个人页面展示的是否为当前用户信息,是则改 follow 为 update

关注

  • 一加载 profile page 就获取 profile host 的跟随者有哪些

    • 如果 currentUser 是 profile host 的跟随者,应该将中间 button 内容改为 following
  • 关注或者取关

    • 判断应该是关注还是取关操作
    • 使用突变,在操作后,再次请求跟随者列表以刷新关注状态
  • 追加,profile page 的 posts 应显示自己的 post

    • 给 api/posts 接口增加 query,即 userId

更新

  • 接收 setOpenUpdate,用于关闭 model
  • state texts cover profile
    • 图片与文本信息分开
    • 图片是文件类型,用 upload 方法
  • 调用 api
    • 使用突变,在更新后再次请求 user 信息
    • 提交,如果有打算上传图片,先请求 upload,再请求更新

后端部分

数据库设计

users


Untitled 14.png

posts


Untitled 15.png

建立外键

Untitled 16.png

comments


Untitled 17.png

建立外键

Untitled 18.png

Untitled 19.png

stories


Untitled 20.png

Untitled 21.png

relationships


保存关注记录的表

Untitled 22.png

Untitled 23.png

Untitled 24.png

likes


记录用户点赞帖子记录的表

Untitled 25.png

Untitled 26.png

Untitled 27.png

api

初始化

进入 api 文件夹

执行命令

npm init -y

npm i express mysql nodemon

创建文件 index.js

package.json 文件中增加命令 "start": "nodemon index.js” ,增加属性 “type”: “module”

// index.js
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';

const app = express();

app.use(cors({
    origin: "http://localhost:3000"
}));
app.use(cookieParser());

app.listen(8800, () => {
    console.log('app is listening');
})

路由


创建 api/routes 文件夹

创建文件 users.js posts.js comments.js likes.js auth.js

import express from 'express';

const router = express.Router();

router.get('/test', (req, res) => {
    res.send('it works');
})

export default router;

使用路由

// index.js
import express from 'express';
import usersRouter from './routes/users.js';
import postsRouter from './routes/posts.js';
import commentsRouter from './routes/comments.js';
import likesRouter from './routes/likes.js';
import authRouter from './routes/auth.js';

const app = express();

app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
app.use('/api/comments', commentsRouter);
app.use('/api/likes', likesRouter);
app.use('/api/auth', authRouter);

app.listen(8800, () => {
    console.log('app is listening');
})

controllers


创建文件夹 api/controllers

创建文件 user.js post.js comment.js like.js auth.js

将对应路由所需要的请求处理逻辑都写在 controller 文件中,导出,路由文件导入使用

数据库


创建文件 api/connect.js

// connect.js
import mysql from 'mysql';

export const db = mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'onlylove245',
    database: 'social'
})

为使服务器能接收JSON数据

// index.js
app.use(express.json());

设置响应头

// index.js
app.use((req, res, next) => {
    res.header("Access-Control-Allow-Credentials", true);
    next();
})

auth路由处理请求逻辑

注册


  • 首先检测是否已经存在该用户
    • 如果已存在则响应已存在的错误信息
    • 如果未存在则继续
  • 接着加密密码
    • 安装依赖 npm i bcryptjs
    • 生成加密密码
  • 插入数据
export const register = (req, res) => {
    // 首先检查用户是否已经存在
    const query = 'select * from users where username = ?';
    db.query(query, [req.body.username], (err, data) => {
        if (err) return res.status(500).json(err);
        if (data.length) return res.status(409).json('User already exists!');

        // 创建新用户对象
        // 加密密码
        const salt = bcrypt.genSaltSync(10);
        const hashedPassword = bcrypt.hashSync(req.body.password, salt);

        const query = 'insert into users (`username`, `email`, `password`, `name`) value (?)';
        const values = [req.body.username, req.body.email, hashedPassword, req.body.name];

        db.query(query, [values], (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json('User has been created');
        })
    })
}

登录


  • 检查用户是否存在

  • 验证密码

  • 设置cookie

    • 安装依赖 npm i jsonwebtoken cookie-parser cors
    // index.js
    app.use(cors());
    app.use(cookieParser());
    
    • 使用 jsonwebtoken
export const login = (req, res) => {
    const query = 'select * from users where username = ?';

    db.query(query, [req.body.username], (err, data) => {
        if (err) return res.status(500).json(err);
        if (data.length === 0) return res.status(404).json('User not exists!');

        // 验证密码
        const checkPassword = bcrypt.compareSync(req.body.password, data[0].password);

        if (!checkPassword) return res.status(400).json('Wrong password or username');

        const token = jwt.sign({ id: data[0].id }, 'secretkey');

        const { password, ...others } = data[0];

        res.cookie('accessToken', token, {
            httpOnly: true
        }).status(200).json(others);
    })
}

登出


  • 清除cookie
export const logout = (req, res) => {
    res.clearCookie('accessToken', {
        secure: true,
        sameSite: 'none'
    }).status(200).json('User has been logged out.');
}

posts路由逻辑

路由路径


// routes/posts.js

router.get('/', getPosts);
router.post('/', addPost);

获取用户相关的帖子信息


  • 验证 token
  • 查询数据库
    • 连接了三个表 users posts relationships,目的是找出自己发的贴子和关注用户发的贴子
  • 追加,如果有 query userId,说明只要该用户的 posts
export const getPosts = (req, res) => {
    const userId = req.query.userId;

    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = userId !== 'undefined' ? `select p.*, u.id as userId, name, profilePic from posts as p join users as u on (u.id = p.userId)
            where p.userId = ?`
            : `select p.*, u.id as userId, name, profilePic from posts as p join users as u on (u.id = p.userId)
            left join relationships as r on (p.userId = r.followedUserId) where r.followerUserId = ? or p.userId = ?
            order by p.createdAt desc`;

        const values = userId !== 'undefined' ? [userId] : [userInfo.id, userInfo.id];

        db.query(query, values, (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json(data);
        })
    })
}

发帖


  • 验证 token

  • 上传文件

    • 安装 multer 依赖 npm i multer
    • 默认将上传的文件放入 client/public/upload 文件夹中
    • api 返回结果为图片在本地的相对路径
    // index.js
    const storage = multer.diskStorage({
        destination: (req, file, cb) => {
            cb(null, '../client/public/upload')
        },
        filename: (req, file, cb) => {
            cb(null, Date.now + file.originalname);
        }
    })
    
    const upload = multer({ storage: storage });
    
    app.use('/api/upload', upload.single('file'), (req, res) => {
        const file = req.file;
        res.status(200).json('./upload/' + file.filename);
    })
    
  • 插入数据库

    • 安装 moment 依赖 npm i moment
// controlles/post.js
export const addPost = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = `insert into posts ("desc","img","createdAt","userId") values ?`;

        const values = [
            req.body.desc,
            req.body.img,
            moment(Date.now()).format("YYYY-MM-DD HH:mm:ss"),
            userInfo.id
        ]

        db.query(query, [values], (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json('post has been created');
        })
    })
}

删除


export const deletePost = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = 'delete from posts where `id` = ? and `userId` = ?';

        db.query(query, [req.params.id, userInfo.id], (err, data) => {
            if (err) return res.status(500).json(err);
            if(data.affectedRows > 0) return res.status(200).json('Post has been deleted.');
            return res.status(403).json('You can delete only your post');
        })
    })
}

comments 路由逻辑

路由路径


// routes/comments.js 
router.get('/', getComments);
router.post('/', addComment);

获取该帖子的评论


// controllers/comment.js
export const getComments = (req, res) => {
    const query = `SELECT c.*, u.id AS userId, name, profilePic FROM comments AS c JOIN users AS u ON (u.id = c.userId)
    WHERE c.postId = ? ORDER BY c.createdAt DESC
    `;

    db.query(query, [req.query.postId], (err, data) => {
        if (err) return res.status(500).json(err);
        return res.status(200).json(data);
    })
}

添加评论


  • 验证身份
  • 插入
export const addComment = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = "INSERT INTO comments(`desc`, `createdAt`, `userId`, `postId`) VALUES (?)";

        const values = [
            req.body.desc,
            moment(Date.now()).format("YYYY-MM-DD HH:mm:ss"),
            userInfo.id,
            req.body.postId
        ]

        db.query(query, [values], (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json('Comment has been created');
        })
    })
}

like路由逻辑

获取该帖子的所有点赞用户ID


export const getLikes = (req, res) => {
    const query = 'select userId from likes where postId = ?';

    db.query(query, [req.query.postId], (err, data) => {
        if (err) return res.status(500).json(err);
        return res.status(200).json(data.map(like => like.userId));
    })
}

点赞


export const addLike = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = 'insert into likes (`userId`,`postId`) values (?)';
        const values = [
            userInfo.id,
            req.body.postId
        ]

        db.query(query, [values], (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json('Post has been liked');
        })
    })
}

取消点赞


export const cancelLike = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = 'delete from likes where `userId` = ? and `postId` = ?';
        const values = [
            userInfo.id,
            req.body.postId
        ]

        db.query(query, [values], (err, data) => {
            if (err) return res.status(500).json(err);
            return res.status(200).json('Like has been canceled');
        })
    })
}

relationships路由逻辑

获取


获取当前用户的跟随者ID

export const getRelationships = (req, res) => {
    const query = 'select followerUserId from relationships where followedUserId = ?';

    db.query(query, [req.query.followedUserId], (err, data) => {
        if (err) return res.status(500).json(err);
        return res.status(200).json(data.map(relationship => relationship.followerUserId));
    })
}

user路由逻辑

路由路径


router.get('/find/:userId', getUser);
router.put('/', updateUser);

获取用户信息


export const getUser = (req, res) => {
    const userId = req.params.userId;
    const query = 'select * from users where id = ?';

    db.query(query, [userId], (err, data) => {
        if (err) return res.status(500).json(err);
        const { password, ...info } = data[0];
        return res.json(info);
    })
}

更新用户信息


export const updateUser = (req, res) => {
    const token = req.cookies.accessToken;
    if (!token) return res.status(401).json('Not logged in');

    jwt.verify(token, 'secretKey', (err, userInfo) => {
        if (err) return res.status(403).json('Token is not valid!');

        const query = 'update users set `name`=?,`city`=?,`website`=?,`profilePic`=?,`coverPic`=? where id=?';

        db.query(query, [
            req.body.name,
            req.body.city,
            req.body.website,
            req.body.profilePic,
            req.body.coverPic,
            userInfo.id
        ], (err, data) => {
            if (err) return res.status(500).json(err);
            if (data.affectedRows > 0) return res.status(200).json('Updated!');
            return res.status(403).json('You can only update your profile');
        })
    })
}