SaaS全栈开发实战 · 从零到上线
前端开发阶段开始!本课将从零搭建React + TypeScript + Vite项目,配置Tailwind CSS、路由、状态管理和API层。我们将建立整个前端的骨架,后续课程逐步填充功能。
| 类别 | 技术 | 用途 |
|---|---|---|
| 构建工具 | Vite 5 | 极速开发服务器+构建 |
| 框架 | React 18 + TypeScript | UI渲染+类型安全 |
| 样式 | Tailwind CSS 3 | 原子化CSS,深色主题 |
| 路由 | React Router 6 | SPA路由 |
| 状态 | Zustand | 轻量全局状态 |
| 数据 | TanStack Query | 服务端状态+缓存 |
| 表单 | React Hook Form + Zod | 表单验证 |
| HTTP | Axios | API请求 |
| 图表 | 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配置
// 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服务层
// 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状态管理
// 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前端不仅要功能完整,更要建立工程化流程确保质量:
// .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配置
// 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全栈开发的重要一环。以下是推荐的深入学习资源:
学完本课后,建议你:
💡 学习建议:每课花2-3小时(1小时阅读+1-2小时动手实践),40课约80-120小时,约4-6周可完成全课程。坚持每天1课,6周后你就是SaaS全栈开发者!
理论结合实践是掌握SaaS开发的关键。完成以下练习巩固本课内容:
# 将本课的核心代码在本地运行
# 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-3小时(1小时阅读+1-2小时实践)。遇到问题时,回顾前课内容或查阅官方文档。关键不是记住所有API,而是理解设计原理和决策逻辑。
完成本课后,你已解锁:
React+TS+Vite Tailwind深色主题 Zustand状态管理 Axios拦截器 路由守卫✅ 你现在能搭建专业的前端项目了!