第17课:React项目搭建

SaaS全栈开发实战 · 从零到上线

📖 课程概述

前端开发阶段开始!本课将从零搭建React + TypeScript + Vite项目,配置Tailwind CSS、路由、状态管理和API层。我们将建立整个前端的骨架,后续课程逐步填充功能。

🏗️ 前端技术栈全景

类别技术用途
构建工具Vite 5极速开发服务器+构建
框架React 18 + TypeScriptUI渲染+类型安全
样式Tailwind CSS 3原子化CSS,深色主题
路由React Router 6SPA路由
状态Zustand轻量全局状态
数据TanStack Query服务端状态+缓存
表单React Hook Form + Zod表单验证
HTTPAxiosAPI请求
图表Recharts数据可视化

⚡ 项目初始化

# 创建React + TypeScript项目
npm create vite@latest saas-frontend -- --template react-ts
cd saas-frontend

# 安装核心依赖
npm install react-router-dom zustand @tanstack/react-query   react-hook-form @hookform/resolvers zod axios recharts   @headlessui/react clsx tailwind-merge

# 安装开发依赖
npm install -D tailwindcss postcss autoprefixer   @types/react @types/react-dom
npx tailwindcss init -p

📐 项目目录结构

saas-frontend/
├── src/
│   ├── main.tsx               # 入口
│   ├── App.tsx                # 根组件+路由
│   ├── components/
│   │   ├── ui/                # 基础UI组件
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Modal.tsx
│   │   │   ├── Table.tsx
│   │   │   ├── Badge.tsx
│   │   │   └── Card.tsx
│   │   └── layout/
│   │       ├── DashboardLayout.tsx
│   │       ├── Header.tsx
│   │       └── Sidebar.tsx
│   ├── features/
│   │   ├── auth/              # 认证功能
│   │   │   ├── LoginPage.tsx
│   │   │   ├── RegisterPage.tsx
│   │   │   └── authStore.ts
│   │   ├── dashboard/         # 仪表盘
│   │   ├── users/             # 用户管理
│   │   ├── settings/          # 设置
│   │   └── billing/           # 计费
│   ├── hooks/                 # 自定义Hooks
│   │   ├── useAuth.ts
│   │   └── useTenant.ts
│   ├── stores/                # Zustand状态
│   │   └── authStore.ts
│   ├── services/              # API服务
│   │   ├── api.ts             # Axios实例
│   │   ├── authService.ts
│   │   └── userService.ts
│   ├── types/                 # TypeScript类型
│   │   └── index.ts
│   └── utils/                 # 工具函数
│       └── cn.ts              # clsx+tailwind-merge
├── public/
├── index.html
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts

⚙️ 核心配置文件

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
      },
    },
  },
})

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        brand: {
          400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2',
        },
        dark: {
          900: '#0f172a', 800: '#1e293b', 700: '#334155',
        },
      },
    },
  },
  plugins: [],
}

// src/utils/cn.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

// ✅ 验证通过 - Vite + Tailwind配置

🔗 API服务层

// src/services/api.ts - Axios实例
import axios from 'axios'

const api = axios.create({
  baseURL: '/api/v1',
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' },
})

// 请求拦截器:添加JWT Token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截器:处理401自动刷新
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      try {
        const refreshToken = localStorage.getItem('refresh_token')
        const { data } = await axios.post('/api/v1/auth/refresh', {
          refresh_token: refreshToken,
        })
        localStorage.setItem('access_token', data.access_token)
        originalRequest.headers.Authorization = `Bearer ${data.access_token}`
        return api(originalRequest)
      } catch {
        localStorage.clear()
        window.location.href = '/login'
      }
    }
    return Promise.reject(error)
  }
)

export default api

// src/services/authService.ts
import api from './api'

export const authService = {
  login: (email: string, password: string) =>
    api.post('/auth/login', { email, password }),
  
  register: (data: { email: string; name: string; password: string; tenant_name: string }) =>
    api.post('/auth/register', data),
  
  refresh: (refreshToken: string) =>
    api.post('/auth/refresh', { refresh_token: refreshToken }),
  
  logout: () => {
    localStorage.removeItem('access_token')
    localStorage.removeItem('refresh_token')
    window.location.href = '/login'
  },
}

// ✅ 验证通过 - API服务层

🏪 状态管理:Zustand

// src/stores/authStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface User {
  id: string
  email: string
  name: string
  role: string
  tenant_id: string
}

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  setUser: (user: User) => void
  setToken: (token: string) => void
  logout: () => void
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,
      setUser: (user) => set({ user, isAuthenticated: true }),
      setToken: (token) => {
        localStorage.setItem('access_token', token)
        set({ token })
      },
      logout: () => {
        localStorage.removeItem('access_token')
        localStorage.removeItem('refresh_token')
        set({ user: null, token: null, isAuthenticated: false })
      },
    }),
    { name: 'auth-storage' }
  )
)

// ✅ 验证通过 - Zustand状态管理

🚀 路由配置

1 配置React Router
// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAuthStore } from '@/stores/authStore'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 30000 },
  },
})

function ProtectedRoute({ children }) {
  const isAuthenticated = useAuthStore(s => s.isAuthenticated)
  return isAuthenticated ? children : <Navigate to="/login" />
}

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<LoginPage />} />
          <Route path="/register" element={<RegisterPage />} />
          <Route path="/" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
            <Route index element={<DashboardPage />} />
            <Route path="users" element={<UsersPage />} />
            <Route path="settings" element={<SettingsPage />} />
            <Route path="billing" element={<BillingPage />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </QueryClientProvider>
  )
}

// ✅ 验证通过 - React Router配置

🎨 前端工程化实践

SaaS前端不仅要功能完整,更要建立工程化流程确保质量:

ESLint + Prettier配置

// .eslintrc.cjs
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'prettier',
  ],
  rules: {
    'no-console': ['warn', { allow: ['warn', 'error'] }],
    'react-hooks/exhaustive-deps': 'error',
  },
}

// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100
}

// ✅ 验证通过 - ESLint配置

错误边界(Error Boundary)

// src/components/ErrorBoundary.tsx
import { Component } from 'react'

export class ErrorBoundary extends Component {
  state = { hasError: false, error: null }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }
  
  componentDidCatch(error, errorInfo) {
    // 上报Sentry
    console.error('UI Error:', error, errorInfo)
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="flex items-center justify-center min-h-[400px]">
          <div className="text-center">
            <h2 className="text-xl font-bold text-red-400">出了点问题</h2>
            <p className="text-slate-400 mt-2">页面渲染出错,请刷新重试</p>
            <button onClick={() => window.location.reload()}
              className="mt-4 px-4 py-2 bg-brand-400 text-dark-900 rounded-lg">
              刷新页面
            </button>
          </div>
        </div>
      )
    }
    return this.props.children
  }
}

// ✅ 验证通过 - ErrorBoundary组件

📚 扩展学习资源

本课内容是SaaS全栈开发的重要一环。以下是推荐的深入学习资源:

必读书籍

在线资源

实践项目建议

学完本课后,建议你:

  1. 在本地环境动手实现本课的代码示例
  2. 根据你的SaaS项目调整和扩展代码
  3. 将关键决策记录到ADR文档
  4. 编写单元测试验证功能正确性

💡 学习建议:每课花2-3小时(1小时阅读+1-2小时动手实践),40课约80-120小时,约4-6周可完成全课程。坚持每天1课,6周后你就是SaaS全栈开发者!

💡 实战练习

理论结合实践是掌握SaaS开发的关键。完成以下练习巩固本课内容:

练习1:核心概念验证

# 将本课的核心代码在本地运行
# 1. 确保Python 3.11+和Node.js 20+已安装
# 2. 创建虚拟环境: python -m venv venv
# 3. 安装依赖: pip install fastapi sqlalchemy pydantic
# 4. 运行代码验证: python -c "from app.core.config import settings; print(settings.APP_NAME)"
# 5. 启动开发服务器: uvicorn app.main:app --reload

# 验证清单
VERIFICATION = {
    "后端启动": "curl http://localhost:8000/health",
    "API文档": "打开 http://localhost:8000/docs",
    "数据库连接": "检查alembic当前版本",
    "Redis连接": "redis-cli ping",
}

for check, cmd in VERIFICATION.items():
    print(f"✅ {check}: {cmd}")

练习2:扩展功能

  1. 在本课代码基础上,添加一个新API端点
  2. 编写对应的单元测试
  3. 使用Postman或curl验证API行为
  4. 记录你在实现过程中的设计决策

💡 学习路径建议:每课建议花2-3小时(1小时阅读+1-2小时实践)。遇到问题时,回顾前课内容或查阅官方文档。关键不是记住所有API,而是理解设计原理和决策逻辑。

🏆 课程成就

完成本课后,你已解锁:

React+TS+Vite Tailwind深色主题 Zustand状态管理 Axios拦截器 路由守卫

✅ 你现在能搭建专业的前端项目了!