⏱ 本页预计时间
阅读 19 分钟 · 练习 60 分钟
8.2 从功能到系统:架构思维入门
学会把零散的功能组合成结构清晰的系统。
什么是架构思维? 架构思维是一种系统化的思考方式,不只关注单个功能怎么实现,更关注:
- 如何组织代码 让它易于维护和扩展
- 如何管理数据流 让状态变化可预测
- 如何处理错误 让系统更健壮
- 如何拆分模块 让不同部分独立开发
简单说,就是从「能跑」到「跑得好、改得动、扩得开」。
🎯
学完这节你会:
- 理解单页面到多页应用的演进过程
- 学会组件抽象和复用策略
- 掌握数据流管理(状态提升、Context、Zustand)
- 建立统一的错误处理和日志系统
- 管理多环境配置
- 理解简单的服务拆分思维
演进示例:从单页 Todo 到完整应用
我们用一个 Todo 应用的演进过程,展示架构思维的实际应用。
第 1 阶段:单页面硬编码(入门)
代码特点:所有逻辑都在一个文件里。
// app/page.tsx - 最初版本
'use client'
import { useState } from 'react'
export default function TodoPage() {
const [todos, setTodos] = useState([
{ id: 1, text: '买菜', done: false },
{ id: 2, text: '写代码', done: true }
])
const [input, setInput] = useState('')
const addTodo = () => {
setTodos([...todos, { id: Date.now(), text: input, done: false }])
setInput('')
}
const toggleTodo = (id: number) => {
setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t))
}
return (
<div className="p-8">
<h1 className="text-2xl mb-4">我的待办</h1>
<div className="mb-4">
<input
value={input}
onChange={e => setInput(e.target.value)}
className="border p-2 mr-2"
placeholder="新任务"
/>
<button onClick={addTodo} className="bg-blue-500 text-white px-4 py-2">
添加
</button>
</div>
<div>
{todos.map(todo => (
<div key={todo.id} className="flex items-center mb-2">
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
className="mr-2"
/>
<span className={todo.done ? 'line-through' : ''}>
{todo.text}
</span>
</div>
))}
</div>
</div>
)
}问题:
- ❌ 页面刷新数据丢失
- ❌ 没有类型定义
- ❌ 代码重复(输入框、按钮样式)
- ❌ 逻辑和 UI 耦合
第 2 阶段:组件抽象(提升可维护性)
改进重点:拆分组件,提取复用逻辑。
步骤 1:定义类型
// types/todo.ts
export interface Todo {
id: number
text: string
done: boolean
createdAt: Date
}步骤 2:拆分组件
// components/TodoInput.tsx
interface Props {
value: string
onChange: (value: string) => void
onSubmit: () => void
}
export function TodoInput({ value, onChange, onSubmit }: Props) {
return (
<div className="mb-4">
<input
value={value}
onChange={e => onChange(e.target.value)}
onKeyPress={e => e.key === 'Enter' && onSubmit()}
className="border rounded p-2 mr-2 w-64"
placeholder="新任务..."
/>
<button
onClick={onSubmit}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"
>
添加
</button>
</div>
)
}// components/TodoItem.tsx
import { Todo } from '@/types/todo'
interface Props {
todo: Todo
onToggle: (id: number) => void
onDelete: (id: number) => void
}
export function TodoItem({ todo, onToggle, onDelete }: Props) {
return (
<div className="flex items-center justify-between p-2 border-b">
<div className="flex items-center">
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
className="mr-2"
/>
<span className={todo.done ? 'line-through text-gray-500' : ''}>
{todo.text}
</span>
</div>
<button
onClick={() => onDelete(todo.id)}
className="text-red-500 hover:text-red-700"
>
删除
</button>
</div>
)
}步骤 3:主页面简化
// app/page.tsx - 组件化版本
'use client'
import { useState } from 'react'
import { TodoInput } from '@/components/TodoInput'
import { TodoItem } from '@/components/TodoItem'
import { Todo } from '@/types/todo'
export default function TodoPage() {
const [todos, setTodos] = useState<Todo[]>([])
const [input, setInput] = useState('')
const addTodo = () => {
if (!input.trim()) return
setTodos([...todos, {
id: Date.now(),
text: input,
done: false,
createdAt: new Date()
}])
setInput('')
}
const toggleTodo = (id: number) => {
setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t))
}
const deleteTodo = (id: number) => {
setTodos(todos.filter(t => t.id !== id))
}
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">我的待办</h1>
<TodoInput
value={input}
onChange={setInput}
onSubmit={addTodo}
/>
<div className="border rounded">
{todos.length === 0 ? (
<div className="p-4 text-gray-500 text-center">
还没有任务,添加一个吧!
</div>
) : (
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))
)}
</div>
<div className="mt-4 text-sm text-gray-600">
共 {todos.length} 个任务,已完成 {todos.filter(t => t.done).length} 个
</div>
</div>
)
}改进点:
- ✅ 组件职责单一
- ✅ TypeScript 类型安全
- ✅ 代码复用性强
- ✅ 易于测试和维护
第 3 阶段:数据持久化(接入数据库)
改进重点:连接 Supabase,数据不再丢失。
步骤 1:创建自定义 Hook
// hooks/useTodos.ts
import { useState, useEffect } from 'react'
import { supabase } from '@/lib/supabase'
import { Todo } from '@/types/todo'
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// 加载数据
useEffect(() => {
fetchTodos()
}, [])
const fetchTodos = async () => {
try {
setLoading(true)
const { data, error } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
setTodos(data || [])
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败')
} finally {
setLoading(false)
}
}
const addTodo = async (text: string) => {
try {
const { data, error } = await supabase
.from('todos')
.insert([{ text, done: false }])
.select()
.single()
if (error) throw error
setTodos([data, ...todos])
return { success: true }
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : '添加失败'
}
}
}
const toggleTodo = async (id: number) => {
const todo = todos.find(t => t.id === id)
if (!todo) return
try {
const { error } = await supabase
.from('todos')
.update({ done: !todo.done })
.eq('id', id)
if (error) throw error
setTodos(todos.map(t => t.id === id ? {...t, done: !t.done} : t))
return { success: true }
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : '更新失败'
}
}
}
const deleteTodo = async (id: number) => {
try {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) throw error
setTodos(todos.filter(t => t.id !== id))
return { success: true }
} catch (err) {
return {
success: false,
error: err instanceof Error ? err.message : '删除失败'
}
}
}
return {
todos,
loading,
error,
addTodo,
toggleTodo,
deleteTodo,
refresh: fetchTodos
}
}步骤 2:页面使用 Hook
// app/page.tsx - 使用 Hook 版本
'use client'
import { useState } from 'react'
import { TodoInput } from '@/components/TodoInput'
import { TodoItem } from '@/components/TodoItem'
import { useTodos } from '@/hooks/useTodos'
export default function TodoPage() {
const [input, setInput] = useState('')
const { todos, loading, error, addTodo, toggleTodo, deleteTodo } = useTodos()
const handleAdd = async () => {
if (!input.trim()) return
const result = await addTodo(input)
if (result.success) {
setInput('')
} else {
alert(result.error)
}
}
if (loading) return <div className="p-8">加载中...</div>
if (error) return <div className="p-8 text-red-500">错误:{error}</div>
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">我的待办</h1>
<TodoInput value={input} onChange={setInput} onSubmit={handleAdd} />
<div className="border rounded">
{todos.length === 0 ? (
<div className="p-4 text-gray-500 text-center">
还没有任务,添加一个吧!
</div>
) : (
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))
)}
</div>
<div className="mt-4 text-sm text-gray-600">
共 {todos.length} 个任务,已完成 {todos.filter(t => t.done).length} 个
</div>
</div>
)
}改进点:
- ✅ 数据持久化
- ✅ 逻辑封装在 Hook 中
- ✅ 错误处理完善
- ✅ 组件更简洁
第 4 阶段:全局状态管理(多页共享数据)
当应用有多个页面需要共享 Todo 数据时,用全局状态管理。
选择 Zustand(比 Redux 简单,比 Context 性能好):
步骤 1:安装 Zustand
pnpm install zustand步骤 2:创建 Store
// store/useTodoStore.ts
import { create } from 'zustand'
import { supabase } from '@/lib/supabase'
import { Todo } from '@/types/todo'
interface TodoStore {
todos: Todo[]
loading: boolean
error: string | null
fetchTodos: () => Promise<void>
addTodo: (text: string) => Promise<void>
toggleTodo: (id: number) => Promise<void>
deleteTodo: (id: number) => Promise<void>
}
export const useTodoStore = create<TodoStore>((set, get) => ({
todos: [],
loading: false,
error: null,
fetchTodos: async () => {
set({ loading: true, error: null })
try {
const { data, error } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
set({ todos: data || [], loading: false })
} catch (err) {
set({
error: err instanceof Error ? err.message : '加载失败',
loading: false
})
}
},
addTodo: async (text: string) => {
try {
const { data, error } = await supabase
.from('todos')
.insert([{ text, done: false }])
.select()
.single()
if (error) throw error
set(state => ({ todos: [data, ...state.todos] }))
} catch (err) {
set({ error: err instanceof Error ? err.message : '添加失败' })
}
},
toggleTodo: async (id: number) => {
const todo = get().todos.find(t => t.id === id)
if (!todo) return
try {
const { error } = await supabase
.from('todos')
.update({ done: !todo.done })
.eq('id', id)
if (error) throw error
set(state => ({
todos: state.todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
)
}))
} catch (err) {
set({ error: err instanceof Error ? err.message : '更新失败' })
}
},
deleteTodo: async (id: number) => {
try {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) throw error
set(state => ({ todos: state.todos.filter(t => t.id !== id) }))
} catch (err) {
set({ error: err instanceof Error ? err.message : '删除失败' })
}
}
}))步骤 3:多个页面使用 Store
// app/page.tsx - 首页
'use client'
import { useEffect, useState } from 'react'
import { useTodoStore } from '@/store/useTodoStore'
import { TodoInput } from '@/components/TodoInput'
import { TodoItem } from '@/components/TodoItem'
export default function HomePage() {
const [input, setInput] = useState('')
const { todos, loading, error, fetchTodos, addTodo, toggleTodo, deleteTodo } =
useTodoStore()
useEffect(() => {
fetchTodos()
}, [fetchTodos])
const handleAdd = async () => {
if (!input.trim()) return
await addTodo(input)
setInput('')
}
if (loading) return <div className="p-8">加载中...</div>
if (error) return <div className="p-8 text-red-500">错误:{error}</div>
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">我的待办</h1>
<TodoInput value={input} onChange={setInput} onSubmit={handleAdd} />
{/* ... 其他代码 ... */}
</div>
)
}// app/stats/page.tsx - 统计页面(共享同一份数据)
'use client'
import { useEffect } from 'react'
import { useTodoStore } from '@/store/useTodoStore'
export default function StatsPage() {
const { todos, fetchTodos } = useTodoStore()
useEffect(() => {
fetchTodos()
}, [fetchTodos])
const total = todos.length
const completed = todos.filter(t => t.done).length
const pending = total - completed
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">任务统计</h1>
<div className="grid grid-cols-3 gap-4">
<div className="bg-blue-100 p-6 rounded">
<div className="text-4xl font-bold">{total}</div>
<div className="text-gray-600">总任务数</div>
</div>
<div className="bg-green-100 p-6 rounded">
<div className="text-4xl font-bold">{completed}</div>
<div className="text-gray-600">已完成</div>
</div>
<div className="bg-yellow-100 p-6 rounded">
<div className="text-4xl font-bold">{pending}</div>
<div className="text-gray-600">待完成</div>
</div>
</div>
</div>
)
}改进点:
- ✅ 多页面共享状态
- ✅ 数据自动同步
- ✅ 性能更好(只有使用到的组件才重新渲染)
统一错误处理与日志系统
创建错误处理工具
// lib/error-handler.ts
export class AppError extends Error {
constructor(
message: string,
public code?: string,
public statusCode?: number
) {
super(message)
this.name = 'AppError'
}
}
// 统一错误处理函数
export function handleError(error: unknown): {
message: string
code: string
} {
if (error instanceof AppError) {
return {
message: error.message,
code: error.code || 'UNKNOWN_ERROR'
}
}
if (error instanceof Error) {
return {
message: error.message,
code: 'UNEXPECTED_ERROR'
}
}
return {
message: '发生未知错误',
code: 'UNKNOWN_ERROR'
}
}
// 日志工具
export const logger = {
info: (message: string, data?: any) => {
if (process.env.NODE_ENV === 'development') {
console.log(`[INFO] ${new Date().toISOString()}`, message, data || '')
}
},
error: (message: string, error?: any) => {
console.error(`[ERROR] ${new Date().toISOString()}`, message, error || '')
// 生产环境可以发送到错误追踪服务(如 Sentry)
if (process.env.NODE_ENV === 'production') {
// sendToSentry(message, error)
}
},
warn: (message: string, data?: any) => {
console.warn(`[WARN] ${new Date().toISOString()}`, message, data || '')
}
}在代码中使用
// store/useTodoStore.ts - 使用错误处理
import { logger, handleError, AppError } from '@/lib/error-handler'
export const useTodoStore = create<TodoStore>((set) => ({
// ...
addTodo: async (text: string) => {
try {
logger.info('添加任务', { text })
const { data, error } = await supabase
.from('todos')
.insert([{ text, done: false }])
.select()
.single()
if (error) {
throw new AppError('数据库操作失败', 'DB_ERROR', 500)
}
set(state => ({ todos: [data, ...state.todos] }))
logger.info('任务添加成功', { id: data.id })
} catch (err) {
const { message, code } = handleError(err)
logger.error('添加任务失败', { error: err, code })
set({ error: message })
}
},
// ...
}))多环境配置管理
环境变量配置
# .env.local (本地开发)
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-local-key
# .env.production (生产环境)
NEXT_PUBLIC_APP_ENV=production
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-prod-key配置文件
// config/index.ts
const config = {
app: {
env: process.env.NEXT_PUBLIC_APP_ENV || 'development',
name: 'Todo App',
version: '1.0.0'
},
api: {
supabase: {
url: process.env.NEXT_PUBLIC_SUPABASE_URL!,
anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
}
},
features: {
enableAnalytics: process.env.NEXT_PUBLIC_APP_ENV === 'production',
enableDebugMode: process.env.NEXT_PUBLIC_APP_ENV === 'development'
}
}
export default config简单的服务拆分思维
当应用变复杂,可以按功能拆分:
src/
├── features/ # 按功能模块拆分
│ ├── todos/
│ │ ├── components/ # Todo 相关组件
│ │ ├── hooks/ # Todo 相关 hooks
│ │ ├── store/ # Todo 状态管理
│ │ └── types/ # Todo 类型定义
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── store/
│ └── analytics/
│ ├── components/
│ └── utils/
├── shared/ # 共享模块
│ ├── components/ # 通用组件 (Button, Input)
│ ├── hooks/ # 通用 hooks
│ └── utils/ # 工具函数
└── app/ # 页面路由
├── page.tsx # 使用 features/todos
├── auth/page.tsx # 使用 features/auth
└── stats/page.tsx # 使用 features/analytics优点:
- ✅ 职责清晰
- ✅ 易于维护
- ✅ 代码复用
- ✅ 团队协作方便
架构思维总结
从功能到系统的 6 个关键转变:
-
单文件 → 模块化
- 拆分组件,职责单一
- 提取 Hooks,逻辑复用
-
硬编码 → 配置驱动
- 环境变量管理
- 功能开关控制
-
临时方案 → 统一标准
- 统一错误处理
- 统一日志格式
-
本地状态 → 全局状态
- 跨页面数据共享
- 数据持久化
-
随意结构 → 分层架构
- 按功能模块拆分
- 共享代码抽离
-
快速实现 → 长期维护
- 考虑扩展性
- 注重可测试性
下一节:8.3 持续成长:实用技能清单 - 掌握性能优化、技术选型、学习方法等实战技能
学习进度0%
0/60 篇已完成
Last updated on