使用React+Vite构建企业级项目模板
-
- **一、项目结构升级**
- **二、核心步骤与代码**
-
- [**1. 初始化与安装依赖**](#1. 初始化与安装依赖)
- [**2. 配置 Vite (`vite.config.js`)**](#2. 配置 Vite (
vite.config.js)) - [**3. 国际化配置 (`src/i18n.js`)**](#3. 国际化配置 (
src/i18n.js)) - [**4. 创建翻译文件**](#4. 创建翻译文件)
- [**5. 认证状态管理 (`src/store/authStore.js`)**](#5. 认证状态管理 (
src/store/authStore.js)) - [**6. 路由守卫 (`src/components/AuthGuard.jsx`)**](#6. 路由守卫 (
src/components/AuthGuard.jsx)) - [**7. 路由配置 (`src/router/index.jsx`)**](#7. 路由配置 (
src/router/index.jsx)) - [**8. 主入口与布局 (`src/main.jsx`, `src/App.jsx`)**](#8. 主入口与布局 (
src/main.jsx,src/App.jsx)) - [**9. 关键页面与组件**](#9. 关键页面与组件)
- [**10. 全局样式 (`src/styles/global.css`)**](#10. 全局样式 (
src/styles/global.css)) - [**11. 更新 `index.html` 和 `package.json`**](#11. 更新
index.html和package.json)
- **三、运行与测试**
- **四、总结**
一、项目结构升级
react-enterprise-template/
├─ public/
├─ src/
│ ├─ api/ # API 请求
│ ├─ assets/ # 静态资源(图片、字体)
│ ├─ components/ # 公共组件
│ │ ├─ AuthGuard.jsx # 路由守卫组件
│ │ ├─ LanguageSwitcher.jsx # 语言切换器
│ │ └─ Sidebar.jsx # 侧边栏
│ ├─ hooks/ # 自定义 Hooks
│ │ └─ useAuth.js # 认证相关逻辑
│ ├─ locales/ # 国际化翻译文件
│ │ ├─ en/ # 英文
│ │ │ └─ translation.json
│ │ └─ zh/ # 中文
│ │ └─ translation.json
│ ├─ pages/ # 页面
│ │ ├─ Login.jsx # 登录页
│ │ ├─ Dashboard.jsx # 仪表盘(需登录)
│ │ ├─ Admin.jsx # 管理页(需 admin 角色)
│ │ └─ Unauthorized.jsx # 无权限页
│ ├─ router/ # 路由配置
│ │ └─ index.jsx
│ ├─ store/ # Zustand 状态
│ │ ├─ authStore.js # 认证状态
│ │ └─ counterStore.js
│ ├─ styles/ # 全局样式
│ ├─ utils/ # 工具函数
│ ├─ App.jsx
│ ├─ main.jsx
│ └─ i18n.js # i18n 配置
├─ index.html
├─ package.json
└─ vite.config.js
二、核心步骤与代码
1. 初始化与安装依赖
bash
# 1. 创建基础 Vite 项目
npm create vite@latest react-enterprise-template -- --template react
cd react-enterprise-template
npm install
# 2. 安装所有核心依赖
npm install react-router-dom zustand antd @ant-design/icons @tanstack/react-query axios framer-motion
# 3. 安装新增功能依赖
npm install i18next react-i18next i18next-browser-languagedetector i18next-http-backend
npm install vite-plugin-style-import -D
2. 配置 Vite (vite.config.js)
js
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import styleImport from 'vite-plugin-style-import'
export default defineConfig({
plugins: [
react(),
styleImport({
libs: [
{
libraryName: 'antd',
esModule: true,
resolveStyle: (name) => `antd/es/${name}/style/index`,
},
],
}),
],
})
3. 国际化配置 (src/i18n.js)
这是实现多语言的核心。
js
// src/i18n.js
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import Backend from 'i18next-http-backend' // 用于加载外部 JSON 文件
i18n
.use(Backend) // 加载翻译文件
.use(LanguageDetector) // 检测用户语言
.use(initReactI18next) // 将 i18n 传递给 react-i18next
.init({
fallbackLng: 'en', // 默认语言
debug: true,
interpolation: {
escapeValue: false, // 不转义 HTML
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', // 翻译文件路径
},
})
export default i18n
4. 创建翻译文件
在 src/locales/en/translation.json:
json
{
"app_title": "Enterprise Dashboard",
"login": "Login",
"logout": "Logout",
"dashboard": "Dashboard",
"admin_panel": "Admin Panel",
"unauthorized": "Unauthorized Access",
"welcome_user": "Welcome, {{name}}!",
"language": "Language",
"english": "English",
"chinese": "Chinese",
"switch_role": "Switch Role (Demo)",
"role_admin": "Admin",
"role_user": "User",
"secret_content": "This is a secret admin-only content.",
"public_content": "This is public content."
}
在 src/locales/zh/translation.json:
json
{
"app_title": "企业管理系统",
"login": "登录",
"logout": "退出登录",
"dashboard": "仪表盘",
"admin_panel": "管理员面板",
"unauthorized": "无权访问",
"welcome_user": "欢迎您,{{name}}!",
"language": "语言",
"english": "英文",
"chinese": "中文",
"switch_role": "切换角色 (演示)",
"role_admin": "管理员",
"role_user": "普通用户",
"secret_content": "这是仅供管理员查看的秘密内容。",
"public_content": "这是公开的内容。"
}
5. 认证状态管理 (src/store/authStore.js)
使用 Zustand 管理用户登录状态和角色。
js
// src/store/authStore.js
import create from 'zustand'
const useAuthStore = create((set, get) => ({
user: null, // { name: 'Alice', role: 'admin' }
token: null,
login: (user, token) => {
set({ user, token })
localStorage.setItem('auth_token', token)
localStorage.setItem('auth_user', JSON.stringify(user))
},
logout: () => {
set({ user: null, token: null })
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
},
initialize: () => {
// 页面刷新时恢复状态
const token = localStorage.getItem('auth_token')
const userStr = localStorage.getItem('auth_user')
if (token && userStr) {
try {
const user = JSON.parse(userStr)
set({ user, token })
} catch (e) {
console.error("Failed to parse user from local storage")
}
}
},
switchRole: () => {
// 演示用:切换当前用户的角色
const currentUser = get().user
if (!currentUser) return
const newRole = currentUser.role === 'admin' ? 'user' : 'admin'
const updatedUser = { ...currentUser, role: newRole }
set({ user: updatedUser })
localStorage.setItem('auth_user', JSON.stringify(updatedUser))
}
}))
export default useAuthStore
6. 路由守卫 (src/components/AuthGuard.jsx)
这是一个高阶组件(HOC),用于保护路由。
jsx
// src/components/AuthGuard.jsx
import React from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import useAuthStore from '../store/authStore'
/**
* 路由守卫组件
* @param {React.ReactNode} children - 需要保护的组件
* @param {string[]} allowedRoles - 允许访问的角色数组,为空则不检查角色
*/
const AuthGuard = ({ children, allowedRoles = [] }) => {
const { user, initialize } = useAuthStore()
const location = useLocation()
// 初始化认证状态(从 localStorage 恢复)
React.useEffect(() => {
initialize()
}, [])
// 1. 检查是否已登录
if (!user) {
// 未登录,重定向到登录页,并记录当前尝试访问的地址
return <Navigate to="/login" state={{ from: location }} replace />
}
// 2. 检查角色权限
if (allowedRoles.length > 0 && !allowedRoles.includes(user.role)) {
// 已登录但无权限
return <Navigate to="/unauthorized" replace />
}
// 3. 通过所有检查,渲染子组件
return children
}
export default AuthGuard
7. 路由配置 (src/router/index.jsx)
定义所有路由,并应用守卫。
jsx
// src/router/index.jsx
import { createBrowserRouter } from 'react-router-dom'
import App from '../App'
import AuthGuard from '../components/AuthGuard'
import Login from '../pages/Login'
import Dashboard from '../pages/Dashboard'
import Admin from '../pages/Admin'
import Unauthorized from '../pages/Unauthorized'
import { useTranslation } from 'react-i18next'
// 一个包装组件,用于在渲染时触发 i18n 更新
const I18nWrapper = ({ children }) => {
const { i18n } = useTranslation()
// 当语言改变时,强制重新渲染以应用新语言
// 注意:在真实大型应用中,可能不需要这个,但 Vite/React 热更新有时需要它来触发 re-render
return <>{children}</>
}
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{ index: true, element: <Navigate to="/dashboard" replace /> },
{ path: 'login', element: <Login /> },
{ path: 'unauthorized', element: <Unauthorized /> },
{
path: 'dashboard',
element: (
<I18nWrapper>
<AuthGuard>
<Dashboard />
</AuthGuard>
</I18nWrapper>
),
},
{
path: 'admin',
element: (
<I18nWrapper>
<AuthGuard allowedRoles={['admin']}>
<Admin />
</AuthGuard>
</I18nWrapper>
),
},
],
},
])
export default router
8. 主入口与布局 (src/main.jsx, src/App.jsx)
整合路由、i18n 和全局布局。
jsx
// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RouterProvider } from 'react-router-dom'
import App from './App'
import router from './router'
import './i18n' // 必须在 RouterProvider 之前导入以初始化 i18n
import './styles/global.css'
const queryClient = new QueryClient()
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>,
)
jsx
// src/App.jsx
import React from 'react'
import { Layout } from 'antd'
import { Outlet } from 'react-router-dom'
import Sidebar from './components/Sidebar'
import LanguageSwitcher from './components/LanguageSwitcher'
import useAuthStore from './store/authStore'
import { useTranslation } from 'react-i18next'
const { Header, Content, Footer } = Layout
export default function App() {
const { user } = useAuthStore()
const { t } = useTranslation()
return (
<Layout style={{ minHeight: '100vh' }}>
{/* 侧边栏:仅登录后显示 */}
{user && <Sidebar />}
<Layout>
<Header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '0 24px' }}>
<h2 style={{ color: 'white', margin: 0 }}>{t('app_title')}</h2>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
{user && <span style={{ color: 'white' }}>Hi, {user.name}</span>}
<LanguageSwitcher />
</div>
</Header>
<Content style={{ margin: '24px' }}>
<div style={{ padding: 24, minHeight: 360, background: '#fff', borderRadius: 8 }}>
<Outlet />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
React Enterprise Template ©{new Date().getFullYear()}
</Footer>
</Layout>
</Layout>
)
}
9. 关键页面与组件
src/components/Sidebar.jsx (带权限的动态菜单)
jsx
// src/components/Sidebar.jsx
import React from 'react'
import { Menu } from 'antd'
import { Link, useLocation } from 'react-router-dom'
import useAuthStore from '../store/authStore'
import { useTranslation } from 'react-i18next'
import {
DashboardOutlined,
UserOutlined,
SettingOutlined,
LockOutlined,
TranslationOutlined,
} from '@ant-design/icons'
const Sidebar = () => {
const { user } = useAuthStore()
const location = useLocation()
const { t } = useTranslation()
// 根据角色定义菜单项
const menuItems = [
{
key: '/dashboard',
icon: <DashboardOutlined />,
label: <Link to="/dashboard">{t('dashboard')}</Link>,
},
{
key: 'admin-section',
label: t('admin_panel'),
icon: <SettingOutlined />,
children: [
{
key: '/admin',
icon: <LockOutlined />,
label: <Link to="/admin">{t('admin_panel')}</Link>,
},
],
// 权限控制:只有 admin 能看到这个父菜单
hidden: user?.role !== 'admin',
},
{
key: '/unauthorized',
icon: <LockOutlined />,
label: <Link to="/unauthorized">{t('unauthorized')}</Link>,
},
]
return (
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={menuItems.filter(item => !item.hidden)}
/>
)
}
export default Sidebar
src/components/LanguageSwitcher.jsx
jsx
// src/components/LanguageSwitcher.jsx
import React from 'react'
import { Select } from 'antd'
import { useTranslation } from 'react-i18next'
const { Option } = Select
const LanguageSwitcher = () => {
const { i18n } = useTranslation()
const changeLanguage = (lng) => {
i18n.changeLanguage(lng)
}
return (
<Select
defaultValue={i18n.language}
style={{ width: 120 }}
onChange={changeLanguage}
options={[
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文' },
]}
/>
)
}
export default LanguageSwitcher
src/pages/Login.jsx
jsx
// src/pages/Login.jsx
import React, { useState } from 'react'
import { Form, Input, Button, Card, message } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { useNavigate, useLocation } from 'react-router-dom'
import useAuthStore from '../store/authStore'
import { useTranslation } from 'react-i18next'
const Login = () => {
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const location = useLocation()
const { login } = useAuthStore()
const { t } = useTranslation()
const onFinish = (values) => {
setLoading(true)
// 模拟 API 登录请求
setTimeout(() => {
// 演示逻辑:输入任意用户名密码登录,默认给 admin 角色
const mockUser = { name: values.username || 'Demo User', role: 'admin' }
const mockToken = 'fake-jwt-token'
login(mockUser, mockToken)
message.success(t('login') + ' Success!')
// 重定向到之前尝试访问的页面或仪表盘
const from = location.state?.from?.pathname || '/dashboard'
navigate(from, { replace: true })
setLoading(false)
}, 800)
}
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '80vh' }}>
<Card title={t('login')} style={{ width: 400 }}>
<Form onFinish={onFinish}>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please input your Username!' }]}
>
<Input prefix={<UserOutlined />} placeholder="Username" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
{t('login')}
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default Login
src/pages/Dashboard.jsx
jsx
// src/pages/Dashboard.jsx
import React from 'react'
import { Card, Button, Typography } from 'antd'
import { useTranslation } from 'react-i18next'
import useAuthStore from '../store/authStore'
const { Title, Paragraph } = Typography
const Dashboard = () => {
const { t } = useTranslation()
const { user, switchRole } = useAuthStore()
return (
<div>
<Title level={3}>{t('welcome_user', { name: user?.name })}</Title>
<Paragraph>{t('public_content')}</Paragraph>
<Card title={t('switch_role')} style={{ marginTop: 24 }}>
<p>Your current role is: <strong>{user?.role.toUpperCase()}</strong></p>
<Button onClick={switchRole}>
Switch to {user?.role === 'admin' ? 'User' : 'Admin'}
</Button>
</Card>
</div>
)
}
export default Dashboard
src/pages/Admin.jsx
jsx
// src/pages/Admin.jsx
import React from 'react'
import { Card, Alert } from 'antd'
import { useTranslation } from 'react-i18next'
const Admin = () => {
const { t } = useTranslation()
return (
<Card title={t('admin_panel')}>
<Alert
message={t('secret_content')}
type="info"
showIcon
description="This content is only visible because you have the 'admin' role."
/>
</Card>
)
}
export default Admin
src/pages/Unauthorized.jsx
jsx
// src/pages/Unauthorized.jsx
import React from 'react'
import { Result, Button } from 'antd'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
const Unauthorized = () => {
const { t } = useTranslation()
const navigate = useNavigate()
return (
<Result
status="403"
title="403"
subTitle={t('unauthorized')}
extra={
<Button type="primary" onClick={() => navigate('/')}>
Back Home
</Button>
}
/>
)
}
export default Unauthorized
10. 全局样式 (src/styles/global.css)
css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f0f2f5;
}
#root {
min-height: 100vh;
}
/* 覆盖 Ant Design 默认样式以配合布局 */
.ant-layout-sider {
background: #001529 !important;
}
11. 更新 index.html 和 package.json
index.html 保持不变,主要更新 package.json 的 dependencies 和 scripts。
json
// package.json
{
"name": "react-enterprise-template",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@tanstack/react-query": "^5.17.19",
"antd": "^5.12.5",
"axios": "^1.6.7",
"framer-motion": "^11.0.5",
"i18next": "^23.7.11",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-http-backend": "^2.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.0.1",
"react-router-dom": "^6.22.1",
"zustand": "^4.5.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.1.4",
"vite-plugin-style-import": "^2.0.0"
}
}
三、运行与测试
-
安装依赖并启动:
bashnpm install npm run dev打开
http://localhost:5173。 -
功能测试流程:
- 登录 :访问
/login,输入任意用户名密码登录。 - 语言切换:点击右上角下拉框,在"English"和"中文"之间切换,观察页面内容变化。
- 路由守卫 :直接访问
/dashboard,会被自动跳转到/login。 - 权限控制 :
- 登录后,侧边栏会显示"Admin Panel"菜单。
- 点击进入
/admin页面,可以看到管理员内容。 - 点击"Switch Role"按钮,将角色从
admin切换到user。 - 刷新页面后,你会发现"Admin Panel"菜单消失了,因为
AuthGuard检测到你不再是admin角色,无法再访问/admin路由。
- 无权限页面 :手动在地址栏输入
/admin,如果当前角色是user,会被重定向到/unauthorized页面。
- 登录 :访问
四、总结
这个模板提供了一个坚实的企业级 React 应用基础,涵盖了现代前端开发的几乎所有核心需求:
- 架构清晰:模块化设计,易于维护和扩展。
- 用户体验:国际化支持、流畅的布局和动画。
- 安全可靠:完善的路由守卫和基于角色的权限控制。
- 开发效率:集成了最佳实践的状态管理、数据请求和 UI 组件库。