FitKick 电商APP项目总结
FitKick是一个基于React技术栈的电商移动端应用,集成了传统电商功能与现代AI能力,采用了现代化的前端架构和最佳实践。项目特色在于结合了React生态的高效开发与大模型AI能力,打造了一个具有创新交互体验的移动电商平台。我需要对这个项目做一篇总结,对项目的技术栈的巩固,难点亮点分析,更好的掌握一些技术。
一:项目整体架构
做项目之前要先想好自己需要做什么,在这个项目中共有八个页面,主页、商品页、喜欢页面、AI聊天助手、我的页面、搜索页面和商品详情页,还有一个和coze相关的图片生成界面。这些都是页面级别组件,放在pages目录下。 还需要有一些通用组件放在Components目录下。整体架构如下: text
/mock # 服务器模拟
/src
api/ # API请求封装
components/ # 通用组件开发
hooks/ # 自定义hooks
LLM/ # 大模型相关功能
mock/ # 模拟后端数据
pages/ # 页面组件
store/ # Zustand状态管理
unils/ # 工具函数
/vite.config.js # 构建配置
在进行开发之前有一项重要工作就是安装一些依赖,我这个项目依赖有: react-router-dom、 zustand、 axios、 react-vant(UI组件库)、 @react-vant/icons、 lib-flexible(移动端适配)、 mockjs react-markdown、 mitt(事件总线)。 开发期间的依赖 vite-plugin-mock 、jwt 、postcss-pxtorem
路由系统
安装好依赖之后就可以开始路由的搭建,包含一些路由的懒加载,路由守卫,使用react-router-dom来实现,我们在项目的main.jsx中用Router将APP给包着,或者在APP里面直接使用Router在最外层实现路由功能。
import {
Suspense,
lazy
} from 'react'
import {
Routes,
Route,
Navigate
} from 'react-router-dom'
import './App.css'
import Toast from '@/components/Toast'
const Loading = lazy(() => import('@/components/Loading'))
const MainLayout = lazy(() => import('@/components/MainLayout'))
const BlankLayout = lazy(() => import('@/components/BlankLayout'))
const Home = lazy(() => import('@/pages/Home'))
const UserProfile = lazy(() => import('@/pages/UserProfile'))
const ProductList = lazy(() => import('@/pages/ProductList'))
const Like = lazy(() => import('@/pages/Like'))
const Search = lazy(() => import('@/pages/Search'))
const ProductDetail = lazy(() => import('@/pages/ProductDetail'))
const Login = lazy(() => import('@/pages/Login'))
const Chat = lazy(() => import('@/pages/Chat'))
const RequireAuth = lazy(() => import('@/components/RequireAuth'))
const Coze = lazy(() => import('@/pages/Coze'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/home" element={<Home />} />
<Route path="/user" element={<UserProfile />} />
<Route path="/product" element={<ProductList />} />
<Route path="/like" element={<RequireAuth><Like /></RequireAuth>} /> //requireAuth
<Route path="/chat" element={<RequireAuth><Chat /></RequireAuth>} />//requireAuth
</Route>
{/* 空的Layout */}
<Route path="/" element={<BlankLayout />}>
<Route path="/search" element={<Search />} />
<Route path="/detail/:id" element={<RequireAuth><ProductDetail /></RequireAuth>} />//requireAuth
<Route path="/login" element={<Login />} />
<Route path="/coze" element={<Coze />} />
</Route>
</Routes>
<Toast />
</Suspense>
)
}
export default App
可以看到pages目录下对应相应的路由,为什么还会有一个MainLayout和BlankLayout呢,那是因为这些页面级别组件也是不一样的,有些像我的,喜欢,主页都是在底部有Tabbar的页面的,点击下方可以进行切换的,而像search,detail这些页面则应该在应该空白页来显示。
因为如果直接用import引入文件会直接运行,为了性能考虑我们使用lazy实现懒加载,需要加载的时候进行加载就可以实现性能的优化。
基于JWT的权限控制
RequireAuth 是一个 权限验证组件(通常是高阶组件HOC),Chat 是实际要渲染的聊天组件, 这种嵌套表示 Chat 组件需要经过 RequireAuth 的验证才能被访问,将想要保护的组件套在里面实现页面级访问控制。
搭建完了路由就可以正式开始写组件了。
登录界面
因为我没有搞后端,数据也都是用mock.js获取一些随机数据,所以简单做了一下登录界面,主要就是涉及到jwt鉴权。将状态放在文件useLoginStore.js中。
login.js
import jwt from "jsonwebtoken";
// 安全性 编码的时候用于加密
// 解码的时候用于解密
// 加盐
const secret = '!&05152054.'
// login 模块 mock
export default [
{
url: '/api/login',
method: 'post',
timeout: 2000,//手动指定请求耗时
response: (req, res) => {
// req,username,password
const { username, password } = req.body
if (username !== 'admin' || password !== '123456') {
return {
code: 1,
message: '用户名或密码错误',
}
}
// JSON 用户数据对象
const token = jwt.sign({
user: {
id: "001",
username: "admin",
nickname: "海绵宝宝" // 添加nickname字段
}
}, secret, {
expiresIn: '24h',
})
// console.log(token, '///')
// 生成token 颁发令牌
return {
code: 0,
token,
data: {
id: "001",
username: "admin",
nickname: "海绵宝宝" // 添加nickname字段
}
}
},
},
{
url: '/api/user',
method: 'get',
response: (req, res) => {
// 用户端 token headers
const token = req.headers["authorization"].split(' ')[1]
// console.log(token)
try {
const decode = jwt.decode(token, secret)
// console.log(decode)
return {
code: 0,
data: decode.user
}
} catch (err) {
return {
code: 1,
message: 'Invalid token'
}
}
}
}
]
- 用户在登录页面输入用户名密码并提交
- useLoginStore 中的 login 方法调用 doLogin API
- 后端验证成功后生成 token 并返回
- 前端将 token 存储到 localStorage ,并更新登录状态
import {
create
} from 'zustand'
import {
doLogin,
getUser
} from '@/api/user'
export const useLoginStore = create((set) => ({
user: null, // 用户信息
isLogin: localStorage.getItem('token') ? true : false, // 是否登录
login: async ({ username = "", password = "" }) => {
try {
const data = await doLogin({ username, password })
// 检查响应是否成功
if (data.code !== 0) {
throw new Error(data.message || '登录失败')
}
const { token, data: user } = data
localStorage.setItem('token', token)
// 登录成功后获取用户详细信息
const userData = await getUser()
if (userData.code === 0) {
set({
isLogin: true,
user: userData.data,
})
} else {
throw new Error('获取用户信息失败')
}
} catch (err) {
console.error('登录失败:', err)
throw err
}
},
logout: () => {
localStorage.removeItem('token')
set({
isLogin: false,
user: null,
})
},
setUser: (user) => set({ user }),
}))
界面也很简单:
index.js
import { useRef } from 'react';
import { useLoginStore } from '@/store/useLoginStore';
import { useNavigate } from 'react-router-dom';
import styles from './login.module.css';
import { useToastStore } from '@/store/useToastStore'
import { ArrowLeft } from '@react-vant/icons';
const Login = () => {
const usernameRef = useRef(null);
const passwordRef = useRef(null);
const { login, user } = useLoginStore();
const navigate = useNavigate();
const { showToast } = useToastStore();
const handleSubmit = (e) => {
e.preventDefault();
const username = usernameRef.current.value;
const password = passwordRef.current.value;
if (!username || !password) {
showToast({
message: '请输入用户名和密码',
type: 'warning'
});
return;
}
login({ username, password })
.then(() => {
const nickname = user?.nickname || '海绵宝宝';
showToast({
message: `欢迎你, ${nickname}`,
type: 'success'
});
setTimeout(() => {
navigate(-1);
}, 1000);
})
.catch(err => {
showToast({
message: err.message || '登录失败',
type: 'error'
});
});
};
return (
<div className={styles.container}>
<div className={styles.back}>
<ArrowLeft onClick={() => navigate('/user')} />
</div>
<h2 className={styles.title}>FITKICK</h2>
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="username">用户名</label>
<input
id="username"
ref={usernameRef}
type="text"
required
placeholder="请输入用户名"
className={styles.input}
defaultValue="admin"
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="password">密码</label>
<input
id="password"
ref={passwordRef}
type="password"
required
placeholder="请输入密码"
className={styles.input}
defaultValue="123456"
/>
</div>
<div>
<button type="submit" className={styles.button}>
登录
</button>
</div>
</form>
</div>
);
};
export default Login;
login.module.css
.container {
max-width: 600px;
height: 500px;
padding: 20px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
/* 水平居中 */
margin: 0 auto;
/* 垂直居中 - 计算上方margin */
margin-top: calc(50vh - 50% - 20px);
}
.title {
text-align: center;
margin-bottom: 20px;
/* 调整字体和大小 */
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-weight: 700;
color: #1eaaaf;
/* 添加字间距增强可读性 */
letter-spacing: 0.5px;
/* 轻微阴影增强立体感 */
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.formGroup {
margin-bottom: 15px;
width: 100%;
height: 100px;
}
.input {
width: 100%;
box-sizing: border-box;
max-width: 100%;
padding: 8px;
margin-top: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
.button {
width: 100%;
padding: 10px;
background-color: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.button:hover {
background-color: #1d4ed8;
/* 稍深的蓝色 */
}
useLoginStore.js
import {
create
} from 'zustand'
import {
doLogin,
getUser
} from '@/api/user'
export const useLoginStore = create((set) => ({
user: null, // 用户信息
isLogin: localStorage.getItem('token') ? true : false, // 是否登录
login: async ({ username = "", password = "" }) => {
try {
const data = await doLogin({ username, password })
// 检查响应是否成功
if (data.code !== 0) {
throw new Error(data.message || '登录失败')
}
const { token, data: user } = data
localStorage.setItem('token', token)
// 登录成功后获取用户详细信息
const userData = await getUser()
if (userData.code === 0) {
set({
isLogin: true,
user: userData.data,
})
} else {
throw new Error('获取用户信息失败')
}
} catch (err) {
console.error('登录失败:', err)
throw err
}
},
logout: () => {
localStorage.removeItem('token')
set({
isLogin: false,
user: null,
})
},
setUser: (user) => set({ user }),
}))
这个实现涵盖了 JWT 鉴权的核心环节,包括 token 的生成、存储、携带和验证,确保了用户身份的安全认证。