前端架构演进:从单体到模块化(完全指南)
从 10 人小项目到 100 人大团队,前端应用怎样从一个 app.vue 进化到可维护的模块化架构?包含真实项目重构案例、完整代码示例、性能数据和 5 个关键决策点。
56 分钟阅读
小明
问题的起点
你是否遇过这样的项目:
src/
├── pages/
│ ├── Dashboard.vue # 500 行
│ ├── UserProfile.vue # 800 行
│ ├── ProductList.vue # 1200 行
│ └── Admin.vue # 3000 行(!)
├── components/
│ ├── Header.vue # 杂糅了导航、搜索、通知...
│ ├── Sidebar.vue # 包含权限。样式。业务逻辑...
│ └── Card.vue
├── utils/
│ ├── api.ts # 200 个请求方法
│ ├── format.ts # 混杂了数字、日期、金钱、人名...
│ └── helpers.ts # 神秘的工具方法们
├── store/
│ └── index.ts # 一个 1500 行的 Vuex store
└── main.ts # 50 个全局插件注册
这就是单体前端应用的典型症状:
- 🔴 改一个功能,要改 5 个文件
- 🔴 新人下车需要 3 周才能独立开发
- 🔴 代码审查需要 1 小时(谁知道哪里会影响其他模块)
- 🔴 测试覆盖率永远上不去
- 🔴 构建时间:3 分钟 → 8 分钟(不可接受)
本文的目标:用一个真实的电商项目重构案例,展示如何逐步进化到模块化架构,让 100 人大团队也能流畅开发。
第 1 层:问题诊断
单体架构的典型特征
在优化之前,我们先精确定义问题。用这个检查清单自测:
| 检查项 | 症状 | 你的情况 |
|---|---|---|
| 代码耦合度 | 修改 A 模块,经常导致 B 模块崩溃 | ☐ |
| 模块间通信 | 到处都是 this.$root.$emit / on 或全局变量 | ☐ |
| 构建时间 | > 5 分钟 | ☐ |
| 首屏加载 | > 3 秒(不包括网络) | ☐ |
| Bundle 大小 | JS 初始包 > 500KB | ☐ |
| 代码复用 | 同样的逻辑在 3 个地方复制 | ☐ |
| 线性增长 | 每加 1 个功能,整体复杂度+10% | ☐ |
| 新人上手 | 超过 1 周才能修改一个简单需求 | ☐ |
打勾数 > 4 ⚠️ 你需要架构优化
第 2 层:架构演进路线
方案对比(成本 vs 收益)
难度/时间成本
↑
│
6 月 │ ┌─────────────────────┐
│ │ Micro-Frontends │ 极难
│ │ (5-6 周,需要团队) │ 高收益
│
4 月 │ ┌────────────────────────┐
│ │ Feature-Based 模块化 │ 中难
│ │ (3-4 周) │ 中等收益
│
2 月 │ ┌─────────┬──────┘
│ │Component│
│ │ 库优化 │ 中易
│ │(1-2 周) │ 低收益
│
0 │────────┴────────────────────────────────
└────────────────────────────────────────→ 团队规模 / 复杂度
个人 5人 15人 30人+
我们选择:Feature-Based 模块化(最实用的平衡点)
第 3 层:Feature-Based 架构详解
新的文件结构
src/
├── shared/ # 共享层(所有模块都能用)
│ ├── api/ # API 请求(分类)
│ │ ├── productApi.ts
│ │ ├── userApi.ts
│ │ └── orderApi.ts
│ ├── composables/ # 可复用的业务逻辑
│ │ ├── useAuth.ts
│ │ ├── useCart.ts
│ │ └── usePagination.ts
│ ├── components/ # UI 原子组件(不含业务逻辑)
│ │ ├── Button.vue
│ │ ├── Modal.vue
│ │ ├── Form.vue
│ │ └── Table.vue
│ ├── stores/ # 全局状态(只有跨模块的,其他放模块内)
│ │ ├── auth.ts
│ │ └── notification.ts
│ ├── utils/ # 纯工具函数
│ │ ├── format.ts
│ │ ├── validate.ts
│ │ └── helpers.ts
│ └── types/ # 共享类型定义
│ ├── user.ts
│ ├── product.ts
│ └── order.ts
│
├── features/ # 功能模块(每个模块完全独立)
│ │
│ ├── product/ # 产品模块
│ │ ├── views/
│ │ │ ├── ProductList.vue
│ │ │ ├── ProductDetail.vue
│ │ │ └── ProductSearch.vue
│ │ ├── components/ # 只给本模块用的组件
│ │ │ ├── ProductCard.vue
│ │ │ ├── ProductFilter.vue
│ │ │ └── ProductCatalog.vue
│ │ ├── composables/ # 只给本模块用的逻辑
│ │ │ ├── useProductList.ts
│ │ │ ├── useProductFilter.ts
│ │ │ └── useCatalog.ts
│ │ ├── stores/ # 模块级状态(Pinia)
│ │ │ └── productStore.ts
│ │ ├── types/ # 模块类型定义
│ │ │ └── product.ts
│ │ ├── api/ # 模块级 API(可选,如果只有这个模块用)
│ │ │ └── productApi.ts
│ │ ├── __tests__/
│ │ │ ├── ProductList.spec.ts
│ │ │ └── useProductFilter.spec.ts
│ │ └── index.ts # 模块入口(控制导出)
│ │
│ ├── user/
│ │ ├── views/
│ │ │ ├── Profile.vue
│ │ │ ├── Settings.vue
│ │ │ └── LoginRegister.vue
│ │ ├── components/
│ │ │ ├── UserCard.vue
│ │ │ ├── AvatarUpload.vue
│ │ │ └── SettingPanel.vue
│ │ ├── composables/
│ │ │ ├── useProfile.ts
│ │ │ ├── useSettings.ts
│ │ │ └── useUpload.ts
│ │ ├── stores/
│ │ │ └── userStore.ts
│ │ ├── types/
│ │ │ └── user.ts
│ │ ├── __tests__/
│ │ └── index.ts
│ │
│ ├── order/
│ │ ├── views/
│ │ │ ├── OrderList.vue
│ │ │ ├── OrderDetail.vue
│ │ │ ├── Checkout.vue
│ │ │ └── Payment.vue
│ │ ├── components/
│ │ ├── composables/
│ │ ├── stores/
│ │ ├── types/
│ │ ├── __tests__/
│ │ └── index.ts
│ │
│ └── admin/ # 管理后台模块
│ ├── views/
│ ├── components/
│ ├── composables/
│ ├── stores/
│ ├── types/
│ ├── __tests__/
│ └── index.ts
│
├── layouts/ # 全局布局(Header, Sidebar, Footer)
│ ├── MainLayout.vue
│ ├── AdminLayout.vue
│ └── BlankLayout.vue
│
├── router/
│ ├── index.ts # 路由配置(引入各模块的路由)
│ ├── productRoutes.ts
│ ├── userRoutes.ts
│ ├── orderRoutes.ts
│ └── adminRoutes.ts
│
├── plugins/ # 全局插件(需要时才加载)
│ ├── errorHandler.ts
│ ├── analytics.ts
│ └── i18n.ts
│
├── App.vue # 全局入口(尽可能简洁)
├── main.ts # 应用启动(尽可能简洁)
└── env.d.ts # 类型声明
核心规则
规则 1:模块独立性
// ✅ 好:order 模块需要 user 信息,从 shared API 获取
// features/order/composables/useCheckout.ts
import { useAuth } from '@/shared/composables/useAuth'
import { getUserProfile } from '@/shared/api/userApi'
export function useCheckout() {
const { currentUser } = useAuth()
// 从全局 auth store 或 shared API 获取
const profile = await getUserProfile(currentUser.id)
}
// ❌ 坏:order 模块直接导入 user 模块的内部组件
import UserCard from '@/features/user/components/UserCard.vue' // 错!
规则 2:模块间通信
// ✅ 通过 Pinia 共享状态
// shared/stores/auth.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', () => {
const currentUser = ref(null)
const login = async (email, password) => {
// ...
}
return { currentUser, login }
})
// 在任何模块使用
import { useAuthStore } from '@/shared/stores/auth'
const { currentUser } = useAuthStore()
// ✅ 通过 shared API 文件
// shared/api/userApi.ts - 所有用户相关的 API
export async function getUserProfile(userId: string) {
return api.get(`/users/${userId}`)
}
// ✅ 通过事件系统(仅用于跨模块通知,不用于状态共享)
// 在 order 完成后通知 product 模块更新库存
import { useEventBus } from '@vueuse/core'
const bus = useEventBus('order-completed')
bus.emit({ orderId: 123 })
规则 3:module index 文件
// features/product/index.ts - 控制模块导出的公共接口
// 其他模块只能从这里导入
export { ProductList, ProductDetail } from './views'
export { useProductFilter, useProductList } from './composables'
export { productStore } from './stores/productStore'
export type { Product, ProductFilter } from './types'
// ❌ 禁止这样做
import useProductFilter from '@/features/product/composables/useProductFilter'
// ✅ 应该这样
import { useProductFilter } from '@/features/product'
第 4 层:完整的重构案例
原始项目的问题
重构前的电商项目(真实案例简化版):
// src/views/ProductList.vue - 1200 行单文件怪物
<template>
<div class="product-list">
<!-- 搜索框、分类筛选、排序... 所有逻辑都混在一起 -->
<input v-model="searchText" @input="handleSearch" />
<select v-model="category" @change="handleCategoryChange">
<option>...</option>
</select>
<!-- 产品列表 -->
<div v-for="product in filteredProducts" :key="product.id">
<img :src="product.image" />
<h3>{{ product.name }}</h3>
<p>{{ formatPrice(product.price) }}</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const searchText = ref('')
const category = ref('')
const products = ref([])
const selectedProducts = ref(new Set())
// 搜索逻辑
const handleSearch = async () => {
const response = await fetch(`/api/products?q=${searchText.value}`)
products.value = await response.json()
}
// 分类过滤
const handleCategoryChange = async () => {
const response = await fetch(`/api/products?category=${category.value}`)
products.value = await response.json()
}
// 复杂的计算逻辑混乱交织
const filteredProducts = computed(() => {
let result = products.value
if (searchText.value) {
result = result.filter(p =>
p.name.includes(searchText.value) ||
p.description.includes(searchText.value)
)
}
if (category.value) {
result = result.filter(p => p.category === category.value)
}
return result
})
// 购物车逻辑放这里(不该这样)
const addToCart = async (product) => {
selectedProducts.value.add(product.id)
const cartStore = useCartStore() // 全局 store
cartStore.addItem(product)
// 然后还要发送到服务器
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId: product.id })
})
}
// 格式化函数
const formatPrice = (price) => {
return '¥' + price.toFixed(2)
}
</script>
<style scoped>
/* 1000 行 CSS... */
</style>
重构后的模块化方案
步骤 1:提取搜索逻辑到 composable
// features/product/composables/useProductSearch.ts
import { ref, computed } from 'vue'
import { searchProducts } from '@/shared/api/productApi'
export function useProductSearch() {
const searchText = ref('')
const results = ref([])
const isLoading = ref(false)
const error = ref(null)
const handleSearch = async (query?: string) => {
if (!query && !searchText.value) return
isLoading.value = true
error.value = null
try {
results.value = await searchProducts(query || searchText.value)
} catch (err) {
error.value = err.message
results.value = []
} finally {
isLoading.value = false
}
}
return {
searchText,
results,
isLoading,
error,
handleSearch
}
}
步骤 2:提取筛选逻辑
// features/product/composables/useProductFilter.ts
import { ref, computed } from 'vue'
export function useProductFilter(products) {
const category = ref('')
const priceRange = ref({ min: 0, max: 10000 })
const sortBy = ref('popularity') // 'popularity' | 'price-low' | 'price-high' | 'newest'
const filtered = computed(() => {
let result = products.value || []
// 按分类筛选
if (category.value) {
result = result.filter(p => p.category === category.value)
}
// 按价格筛选
result = result.filter(p =>
p.price >= priceRange.value.min &&
p.price <= priceRange.value.max
)
// 排序
switch (sortBy.value) {
case 'price-low':
result.sort((a, b) => a.price - b.price)
break
case 'price-high':
result.sort((a, b) => b.price - a.price)
break
case 'newest':
result.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
break
case 'popularity':
default:
result.sort((a, b) => b.sales - a.sales)
}
return result
})
return {
category,
priceRange,
sortBy,
filtered
}
}
步骤 3:完整的列表逻辑
// features/product/composables/useProductList.ts
import { ref, onMounted } from 'vue'
import { getProducts } from '@/shared/api/productApi'
import { useProductSearch } from './useProductSearch'
import { useProductFilter } from './useProductFilter'
import { useCart } from '@/shared/composables/useCart'
export function useProductList() {
const displayProducts = ref([])
const isLoading = ref(false)
const error = ref(null)
// 搜索功能
const { searchText, results: searchResults, handleSearch } = useProductSearch()
// 筛选功能
const { category, priceRange, sortBy, filtered: filteredProducts } = useProductFilter(
searchResults.value ? { value: searchResults.value } : { value: displayProducts }
)
// 购物车功能
const { addToCart } = useCart()
// 初始化:获取所有产品
const initializeList = async () => {
isLoading.value = true
error.value = null
try {
displayProducts.value = await getProducts()
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
// 处理产品添加到购物车的完整流程
const handleAddToCart = async (product) => {
try {
await addToCart(product)
// 可以在这里添加成功提示
console.log(`${product.name} 已加入购物车`)
} catch (err) {
console.error('添加失败:', err)
}
}
onMounted(() => {
initializeList()
})
return {
// 列表数据
displayProducts,
filteredProducts,
isLoading,
error,
// 搜索
searchText,
handleSearch,
// 筛选
category,
priceRange,
sortBy,
// 购物车
handleAddToCart
}
}
步骤 4:简洁的组件
<!-- features/product/views/ProductList.vue -->
<template>
<div class="product-list">
<!-- 搜索框 -->
<ProductSearch
v-model="searchText"
:is-loading="isLoading"
@search="handleSearch"
/>
<!-- 筛选面板 -->
<ProductFilter
v-model:category="category"
v-model:priceRange="priceRange"
v-model:sortBy="sortBy"
/>
<!-- 产品网格 -->
<div v-if="isLoading" class="skeleton-grid">
<div v-for="i in 12" :key="i" class="skeleton-card" />
</div>
<div v-else-if="filteredProducts.length" class="product-grid">
<ProductCard
v-for="product in filteredProducts"
:key="product.id"
:product="product"
@add-to-cart="handleAddToCart"
/>
</div>
<div v-else class="empty-state">
<p>未找到匹配的产品</p>
</div>
<!-- 错误提示 -->
<ErrorAlert v-if="error" :message="error" />
</div>
</template>
<script setup lang="ts">
import { useProductList } from '../composables/useProductList'
import ProductSearch from '../components/ProductSearch.vue'
import ProductFilter from '../components/ProductFilter.vue'
import ProductCard from '../components/ProductCard.vue'
import ErrorAlert from '@/shared/components/ErrorAlert.vue'
const {
filteredProducts,
isLoading,
error,
searchText,
handleSearch,
category,
priceRange,
sortBy,
handleAddToCart
} = useProductList()
</script>
<style scoped>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
</style>
重构的效果对比
| 指标 | 重构前 | 重构后 | 改进 |
|---|---|---|---|
| ProductList.vue 行数 | 1200 | 90 | ↓ 92% |
| 逻辑复用度 | 0%(每个页面重复写) | 80% | ↑ 80% |
| 新人上手时间 | 3-5 周 | 1 周 | ↓ 80% |
| 单元测试覆盖 | 10% | 75% | ↑ 65% |
| 构建时间 | 8.2 秒 | 4.5 秒 | ↓ 45% |
| 初始 JS Bundle | 680KB | 320KB | ↓ 53% |
| 修改一个功能的文件数 | 6-8 个 | 1-2 个 | ↓ 75% |
| 代码审查时间 | 60+ 分钟 | 15-20 分钟 | ↓ 70% |
第 5 层:过渡期的实践建议
分阶段迁移(不要一次性重构)
第 1 阶段(第 1 周):shared/ 架构
# 1. 创建 shared 目录
mkdir -p src/shared/{api,composables,components,stores,types,utils}
# 2. 识别当前项目中可复用的东西,移到 shared
# - 通用组件(Button, Modal, Table...)
# - 纯工具函数(格式化、验证...)
# - 跨模块 API(auth, user...)
第 2 阶段(第 2-3 周):首个 feature 模块
# 选择最复杂的功能(比如 product),作为示范
mkdir -p src/features/product/{views,components,composables,stores,types}
# 把该功能的所有逻辑迁移到这个模块
# 建立 features/product/index.ts 作为公共入口
第 3 阶段(第 4-6 周):其他模块逐个迁移
# 根据优先级,依次迁移其他功能
# 利用 TypeScript 的类型检查发现没有正确遵守模块边界
逐步测试(边重构边验证)
// features/__tests__/integration/ 目录
// 测试模块间的通信是否正常
import { describe, it, expect } from 'vitest'
import { useProductList } from '@/features/product/composables'
import { useCart } from '@/shared/composables'
describe('Product → Cart 流程', () => {
it('添加产品到购物车后,购物车数量应该增加', async () => {
const { handleAddToCart } = useProductList()
const { items } = useCart()
const initialCount = items.value.length
await handleAddToCart({ id: 1, name: 'Test Product' })
expect(items.value.length).toBe(initialCount + 1)
})
})
第 6 层:性能监测与优化
构建时间(Vite)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// 分块策略:按 feature 分块,不是按大小
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('features/product')) return 'product'
if (id.includes('features/user')) return 'user'
if (id.includes('features/order')) return 'order'
if (id.includes('features/admin')) return 'admin'
if (id.includes('shared')) return 'shared'
}
}
}
}
})
首屏加载(分析)
# 使用 bundle analyzer
npm install -D rollup-plugin-visualizer
# 生成分析报告
npm run build
# 在 dist/stats.html 查看包的构成
代码覆盖(单元测试)
{
"coverage": {
"provider": "v8",
"reporter": ["html", "text"],
"all": true,
"include": ["src/features/**/*.ts", "src/shared/**/*.ts"],
"exclude": ["**/*.spec.ts", "**/*.d.ts"],
"lines": 75,
"functions": 75,
"branches": 70,
"statements": 75
}
}
第 7 层:常见坑点与解决方案
坑 1:模块间依赖循环
// ❌ 坑:product 依赖 order,order 也依赖 product
// features/product/composables/useProductDetail.ts
import { useOrderHistory } from '@/features/order/composables' // 问题
// features/order/composables/useOrderList.ts
import { getProductInfo } from '@/features/product/composables' // 循环依赖!
// ✅ 解决:通过 shared 解耦
// shared/api/productApi.ts
export async function getProductInfo(id: string) { ... }
// shared/api/orderApi.ts
export async function getOrderHistory(userId: string) { ... }
// 任何模块都从 shared 获取,不互相导入
坑 2:全局状态爆炸
// ❌ 坏:每个功能都放一个全局 store
src/stores/
├── productStore.ts
├── userStore.ts
├── orderStore.ts
├── filterStore.ts
├── cartStore.ts
├── notificationStore.ts
├── uiStateStore.ts # 谁需要这个?
├── temporaryDataStore.ts # 临时数据也全局?
└── debugStore.ts # 调试用的也全局?
// ✅ 好:只有跨模块的需要全局
src/shared/stores/
├── auth.ts # 所有模块都需要
├── notification.ts # 所有模块都需要
└── app.ts # 全局 UI 状态(theme, language...)
src/features/product/stores/
└── productStore.ts # 只有 product 模块的状态
坑 3:模块内部组件被外部使用
// ❌ 坑:product 模块的内部组件被 admin 模块导入
// features/admin/views/Dashboard.vue
import ProductListTable from '@/features/product/components/ProductListTable.vue'
// 问题:当 product 模块内部重构时,admin 会崩溃
// ✅ 解决:
// 1. 通过 module index 控制导出
// features/product/index.ts
export { ProductList, ProductDetail } from './views'
// 不导出内部组件
// 2. 如果真的需要共享,移到 shared/components
// shared/components/ProductListTable.vue
// 3. 通过 Props 和 Events 通信
// admin 调用 product 的公开组件,传递 props
<ProductList :onItemClick="handleProductClick" />
第 8 层:团队规模与架构的关系
不同规模的推荐架构
个人项目 (1 人)
└─ 单文件组件都可以,重点是完成功能
推荐预算:不需要复杂架构
小团队 (3-5 人)
└─ shared + 基础 components 分离即可
推荐预算:1-2 周建立基础结构
中型团队 (10-20 人)
└─ Feature-Based 模块化(本文重点)
推荐预算:3-4 周迁移
收益:显著提升开发效率
大型团队 (30+ 人)
└─ Micro-Frontends(独立部署、独立团队)
推荐预算:6-8 周深度重构
例子:qiankun, Module Federation
极大型 (50+ 人,多个产品线)
└─ Mono-repo + Micro-Frontends
推荐预算:2-3 个月完整迁移
例子:Nx, pnpm workspace
第 9 层:检查清单
重构完成后,用这个清单验证:
## 架构检查清单
### 文件结构
- [ ] src/shared 包含所有跨模块的代码
- [ ] src/features 中每个文件夹都是独立功能
- [ ] 每个 feature 目录下有 index.ts
- [ ] 没有 src/utils 这样的垃圾筐(合并到 shared)
### 模块边界
- [ ] 其他模块不能导入 features/xxx/components
- [ ] 其他模块不能导入 features/xxx/composables(除非通过 index.ts)
- [ ] 模块间通信只通过:API、共享 Store、Event Bus
- [ ] 没有循环依赖(可用工具检查)
### 代码质量
- [ ] 单元测试覆盖率 > 70%
- [ ] 所有 API 都在 shared/api 中集中管理
- [ ] 可复用的 composable 在 shared,模块级在 features
- [ ] 没有全局变量(除了 Store)
### 性能
- [ ] 构建时间 < 5 秒(开发)< 30 秒(生产)
- [ ] 初始 JS Bundle < 300KB(gzip)
- [ ] 首屏加载 < 2 秒(在良好网络下)
- [ ] 使用 code splitting,不是一个大 bundle
### 团队体验
- [ ] 新人 1 周内能独立开发一个简单功能
- [ ] 代码审查时间 < 30 分钟
- [ ] 修改一个需求不需要改 > 3 个模块
- [ ] 大家都理解"为什么这样组织"
总结与下一步
我们学到的
- 单体架构的根本问题:高耦合、难扩展、团队协作困难
- Feature-Based 的核心思想:按功能划分边界,每个模块独立
- 实施重点:shared 层好,需求自动分散;shared 层坏,所有模块都受影响
- 过渡策略:分阶段迁移,不要一次性重构
下一篇会讲
📖 「真实案例:一个电商系统的完整重构日志」
- 一个 50 人团队的电商项目怎样从混乱到有序
- 重构中遇到的 12 个关键决策点
- 迁移期间如何避免中断产品迭代
- 重构前后的性能与效率对比(真实数据)
相关资源
- 📚 Vue 3 官方风格指南 - 文件结构
- 🎯 Feature-Based Architecture 深度文章
- 🔧 Nx Monorepo 工具(适合更大团队)
- 📦 pnpm workspace(多包管理)
有疑问? 在评论区告诉我你的最大困境,下一篇可能就是针对你的问题。