代码仓库
创建项目
首先保证安装了node, 然后使用vite创建项目
shell
npm create vite react-learn
cd react-learn
npm i
目录结构
一个完整的前端项目需要:
- 状态管理
在全局维护共有的状态(数据), 让页面组件之间共享数据, 我们使用pinia - 路由
路由让页面之间可以进行跳转, 我们使用vue-router - 样式
样式让页面更美观, 我们使用tailwindcss - 网络请求
前端需要通过网络请求的方式和后端进行数据交互, 从而实现功能, 我们使用axios
text
├── dev-dist/ # 开发环境构建输出目录
├── node_modules/ # Node.js依赖包目录
├── public/ # 静态资源目录,不会被构建工具处理
├── src/
├── admin/ # 存放后台管理页面
├── api/ # 接口请求逻辑
├── assets/ # 静态资源(图片、字体等)
├── components/ # 公共组件
├── includes/ # 包含文件,存放外部库
├── lib/ # 存放项目内的一些公共资源
├── locales/ # 国际化语言包
├── mocks/ # 模拟数据
├── pages/ # 存放普通的页面组件
├── router/ # 路由配置
├── store/ # 状态管理(Pinia)
├── styles/ # 全局样式或CSS文件
├── utils/ # 工具函数
├── App.jsx # 根组件
├── main.js # 项目入口文件
├── middleware.js # 中间件逻辑(如路由守卫)
└── settings.js # 项目设置或配置文件
├── .gitignore # Git忽略文件配置
├── index.html # 项目入口HTML文件
├── package.json # 项目配置及依赖声明
├── postcss.config.js # PostCSS配置文件
├── README.md # 项目说明文档
├── tailwind.config.js # Tailwind CSS配置文件
├── vite.config.js # Vite构建工具配置文件
配置
路径别名
配置一下路径别名, 用@/表示src/目录
shell
# node的类型声明, 防止使用node的依赖后报错
pnpm i @types/node --save-dev
js
import {defineConfig} from 'vite'
import {join} from 'path';
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
// 路径别名
resolve: {
alias: {
'@':
join(__dirname, 'src'),
}
}
})
PWA配置
shell
# v1
pnpm i vite-plugin-pwa --save-dev
js
import {defineConfig} from 'vite'
import {join} from 'path';
import react from '@vitejs/plugin-react'
import {VitePWA} from "vite-plugin-pwa";
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
devOptions: {
// 生成清单文件
enabled: true
},
manifest: {
name: "vue-quick-start",
theme_color: '#ff5e3a',
icons: [
{
src: 'assets/logo.png',
size: '192x192',
type: 'image/png'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,jpg}']
}
})
],
resolve: {
alias: {
'@':
join(__dirname, 'src'),
}
}
})
项目的设置
js
// settings.js
export default {
routeMode: 'history', // 路由模式
BaseURL: 'http://localhost:4000', // 后端请求地址
timeout: 5000, // 请求超时时间
}
tailwind
使用tailwind3(如果你要用4的也可以, 不过两个安装有点区别)
shell
pnpm install -D tailwindcss@3 postcss autoprefixer
pnpm dlx tailwindcss@3 init -p
tailwind.config.js
js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
styles/base.css
css
@tailwind base;
@layer base {
h1 {
@apply text-2xl;
}
h2 {
@apply text-xl;
}
h3 {
@apply text-lg;
}
h4 {
@apply text-base;
}
h5 {
@apply text-sm;
}
h6 {
@apply text-xs;
}
}
@tailwind components;
@tailwind utilities;
body {
@apply h-full w-full p-0 m-0;
}
/* 覆盖默认的最大宽度限制 */
@media (min-width: 1536px) {
.container {
max-width: 100%;
}
}
/* 页面主内容 */
.container {
width: 100%;
height: 100%;
background-color: #f5f7fb;
padding: 20px;
.container-wrapper {
width: 100%;
height: 100%;
padding: 15px 30px 0;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
}
/* 设置打印控件样式 */
.plugin-download {
width: 500px !important;
a:hover {
text-decoration: underline;
}
}
记得在src/main.js引入
js
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import App from './App.jsx'
import '@/styles/base.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App/>
</StrictMode>,
)
路由 router
使用react-router-dom进行路由跳转, 并且我们会实现文件路由, 自动扫描目录下的page.jsx文件, 然后注册为路由,
有两个页面目录, 一个是admin, 一个是pages, admin目录是后台管理页面, pages目录是普通的页面组件
shell
# v7
pnpm i react-router-dom@7
文件路由
原生方案
文件路由就是根据目录结构, 自动扫描并注册路由, 不需要我们一个一个手动声明注册
实现的关键是
js
import.meta.glob("xxx")
这是由vite提供的方法, 可以扫描获取文件, webpack也有类似的方法
js
require.context()
我们使用的是vite, 使用import.meta.glob就行, 现在来实现文件扫描功能
插件方案
使用vite-plugin-pages, 也可以实现文件路由注册
shell
npm install -D vite-plugin-pages
npm install react-router react-router-dom
vite.config
js
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import {VitePWA} from "vite-plugin-pwa";
import {join} from 'path';
import pages from 'vite-plugin-pages'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
///
/*pages({
// 自定义配置
// dir: 'src/pages', // 路由组件目录
extensions: ['jsx', 'tsx'], // 支持文件后缀
// exclude: ['components'], // 排除组件目录
base: process.env.VITE_APP_BASE_URL
}),*/
pages({
// 注册多个目录, 有不同的路由前缀
dirs: [
// basic
{dir: 'src/pages', baseRoute: ''},
// with custom file pattern
{dir: 'src/admin/', baseRoute: 'admin'},
],
}),
],
resolve: {
alias: {
'@':
join(__dirname, 'src'),
}
},
})
src/components/router-guard.jsx
js
import {matchPath, Route, Routes, useLocation, useNavigate, useRoutes} from "react-router-dom";
import {Suspense, useEffect} from "react";
import routes from "~react-pages";
import Login from "../pages/login.jsx";
import NotFount from "../pages/not-fount.jsx";
const isAuthenticated = () => {
return localStorage.getItem("token") !== null; // 示例逻辑
};
// 全局路由组件
function RouterGuard() {
const navigate = useNavigate();
const location = useLocation();
// 添加 requiresAuth 属性
const toNeedAuth = (r) => {
const route = {...r, requiresAuth: true};
if (route.children) {
route.children = route.children.map(c => toNeedAuth(c));
}
return route;
}
const authRoutes = routes.map(toNeedAuth)
useEffect(() => {
/*
// 无法匹配嵌套的路由
const currentRoute = authRoutes.find(route =>
matchPath({path: route.path, end: true}, location.pathname)
);
console.log(currentRoute)
*/
console.log(location.pathname)
// 全局导航守卫逻辑
/*
if (currentRoute?.requiresAuth && !isAuthenticated()) {
navigate("/login", {replace: true});
}
*/
// 后续添加更多不需要身份验证的页面
if (location.pathname !== '/login' && !isAuthenticated()) {
navigate("/login", {replace: true});
}
}, [location, navigate]); // 监听路由变化
return (
<Suspense fallback={<p>Loading...</p>}>
{useRoutes(
[
...authRoutes,
{
path: 'login',
element: <Login/>
},
{
path: "*",
element: <NotFount/>
}
]
)}
</Suspense>
);
}
export default RouterGuard;
app.jsx
js
import RouterGuard from "./components/router-guard.jsx";
function App() {
return (
<RouterGuard/>
)
}
export default App
main.jsx
js
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import App from './App.jsx'
import '@/styles/base.css'
import {BrowserRouter} from "react-router-dom";
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</StrictMode>,
)
状态管理 store
之前学习react的时候会发现原生的组件传值会遇到很麻烦的情况, 比如兄弟/跨级传值, 而我们可以使用全局状态管理的方案了解决
这里我们使用zustand
shell
pnpm i zustand
src/store/count/index.js
js
import {create} from 'zustand'
export const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({count: state.count + 1})),
reset: () => set({count: 0}),
}))
测试 /pages/index.jsx
js
import React from 'react';
import {useCountStore} from "../store/count/index.js";
const Index = () => {
const {count, increment, reset} = useCountStore()
localStorage.setItem("token", "123")
return (
<div className={`flex items-center flex-col`}>
<div className={``}>
{count}
</div>
<div className={`flex gap-x-4`}>
<button
className={`bg-blue-400 py-1 px-2 rounded-lg`}
onClick={() => increment()}>+1
</button>
<button
className={`bg-blue-400 py-1 px-2 rounded-lg`}
onClick={() => reset()}>reset
</button>
</div>
</div>
);
};
export default Index;
网络请求 api
使用axios进行网络请求, 一般情况下, 前后端是分人分组开发的, 前端请求接口地址是后端提供的
如果你此时后端还没有接口, 就需要自己用假数据或者用json-server来模拟测试
shell
# v1
pnpm i axios
封装
js
// request.js
import axios from 'axios'
import settings from "../settings.js";
const request = axios.create({
baseURL: settings.BaseURL, // 设置基础URL
timeout: settings.timeout, // 请求超时时间
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么,例如添加 token
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
// 对响应数据做处理
return response.data
},
(error) => {
// 对响应错误做处理
return Promise.reject(error)
}
)
export default request
测试
js
/*
* @Author Malred · Wang
* @Date 2025-06-23 16:42:29
* @Description
* @Path src/pages/login.jsx
*/
import React, {useState} from 'react';
import auth from '@/api/auth/index.js'
import {useNavigate} from "react-router-dom";
const Login = () => {
const navigate = useNavigate();
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
return (
<div className={`flex items-center flex-col`}>
<h1>login</h1>
<form
onSubmit={async (e) => {
e.preventDefault(); // 阻止默认提交行为
const res = await auth.login({username, password})
localStorage.setItem('token', res.token)
navigate('/')
}}
className={`flex flex-col gap-y-2`}
>
账号
<input className={`px-2 py-1 border rounded-md`} type="text" value={username}
onChange={(e) => setUsername(e.target.value)}/>
密码
<input className={`px-2 py-1 border rounded-md`} type="password" value={password}
onChange={(e) => setPassword(e.target.value)}/>
<button
type={"submit"}
className={`text-white bg-blue-400 py-1 px-2 rounded-lg`}
>
登录
</button>
</form>
</div>
);
};
export default Login;
后端服务可以参考我之前的后端web单体教学代码模板:
rust-web-starter
go-web-starter
下一步
- json-server模拟接口
- 代码生成器, 批量生成重复页面和代码
社群
你可以在这些平台联系我:
- bili: 刚子哥forever
- 企鹅群: 940263820
- gitee: gitee
- 博客: malcode-site
- 邮箱: malguy2022@163.com
- 知乎: 乐妙善哉居士
- csdn: 飞鸟malred