前端架构模式
What — 什么是前端架构模式
前端架构模式是组织前端代码结构、模块划分、依赖关系的顶层设计。它决定了代码如何分层、模块如何通信、应用如何拆分与组合。
核心架构模式
| 模式 | 核心思想 | 适用规模 |
|---|---|---|
| 单体应用 | 一个仓库一个应用 | 小型项目 |
| 分层架构 | 按职责分层(视图/逻辑/数据) | 中型项目 |
| Monorepo | 多包单仓库,共享工具链 | 中大型 |
| 微前端 | 多团队独立开发独立部署 | 大型/超大型 |
| Islands 架构 | 静态 HTML + 交互岛屿 | 内容型网站 |
Why — 为什么需要前端架构
1. 项目规模增长的必然需求
| 阶段 | 规模 | 痛点 | 推荐架构 |
|---|---|---|---|
| 创业期 | 1-3人 | 快速迭代 | 单体 + 简单分层 |
| 成长期 | 5-15人 | 代码冲突、构建慢 | Monorepo + 分层 |
| 成熟期 | 15+人 | 团队协作、独立部署 | 微前端 |
2. 架构选型的关键维度
| 维度 | 关注点 |
|---|---|
| 开发效率 | 启动速度、热更新、构建速度 |
| 协作效率 | 代码冲突、发布耦合、团队边界 |
| 可维护性 | 模块边界、依赖方向、代码复用 |
| 可扩展性 | 新功能接入成本、团队扩展 |
| 部署灵活性 | 独立部署、灰度发布、回滚 |
How — 各架构模式详解
1. 分层架构
分层架构是最基础的前端架构,将代码按职责分为多个层次。
经典三层架构:
┌──────────────────────────┐
│ 表现层 (Presentation) │ 组件、页面、样式
├──────────────────────────┤
│ 业务层 (Business) │ 状态管理、业务逻辑、Hooks/Composables
├──────────────────────────┤
│ 数据层 (Data) │ API 调用、数据转换、缓存
└──────────────────────────┘
目录结构:
src/
├── pages/ — 页面组件(表现层)
│ ├── Home/
│ ├── User/
│ └── Dashboard/
├── components/ — 通用组件(表现层)
│ ├── Button/
│ ├── Modal/
│ └── Table/
├── composables/ — 业务组合函数(业务层)
│ ├── useAuth.ts
│ ├── useCart.ts
│ └── usePermission.ts
├── stores/ — 状态管理(业务层)
│ ├── userStore.ts
│ └── appStore.ts
├── services/ — API 服务(数据层)
│ ├── userService.ts
│ ├── orderService.ts
│ └── http.ts — Axios 实例
├── utils/ — 工具函数
├── types/ — TypeScript 类型
└── constants/ — 常量
各层职责与依赖规则:
// ===== 数据层:只关心 API 调用和数据转换 =====
// services/http.ts
import axios from 'axios'
const http = axios.create({
baseURL: '/api',
timeout: 10000,
})
http.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
export default http
// services/userService.ts
import http from './http'
import type { User, LoginParams } from '@/types'
export const userService = {
login: (params: LoginParams) => http.post<User>('/auth/login', params),
getProfile: () => http.get<User>('/user/profile'),
updateProfile: (data: Partial<User>) => http.put<User>('/user/profile', data),
}
// ===== 业务层:组合数据层,提供业务逻辑 =====
// composables/useAuth.ts
import { ref, computed } from 'vue'
import { userService } from '@/services/userService'
const user = ref<User | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
export function useAuth() {
const isLoggedIn = computed(() => !!token.value)
async function login(params: LoginParams) {
const { data } = await userService.login(params)
token.value = data.token
user.value = data.user
localStorage.setItem('token', data.token)
}
function logout() {
token.value = null
user.value = null
localStorage.removeItem('token')
}
return { user, token, isLoggedIn, login, logout }
}
<!-- ===== 表现层:只关心渲染和交互 ===== -->
<template>
<form @submit.prevent="handleLogin">
<input v-model="form.username" placeholder="用户名" />
<input v-model="form.password" type="password" placeholder="密码" />
<Button type="submit" :loading="loading">登录</Button>
</form>
</template>
<script setup>
import { useAuth } from '@/composables/useAuth'
import { Button } from '@/components'
const { login } = useAuth()
const loading = ref(false)
const form = reactive({ username: '', password: '' })
async function handleLogin() {
loading.value = true
try {
await login(form)
router.push('/dashboard')
} finally {
loading.value = false
}
}
</script>
依赖方向规则:
表现层 → 业务层 → 数据层
(不能反向依赖)
2. Feature-Sliced Design (FSD)
FSD 是近年流行的前端架构方法论,按业务功能垂直切分,而非按技术层水平切分。
FSD 的层级:
src/
├── app/ — 应用初始化(路由、Provider、全局样式)
├── pages/ — 页面路由组件(组合 features)
├── widgets/ — 页面级区块(Header、Sidebar、Feed)
├── features/ — 业务功能(auth、search、cart)
├── entities/ — 业务实体(user、product、order)
└── shared/ — 共享资源(UI 组件、工具函数、API 客户端)
依赖规则(只能向下依赖):
app → pages → widgets → features → entities → shared
// features/auth/index.ts — 功能的公共 API
export { LoginForm } from './ui/LoginForm'
export { useAuth } from './model/useAuth'
// features/auth/model/useAuth.ts
import { userService } from '@/entities/user/api/userService'
import { userStore } from '@/entities/user/model/userStore'
export function useAuth() {
// 使用 entities 层的 store 和 service
const { setUser } = userStore()
async function login(params: LoginParams) {
const { data } = await userService.login(params)
setUser(data)
}
return { login }
}
// features/auth/ui/LoginForm.vue
// 只导入当前 feature 的公共 API 和 shared 层
FSD vs 传统分层:
| 维度 | 传统分层 | FSD |
|---|---|---|
| 切分方式 | 按技术层(components/stores/services) | 按业务功能(auth/user/product) |
| 修改范围 | 改一个功能需跨多个目录 | 改一个功能只改一个 feature |
| 复用粒度 | 组件级 | 功能级 |
| 上手成本 | 低 | 中(需理解层级规则) |
3. Monorepo 架构
Monorepo 将多个相关包放在同一个仓库中,共享工具链和依赖。
my-monorepo/
├── packages/
│ ├── ui/ — 组件库
│ ├── utils/ — 工具函数库
│ ├── admin-app/ — 后台管理应用
│ └── mobile-app/ — 移动端应用
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
# pnpm-workspace.yaml
packages:
- 'packages/*'
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
}
}
}
// packages/ui/package.json
{
"name": "@my/ui",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --watch"
},
"peerDependencies": {
"react": ">=18"
}
}
// packages/admin-app/package.json
{
"name": "@my/admin-app",
"dependencies": {
"@my/ui": "workspace:*",
"@my/utils": "workspace:*"
}
}
Monorepo 工具对比:
| 维度 | pnpm workspace | Turborepo | Nx |
|---|---|---|---|
| 定位 | 包管理 | 构建编排 | 全功能平台 |
| 缓存 | 无 | 本地/远程缓存 | 本地/远程缓存 |
| 依赖图 | 手动 | 自动 | 自动 |
| 增量构建 | 无 | 有 | 有 |
| 学习成本 | 低 | 中 | 高 |
4. 微前端架构
微前端将大型应用拆分为多个独立的子应用,各子应用可独立开发、测试、部署。
主流方案:
| 方案 | 原理 | 代表 |
|---|---|---|
| JS 沙箱 | 运行时加载子应用 JS,沙箱隔离 | qiankun |
| Web Components | Shadow DOM 隔离子应用 | single-spa |
| Module Federation | 构建时共享模块 | Webpack 5 MF |
| iframe | 物理隔离 | iframe |
qiankun 实战:
// 主应用
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'admin',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/admin',
},
{
name: 'crm',
entry: '//localhost:8082',
container: '#subapp-container',
activeRule: '/crm',
},
])
start()
// 子应用 main.js
export async function bootstrap() {
console.log('子应用初始化')
}
export async function mount(props) {
render(props.container)
}
export async function unmount() {
// 清理副作用
app.unmount()
}
function render(container) {
const app = createApp(App)
app.mount(container ? container.querySelector('#app') : '#app')
}
Module Federation 实战:
// webpack.config.js — 远程应用(提供组件)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./UserCard': './src/components/UserCard',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
}
// webpack.config.js — 消费应用
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:8081/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})
// 消费远程组件
const RemoteButton = React.lazy(() => import('remoteApp/Button'))
function App() {
return (
<Suspense fallback="Loading...">
<RemoteButton>Remote Button</RemoteButton>
</Suspense>
)
}
微前端方案对比:
| 维度 | qiankun | Module Federation | single-spa |
|---|---|---|---|
| 隔离方式 | JS 沙箱 + CSS 隔离 | 模块共享 | 框架无关路由 |
| 技术栈 | 统一推荐 | 可跨框架 | 可跨框架 |
| 性能 | 中(运行时加载) | 好(构建时共享) | 中 |
| 入侵性 | 子应用需改造 | 需 Webpack 5 | 子应用需改造 |
| 共享依赖 | 手动配置 | 自动 | 手动配置 |
5. Islands 架构
Islands 架构适用于内容型网站(博客、文档、营销页),大部分页面是静态 HTML,只有交互部分(岛屿)hydrate 为 JS。
┌─────────────────────────────────┐
│ 静态 HTML(零 JS) │
│ ┌──────┐ ┌──────┐ │
│ │ 岛屿1 │ │ 岛屿2 │ │
│ │ 轮播图 │ │ 搜索框 │ │
│ └──────┘ └──────┘ │
│ │
│ ┌──────────┐ │
│ │ 岛屿3 │ │
│ │ 评论区 │ │
│ └──────────┘ │
└─────────────────────────────────┘
Astro 实战:
---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro'
import Carousel from '../components/Carousel'
import SearchBox from '../components/SearchBox'
import Comments from '../components/Comments'
---
<Layout title="Home">
<!-- 静态内容:零 JS -->
<h1>Welcome</h1>
<p>静态内容不需要 JavaScript</p>
<!-- 交互岛屿:只有这些组件会加载 JS -->
<Carousel client:visible />
<SearchBox client:idle />
<Comments client:visible />
</Layout>
| 指令 | 含义 |
|---|---|
client:load | 页面加载时立即 hydrate |
client:idle | 浏览器空闲时 hydrate |
client:visible | 元素进入视口时 hydrate |
client:media | 匹配媒体查询时 hydrate |
client:only | 跳过 SSR,仅客户端渲染 |
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 分层边界模糊 | 业务逻辑混入组件 | 严格规则:组件只调 composable,不直接调 service |
| Monorepo 构建慢 | 所有包全量构建 | Turborepo 缓存 + 增量构建 |
| 微前端样式冲突 | 全局 CSS 污染 | CSS Modules / Shadow DOM / qiankun 的 CSS 沙箱 |
| 微前端通信复杂 | 子应用间直接依赖 | 通过主应用的全局状态 / CustomEvent 通信 |
| Islands 交互割裂 | 岛屿间无法共享状态 | 将共享状态提升到 layout 层 |
最佳实践
- 从小开始:不要过度设计,单体 + 分层足够应对 90% 的项目。
- 依赖方向单一:上层依赖下层,绝不反向。
- Monorepo 用 pnpm:硬链接节省磁盘,workspace 协议确保包版本一致。
- 微前端慎用:只有团队规模和独立部署需求真正存在时才用。
- 内容站用 Islands:Astro 的 Islands 架构可以让页面 JS 减少 90%。
面试题
1. 分层架构中为什么不能让数据层依赖表现层?
答:因为依赖方向必须单向,避免循环依赖和职责混乱。表现层(组件)是消费方,数据层(API 服务)是提供方。如果数据层依赖表现层,意味着 API 调用代码中会导入组件——这在逻辑上不合理(API 不应该关心 UI 怎么展示),在工程上导致循环依赖(组件导入 API,API 导入组件),在复用上受限(数据层无法被其他非 UI 代码复用,如 CLI 工具、定时任务)。
2. Feature-Sliced Design 和传统分层架构的核心区别是什么?
答:传统分层按技术层水平切分(所有组件放 components/,所有 API 放 services/),FSD 按业务功能垂直切分(认证功能的所有代码放 features/auth/)。区别:(1) 修改范围——改一个功能,传统分层需改 components/stores/services 三个目录,FSD 只改一个 feature 目录;(2) 删除便利——删一个功能,FSD 直接删文件夹,传统分层要搜索所有目录找出相关文件;(3) 复用粒度——传统分层复用组件级,FSD 复用功能级(整个 feature 可被多个页面导入)。代价是 FSD 学习曲线更陡。
3. Monorepo 和 Multirepo 各自的优缺点是什么?
答:Monorepo 优点:(1) 代码共享方便——包之间直接引用,无需发布 npm;(2) 原子提交——一个 PR 可以同时改多个包;(3) 统一工具链——ESLint、TypeScript、CI 配置统一管理。Monorepo 缺点:(1) 构建变慢——需要增量构建和缓存;(2) 权限控制难——所有代码在同一仓库;(3) 仓库体积大。Multirepo 优点:(1) 团队独立——每个仓库独立权限和 CI;(2) 构建快——每个仓库体量小。Multirepo 缺点:(1) 共享困难——需要发布 npm 包;(2) 版本协调——包 A 依赖包 B 的新版本,需等 B 发布;(3) 跨仓库修改——一个功能涉及多个仓库要开多个 PR。
4. qiankun 和 Module Federation 的核心区别是什么?
答:qiankun 是运行时方案,Module Federation 是构建时方案。qiankun 在运行时通过 import-html-entry 加载子应用的 HTML/JS/CSS,用 Proxy 沙箱隔离全局变量,适合不同技术栈的子应用。Module Federation 在构建时通过 Webpack 插件将模块暴露为远程入口,消费方构建时就确定了远程依赖,共享 React 等基础库避免重复加载,适合同技术栈的模块共享。qiankun 隔离性好但性能一般,MF 性能好但需要 Webpack 5 且共享依赖版本需兼容。
5. Islands 架构解决了什么问题?与传统 SSR 有什么区别?
答:Islands 架构解决了 SSR 应用的 JS 体积问题。传统 SSR 虽然首屏 HTML 快,但 hydration 时需要加载整个页面的 JS,即使大部分内容是纯静态的(标题、段落、图片)。Islands 架构让静态内容保持为纯 HTML(零 JS),只有交互组件(搜索框、轮播图、评论区)hydrate 为 JS。结果是:一个博客页面的 JS 从 200KB 降到 20KB,TTI(Time to Interactive)从 3s 降到 0.5s。代表框架是 Astro。
6. 如何确定一个项目应该用什么架构?
答:按团队规模和项目复杂度选择:(1) 1-3 人、1 个产品——单体 + 简单分层,不要过度设计;(2) 5-10 人、1 个产品——Monorepo + FSD 分层,共享组件和工具库;(3) 10-20 人、多个产品——Monorepo + 多应用,共享 UI 库和业务库;(4) 20+ 人、多个团队——微前端,各团队独立开发和部署。关键原则:架构服务于团队协作效率,不是为了技术先进。一个 3 人团队上微前端是过度设计,一个 50 人团队用单体是灾难。
7. Turborepo 的缓存机制是怎么工作的?
答:Turborepo 基于输入哈希做缓存:(1) 计算每个 task 的输入哈希(源文件内容 + 依赖的 task 输出 + 环境变量 + 配置文件);(2) 如果本地缓存中有匹配哈希的输出,直接复制缓存结果,跳过执行;(3) 如果配置了远程缓存(Turbo Remote Cache),团队成员的构建结果可以共享——A 构建过 @my/ui,B 拉取缓存后无需重新构建。这使得 CI 中的大部分 task 可以秒级完成。关键:只有输入不变才命中缓存,修改一行代码只会导致依赖链上的 task 重新执行。
8. 微前端中子应用之间如何通信?
答:三种通信方式:(1) 主应用中转——子应用通过 props 从主应用获取数据,通过回调函数向主应用发送消息,主应用作为中心枢纽转发;(2) CustomEvent——子应用通过 window.dispatchEvent(new CustomEvent('event-name', { detail })) 发送事件,其他子应用通过 window.addEventListener 监听,完全解耦但无类型安全;(3) 共享状态——主应用创建全局状态(如 Observable / EventEmitter / MicroState),子应用通过 API 读写。最佳实践:简单场景用 props + 回调,复杂场景用共享状态,跨技术栈用 CustomEvent。避免子应用之间直接依赖。