Skip to Content
课程手册第 8 章:📈 持续成长与进阶8.2 从功能到系统:架构思维入门
⏱ 本页预计时间
阅读 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 个关键转变

  1. 单文件 → 模块化

    • 拆分组件,职责单一
    • 提取 Hooks,逻辑复用
  2. 硬编码 → 配置驱动

    • 环境变量管理
    • 功能开关控制
  3. 临时方案 → 统一标准

    • 统一错误处理
    • 统一日志格式
  4. 本地状态 → 全局状态

    • 跨页面数据共享
    • 数据持久化
  5. 随意结构 → 分层架构

    • 按功能模块拆分
    • 共享代码抽离
  6. 快速实现 → 长期维护

    • 考虑扩展性
    • 注重可测试性

下一节8.3 持续成长:实用技能清单 - 掌握性能优化、技术选型、学习方法等实战技能

学习进度0%
0/60 篇已完成
Last updated on