阶段 4:前端架构设计 - 真正的分水岭
这部分是架构师的核心竞争力。前面三阶段是"能干活",这部分是"能带团队、能设计系统"。
一、系统拆分能力
1.1 拆分的三个维度
| 维度 | 原则 | 示例 |
|---|---|---|
| 按业务拆 | 高内聚低耦合,独立业务上下文 | 电商:用户域、商品域、订单域、支付域 |
| 按模块拆 | 功能边界清晰,可独立开发测试 | 后台管理系统:仪表盘、用户管理、权限管理、日志 |
| 按团队拆 | 康威定律:系统结构 = 组织沟通结构 | 每个团队负责 1-2 个业务域,边界由 API 定义 |
1.2 前端分层架构
┌─────────────────────────────────────────────┐
│ 视图层 │ UI 组件(无业务逻辑)
├─────────────────────────────────────────────┤
│ 容器/页面层 │ 组合组件、连接数据
├─────────────────────────────────────────────┤
│ 业务逻辑层 │ Hooks / Composables / Services
├─────────────────────────────────────────────┤
│ 状态管理层 │ Store(全局/局部)
├─────────────────────────────────────────────┤
│ 数据访问层 │ API 请求、缓存、存储
└─────────────────────────────────────────────┘
实际代码组织示例:
src/
├── pages/ # 页面层
│ └── order/
│ ├── OrderList.tsx # 页面组件
│ └── components/ # 页面私有组件
├── features/ # 业务功能(按业务域)
│ ├── order/
│ │ ├── hooks/ # 业务逻辑
│ │ ├── services/ # 调用接口
│ │ ├── store/ # 状态
│ │ └── types/ # 类型定义
│ └── user/
├── shared/ # 共享层
│ ├── ui/ # 通用 UI 组件
│ ├── hooks/ # 通用 Hooks
│ ├── utils/ # 工具函数
│ └── api/ # HTTP 客户端封装
└── app/ # 应用配置
├── router/
├── store/
└── styles/
二、微前端
2.1 核心概念对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| qiankun | HTML Entry + JS 沙箱(Proxy) | 框架无关、CSS 隔离、JS 沙箱 | 较大、MPA 模式有局限 |
| Module Federation | Webpack 5 插件,运行时加载远程模块 | 原生、轻量、共享依赖 | 仅 Webpack |
| single-spa | 底层库,需要自己实现加载/卸载 | 灵活、轻量 | 需要更多配置 |
2.2 Module Federation 核心原理
概念理解:
多个独立构建的应用,在运行时共享模块。一个应用可以"暴露"自己的模块,另一个应用可以"消费"它。
配置示例:
// 主应用(host)webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp', // 应用名
remotes: { // 消费远程模块
orderApp: 'orderApp@http://localhost:3001/remoteEntry.js',
userApp: 'userApp@http://localhost:3002/remoteEntry.js'
},
shared: { // 共享依赖,避免重复加载
react: { singleton: true, eager: true },
'react-dom': { singleton: true },
'react-router-dom': { singleton: true }
}
})
]
}
// 子应用(remote)webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'orderApp',
filename: 'remoteEntry.js', // 入口文件
exposes: { // 暴露给其他应用使用的模块
'./OrderList': './src/OrderList',
'./OrderDetail': './src/OrderDetail',
'./store': './src/store'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
}
运行时使用:
// 主应用中动态加载子应用组件
const OrderList = React.lazy(() => import('orderApp/OrderList'))
function App() {
return (
<div>
<h1>主应用</h1>
<React.Suspense fallback="Loading Order...">
<OrderList />
</React.Suspense>
</div>
)
}
版本冲突处理策略:
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
eager: false,
version: '18.2.0'
}
}
// 如果子应用要求 react 17,会自动降级/拒绝加载
2.3 qiankun 核心实现思路
// 1. 主应用注册子应用
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'order-app',
entry: '//localhost:3001',
container: '#subapp-container',
activeRule: '/order',
props: { token: 'xxx' }
}
])
start({
sandbox: { // JS 沙箱
strictStyleIsolation: true, // CSS 隔离(Shadow DOM)
experimentalStyleIsolation: true // 运行时样式转换
}
})
// 2. 子应用需要暴露生命周期
export async function bootstrap() {
console.log('子应用初始化')
}
export async function mount(props) {
render(props.container)
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(container)
}
三、设计模式在前端的应用
3.1 发布订阅 vs 观察者模式
| 模式 | 关系 | 耦合度 | 前端应用 |
|---|---|---|---|
| 观察者 | 观察者直接注册到目标 | 松耦合(双方都知道对方) | Vue 响应式、MutationObserver |
| 发布订阅 | 通过事件中心转发 | 完全解耦 | Event Bus、Redux、消息队列 |
// 观察者模式(直接通信)
class Subject {
observers = []
attach(observer) { this.observers.push(observer) }
notify(data) { this.observers.forEach(obs => obs.update(data)) }
}
class Observer {
update(data) { console.log('收到:', data) }
}
// 发布订阅(通过事件中心)
class EventBus {
events = {}
on(name, fn) { this.events[name] = this.events[name] || []; this.events[name].push(fn) }
emit(name, data) { (this.events[name] || []).forEach(fn => fn(data)) }
}
3.2 工厂模式 — 组件动态渲染
// 表单组件工厂
interface FormComponent {
render(): JSX.Element
getValue(): any
validate(): boolean
}
class InputComponent implements FormComponent {
constructor(private schema: any) {}
render() { return <input type={this.schema.type} /> }
getValue() { /* ... */ }
validate() { /* ... */ }
}
class SelectComponent implements FormComponent {
constructor(private schema: any) {}
render() { return <select>{this.schema.options.map(...)}</select> }
getValue() { /* ... */ }
validate() { /* ... */ }
}
class FormComponentFactory {
static create(schema: any): FormComponent {
const type = schema.type
if (type === 'input') return new InputComponent(schema)
if (type === 'select') return new SelectComponent(schema)
if (type === 'datepicker') return new DatePickerComponent(schema)
throw new Error(`Unknown component type: ${type}`)
}
}
// 使用
const schema = { type: 'input', placeholder: '请输入用户名' }
const component = FormComponentFactory.create(schema)
3.3 单例模式 — 全局状态/请求
// 全局请求去重单例
class RequestDeduplicator {
private static instance: RequestDeduplicator
private pending = new Map<string, Promise<any>>()
static getInstance() {
if (!RequestDeduplicator.instance) {
RequestDeduplicator.instance = new RequestDeduplicator()
}
return RequestDeduplicator.instance
}
async request<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
if (this.pending.has(key)) {
return this.pending.get(key) as Promise<T>
}
const promise = fetcher().finally(() => {
this.pending.delete(key)
})
this.pending.set(key, promise)
return promise
}
}
// 使用
const deduper = RequestDeduplicator.getInstance()
const data = await deduper.request('/api/user', () => fetch('/api/user'))
四、插件化架构(高级)
4.1 核心设计
插件系统三要素:
- 注册机制 - 如何发现和注册插件
- 生命周期 - 插件在不同阶段的执行点
- 扩展点 - 插件能"插入"的钩子位置
// 插件接口定义
interface Plugin {
name: string
version: string
// 生命周期钩子
onRegister?(context: PluginContext): void
onLoad?(context: PluginContext): void
onUnload?(): void
// 扩展点实现
hooks?: {
[hookName: string]: (...args: any[]) => any
}
// 配置
config?: Record<string, any>
}
interface PluginContext {
api: {
registerHook(name: string, handler: Function): void
emitHook(name: string, ...args: any[]): Promise<any[]>
getConfig(key: string): any
logger: Console
}
}
4.2 完整实现
// 插件管理器
class PluginManager {
private plugins = new Map<string, Plugin>()
private hooks = new Map<string, Function[]>()
private context: PluginContext
constructor() {
this.context = this.createContext()
}
private createContext(): PluginContext {
return {
api: {
registerHook: (name: string, handler: Function) => {
if (!this.hooks.has(name)) {
this.hooks.set(name, [])
}
this.hooks.get(name)!.push(handler)
},
emitHook: async (name: string, ...args: any[]) => {
const handlers = this.hooks.get(name) || []
const results = []
for (const handler of handlers) {
results.push(await handler(...args))
}
return results
},
getConfig: (key: string) => {
// 从全局配置获取
return (window as any).__PLUGIN_CONFIG__?.[key]
},
logger: console
}
}
}
// 注册插件
register(plugin: Plugin) {
if (this.plugins.has(plugin.name)) {
console.warn(`插件 ${plugin.name} 已存在,将被覆盖`)
}
// 调用 onRegister
plugin.onRegister?.(this.context)
// 注册扩展点
if (plugin.hooks) {
Object.entries(plugin.hooks).forEach(([name, handler]) => {
this.context.api.registerHook(name, handler)
})
}
this.plugins.set(plugin.name, plugin)
console.log(`插件 ${plugin.name} v${plugin.version} 注册成功`)
}
// 加载所有插件
async load() {
const loadPromises = Array.from(this.plugins.values()).map(async (plugin) => {
try {
await plugin.onLoad?.(this.context)
console.log(`插件 ${plugin.name} 加载完成`)
} catch (error) {
console.error(`插件 ${plugin.name} 加载失败:`, error)
this.unregister(plugin.name)
}
})
await Promise.all(loadPromises)
}
// 卸载插件
unregister(name: string) {
const plugin = this.plugins.get(name)
if (!plugin) return
plugin.onUnload?.()
this.plugins.delete(name)
console.log(`插件 ${name} 已卸载`)
}
// 获取所有插件
getPlugins() {
return Array.from(this.plugins.values())
}
}
4.3 实际应用示例
// 1. 定义编辑器插件
const MarkdownPlugin: Plugin = {
name: '@editor/markdown',
version: '1.0.0',
onRegister(ctx) {
console.log('Markdown 插件正在注册')
},
onLoad(ctx) {
// 注册工具栏按钮
ctx.api.registerHook('toolbar:buttons', () => ({
id: 'markdown',
icon: '📝',
onClick: () => console.log('切换 Markdown 模式')
}))
// 注册解析器
ctx.api.registerHook('parser:transform', (content: string) => {
// 将 Markdown 转为 HTML
return content.replace(/^# (.*)$/gm, '<h1>$1</h1>')
})
// 注册快捷键
ctx.api.registerHook('shortcuts', () => ({
key: 'ctrl+m',
handler: () => console.log('Markdown 快捷操作')
}))
}
}
// 2. 动态加载远程插件(Module Federation 结合)
async function loadRemotePlugin(remoteUrl: string) {
const { default: plugin } = await import(/* @vite-ignore */ remoteUrl)
pluginManager.register(plugin)
}
// 3. 使用插件扩展的核心功能
class Editor {
private pluginManager = new PluginManager()
async init(plugins: Plugin[]) {
// 注册内置插件
plugins.forEach(p => this.pluginManager.register(p))
await this.pluginManager.load()
}
renderToolbar() {
// 调用插件注册的钩子
const buttons = this.pluginManager.context.api.emitHook('toolbar:buttons')
return buttons.map(btn => /* render button */)
}
transformContent(raw: string) {
return this.pluginManager.context.api.emitHook('parser:transform', raw)
}
}
4.4 插件化架构的关键设计决策
| 决策点 | 推荐方案 | 原因 |
|---|---|---|
| 插件发现 | 配置文件声明 + 动态 import | 按需加载、减少体积 |
| 依赖管理 | 显式声明 peerDependencies + 版本检查 | 避免运行时冲突 |
| 作用域隔离 | 每个插件独立 iframe 或 Shadow DOM | 样式/脚本不污染 |
| 通信方式 | 通过核心 API 代理,不直接通信 | 可控、可追踪 |
| 版本升级 | 语义化版本 + 兼容层(Adapter) | 平滑迁移 |
五、架构设计面试总结
场景题:设计一个"可插拔的图表库"
回答框架:
- 核心层:渲染引擎(ECharts/Highcharts 封装)
- 扩展点:主题、交互、数据处理、导出
- 插件机制:registerPlugin + 生命周期(beforeRender、afterRender)
- 按需加载:动态导入插件代码
- 配置化:声明式启用插件
常见追问
| 追问 | 回答要点 |
|---|---|
| "插件间有依赖怎么办?" | 在 register 时声明,管理器按拓扑顺序加载 |
| "如何保证插件不拖慢主应用?" | Web Worker 执行、空闲时加载、性能监控上报 |
| "插件热更新怎么做?" | 监听文件变化 → 重新注册 → 卸载旧实例 → 通知使用方刷新 |
| "微前端和插件化区别?" | 微前端解决"整页整合",插件化解决"功能扩展" |