CC 咖啡猫的工作空间 Coding Space

前端架构与设计模式

架构是「变化」的管理,设计模式是「常见问题」的复用解法。前端领域的架构演进本质是对「状态 → UI」映射关系的不断优化。


1. 架构模式

1.1 MVC vs MVP vs MVVM

MVC(Model-View-Controller)

┌─────┐    通知变化    ┌──────┐    用户操作    ┌──────────┐
│View │ ◄──────────── │Model │ ◄──────────── │Controller│
│     │              │      │               │          │
│     │ ────→触发───→ │      │               │  ┌──────┐ │
└─────┘   Controller  └──────┘               │  │ 逻辑  │ │
                                              └──────────┘
  View 直接从 Model 获取数据(View ↔ Model 双向)
  Controller 处理用户输入、更新 Model
  View 监听 Model 变化自动更新

前端实现(Backbone.js)

// Model
const Todo = Backbone.Model.extend({
  defaults: { title: '', completed: false },
});

// View
const TodoView = Backbone.View.extend({
  initialize() {
    this.model.on('change', this.render, this);  // View 直接监听 Model
  },
  template: _.template('<%= title %>'),
  render() { this.$el.html(this.template(this.model.toJSON())); },
  events: { 'click .toggle': 'toggle' },
  toggle() { this.model.set('completed', !this.model.get('completed')); },
});

问题

  • View 和 Model 直接耦合,难以单元测试
  • Controller 逻辑容易膨胀(Massive ViewController)
  • View 需要手动管理 Model 的事件订阅

MVP(Model-View-Presenter)

┌──────┐   通信接口    ┌───────────┐    更新      ┌───────┐
│ View │ ◄─────────── │Presenter  │ ←────────── │ Model │
│(被动)│  (interface) │           │             │       │
│      │ ────→事件───→ │ 所有逻辑   │ ───→读取───→ │       │
└──────┘              └───────────┘             └───────┘
  View 完全被动,不直接访问 Model
  Presenter 持有 View 接口和 Model 引用
  双向通过接口通信(测试友好)
// 接口定义
class TodoViewInterface {
  render(todos) {}
  showError(msg) {}
}

// Presenter 纯逻辑,可测试
class TodoPresenter {
  constructor(view, model) {
    this.view = view;
    this.model = model;
  }

  async onSearch(query) {
    try {
      const todos = await this.model.search(query);
      this.view.render(todos);
    } catch (e) {
      this.view.showError(e.message);
    }
  }
}

// 测试
const mockView = new MockTodoView();
const presenter = new TodoPresenter(mockView, mockModel);

前端适用场景:逻辑复杂的页面(表格/表单编排),测试是核心需求时。

MVVM(Model-View-ViewModel)

┌──────┐   Data Binding  ┌───────────┐   请求/通知   ┌───────┐
│ View │ ◄───────────── │ ViewModel │ ←────────── │ Model │
│ (DOM)│  双向绑定/声明式  │  (状态)   │             │       │
│      │ ────→用户操作──→ │           │ ────→变更───→│       │
└──────┘                 └───────────┘             └───────┘
  View = 模板 + 指令(HTML)
  ViewModel = 响应式状态 + 计算属性 + 方法
  Data Binding 层自动同步 View ↔ ViewModel

前端实现(Vue/React)

// Vue(MVVM 典型实现)
const vm = new Vue({
  el: '#app',
  data: {        // ViewModel
    todos: [],
    loading: false,
  },
  computed: {    // 计算属性
    activeTodos() { return this.todos.filter(t => !t.done); },
  },
  methods: {
    async fetchTodos() {
      this.loading = true;
      this.todos = await api.getTodos();  // 改变数据 → 自动驱动 View 更新
      this.loading = false;
    },
  },
});

三者的核心区别

模式 View 状态 View 是否引用 Model 测试策略 前端典型
MVC 被动(从 Model 读取) 测试 Model + Controller Backbone
MVP 被动(由 Presenter 填充) 否(通过接口) 测试 Presenter(Mock View) GWT
MVVM 自动同步(Data Binding) 测试 ViewModel 纯函数 Vue/React

1.2 Flux / Redux 单向数据流

Flux 动机:MVC 的双向数据流在大型应用中不可预测(Model 更新 View → View 触发 Controller → Controller 更新另一个 Model → 连锁反应)。

 Flux 架构:
 ┌──────┐  Action  ┌──────────┐   ┌──────────┐   ┌──────┐
 │ View │ ───────→ │ Dispatcher│ → │  Store   │ → │ View │
 │      │ ←────────│ (单例)    │   │ (状态树)  │   │      │
 └──────┘   State   └──────────┘   └──────────┘   └──────┘

Redux 三原则

  1. 单一数据源:整个应用的状态存储在单⼀ Store 中
  2. State 是只读的:唯一改变状态的方式是触发 Action
  3. 纯函数修改:Reducer 是纯函数,(prevState, action) => newState
// Redux 完整流程
// 1. Action
const ADD_TODO = 'ADD_TODO';
const addTodo = (text) => ({ type: ADD_TODO, payload: { text } });

// 2. Reducer(纯函数)
const todosReducer = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [...state, { id: Date.now(), text: action.payload.text, done: false }];
    default:
      return state;
  }
};

// 3. Store
const store = createStore(todosReducer);

// 4. Dispatch
store.dispatch(addTodo('Learn Redux'));

// 5. Subscribe
store.subscribe(() => console.log(store.getState()));

单向数据流的价值

  • 可预测:给定相同的 Action 序列,永远得到相同的最终状态
  • 可追溯:DevTools 实现时间旅行(Actions 回放/撤销)
  • 可测试:Reducer 是纯函数,不需要 Mock
  • 并行开发:组件只关心 dispatch Action,Store 结构统一

1.3 洋葱架构(Clean Architecture)

                 ┌──────────────┐
                 │    UI Layer   │  ← 框架层(React/Vue 组件)
                 └──────┬───────┘
                        │ 调用
                 ┌──────▼───────┐
                 │ Application   │  ← 用例层(Use Case)
                 │  Services     │
                 └──────┬───────┘
                        │ 调用
                 ┌──────▼───────┐
                 │  Domain       │  ← 核心业务逻辑(与框架无关)
                 │  Entities     │
                 └──────┬───────┘
                        │ 接口
                 ┌──────▼───────┐
                 │ Infrastructure│  ← 外部适配(DB/API/文件)
                 └──────────────┘

依赖规则:源码依赖只能从外向内,内部层不能知道外部层的存在。

// Domain 层(纯业务逻辑,无框架引用)
class OrderEntity {
  constructor(items) {
    this.items = items;
    this.status = 'pending';
  }

  canCancel() {
    return this.status === 'pending' && !this.isExpired();
  }

  isExpired() {
    return Date.now() - this.createdAt > 30 * 60 * 1000; // 30min
  }
}

// Application 层
class CancelOrderUseCase {
  constructor(orderRepo, paymentGateway) {
    this.orderRepo = orderRepo;
    this.paymentGateway = paymentGateway;
  }

  async execute(orderId) {
    const order = await this.orderRepo.findById(orderId);
    if (!order.canCancel()) throw new Error('Cannot cancel order');
    await this.paymentGateway.refund(order);
    await this.orderRepo.save(order);
  }
}

// Infrastructure 层(实现接口)
class PostgresOrderRepository {
  async findById(id) {
    const row = await db.query('SELECT * FROM orders WHERE id = $1', [id]);
    return new OrderEntity(row.items);  // 适配 Domain
  }
}

1.4 六边形架构(端口适配器)

                   ┌──────────┐
     HTTP API ───→ │   Port   │ ←─── Frontend
    WebSocket ───→ │ (Inbound)│ ←─── CLI
                   └────┬─────┘
                        │
             ┌──────────▼──────────┐
             │  Application Core   │
             │  (Domain Logic)     │
             └──────────┬──────────┘
                        │
     ┌──────────────────▼──────────────────┐
     │              Port                   │
     │            (Outbound)               │
     └────┬──────────┬──────────┬──────────┘
          │          │          │
    ┌─────▼──┐ ┌────▼───┐ ┌───▼─────┐
    │ MySQL  │ │  Redis  │ │  S3     │
    │ Adapter │ │ Adapter │ │ Adapter │
    └────────┘ └────────┘ └─────────┘

关键设计

  • 核心业务逻辑只依赖端口接口(抽象),不依赖任何具体适配器
  • 端口适配器可以随时替换(MySQL → PostgreSQL,S3 → MinIO)
  • 非常适合微服务和 BFF 层

2. 常见设计模式

2.1 创建型

单例(Singleton)

// ES Module 天然单例(模块只执行一次)
// config.js
export const config = {
  apiUrl: 'https://api.example.com',
};

// 任何地方 import 得到的都是同一份引用
import { config } from './config.js';
// 手动实现(类式)
class Cache {
  static #instance;
  #store = new Map();
  
  static getInstance() {
    if (!Cache.#instance) {
      Cache.#instance = new Cache();
    }
    return Cache.#instance;
  }
  
  get(key) { return this.#store.get(key); }
  set(key, value) { this.#store.set(key, value); }
}

前端场景:全局状态 Store、缓存、日志、请求实例(axios)。

工厂模式

// 简单工厂
class ComponentFactory {
  static create(type, props) {
    switch (type) {
      case 'button': return new Button(props);
      case 'input': return new Input(props);
      case 'select': return new Select(props);
      default: throw new Error(`Unknown type: ${type}`);
    }
  }
}
// React 中的工厂模式应用
function createFieldRenderer(type) {
  const fieldMap = {
    text: TextField,
    number: NumberField,
    date: DatePicker,
    select: SelectField,
  };
  return fieldMap[type] || FallbackField;
}

建造者模式

// 链式建造器
class QueryBuilder {
  constructor() { this._query = {}; }
  
  select(...fields) {
    this._query.select = fields;
    return this;
  }
  
  from(table) {
    this._query.from = table;
    return this;
  }
  
  where(condition) {
    this._query.where = condition;
    return this;
  }
  
  build() {
    return this._query;  // 或生成 SQL 字符串
  }
}

const query = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where({ age: { $gt: 18 } })
  .build();

2.2 结构型

适配器(Adapter)

// 旧接口
class LegacyAPI {
  getUserXML(id) { return `<user><id>${id}</id></user>`; }
}

// 新接口期望
interface IUserAPI {
  getUserJSON(id): { id: number };
}

// 适配器
class UserAPIAdapter {
  constructor() { this.legacy = new LegacyAPI(); }
  
  getUserJSON(id) {
    const xml = this.legacy.getUserXML(id);
    // 解析 XML → JSON
    return this.parseXML(xml);
  }
}

前端场景:统一不同后端 API 响应格式、微信/支付宝支付接口适配。

装饰器(Decorator)

// ES Stage 3 Decorator(TypeScript)
function log(target, propertyKey, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`[${propertyKey}] called with`, args);
    const result = original.apply(this, args);
    console.log(`[${propertyKey}] returned`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) { return a + b; }
}
// React HOC(高阶组件,装饰器模式的变体)
function withAuth(Component) {
  return function AuthenticatedComponent(props) {
    const user = useAuth();
    if (!user) return <LoginRedirect />;
    return <Component {...props} user={user} />;
  };
}

const AdminPanel = withAuth(AdminDashboard);

代理(Proxy)

// ES Proxy(拦截对象操作)
const handler = {
  get(target, prop) {
    if (prop in target) {
      console.log(`Accessing ${prop}`);
      return target[prop];
    }
    console.warn(`Property ${prop} not found`);
    return undefined;
  },
  set(target, prop, value) {
    if (prop === 'age' && typeof value !== 'number') {
      throw new TypeError('age must be a number');
    }
    target[prop] = value;
    return true;  // 表示设置成功
  },
};

const user = new Proxy({ name: 'Alice' }, handler);

Vue3 响应式核心

// Vue3 reactive 的实现简化
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);        // 依赖收集
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, key);     // 触发更新
      }
      return result;
    },
  });
}

组合模式(Composite)

<!-- React 中的组合模式 -->
<Menu>
  <MenuItem>
    <SubMenu title="File">
      <MenuItem>New</MenuItem>
      <MenuItem>Save</MenuItem>
    </SubMenu>
  </MenuItem>
</Menu>
// 组合模式:树形结构统一处理
class TreeNode {
  constructor(name) { this.name = name; this.children = []; }
  add(child) { this.children.push(child); }
  render() {
    return `<li>${this.name}<ul>${this.children.map(c => c.render()).join('')}</ul></li>`;
  }
}

2.3 行为型

观察者模式(发布订阅)

// EventEmitter 实现
class EventEmitter {
  #events = new Map();
  
  on(event, listener) {
    if (!this.#events.has(event)) this.#events.set(event, []);
    this.#events.get(event).push(listener);
    return () => this.off(event, listener);
  }
  
  off(event, listener) {
    const listeners = this.#events.get(event);
    if (listeners) {
      this.#events.set(event, listeners.filter(l => l !== listener));
    }
  }
  
  emit(event, ...args) {
    const listeners = this.#events.get(event);
    listeners?.forEach(l => l(...args));
  }
  
  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

前端应用

  • window.addEventListener / element.addEventListener
  • Vue $emit / $on
  • React 自定义事件的跨组件通信

策略模式

// 策略模式:用函数/对象替代 if-else 链
const validators = {
  required: (value) => value != null && value !== '' ? null : '必填项',
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : '邮箱格式错误',
  minLength: (min) => (value) =>
    value.length >= min ? null : `最少 ${min} 个字符`,
  phone: (value) => /^1[3-9]\d{9}$/.test(value) ? null : '手机号格式错误',
};

// 使用
const rules = [validators.required, validators.email];
const errors = rules.map(rule => rule(input)).filter(Boolean);

策略模式 vs if-else

  • 策略模式将算法封装为独立单元,可单独测试和复用
  • 新策略无需修改原有代码(开闭原则)
  • if-else 修改需改动函数体,违反开闭原则

状态模式

// 有限状态机
class Order {
  constructor() { this.state = new PendingState(this); }
  
  next() { this.state.next(); }
  cancel() { this.state.cancel(); }
  getStatus() { return this.state.status; }
}

class PendingState {
  constructor(order) { this.order = order; this.status = 'pending'; }
  
  next() { this.order.state = new PaidState(this.order); }
  cancel() { this.order.state = new CancelledState(this.order); }
}

class PaidState {
  constructor(order) { this.order = order; this.status = 'paid'; }
  
  next() { this.order.state = new ShippedState(this.order); }
  cancel() { throw new Error('已付款订单不能取消'); }
}

class CancelledState {
  constructor(order) { this.order = order; this.status = 'cancelled'; }
  next() { throw new Error('已取消订单不能继续'); }
  cancel() { throw new Error('已取消'); }
}

// 使用
const order = new Order();
order.next();          // pending → paid
order.next();          // paid → shipped

XState 状态图:工业级状态机库,支持嵌套状态、并行状态、守卫条件。


3. 前端特有模式

3.1 HOC / Render Props / 自定义 Hook

三种逻辑复用模式的演进

// 1. HOC(高阶组件)- 类组件时代
function withLogging(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log(`Mounted ${WrappedComponent.name}`);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

// HOC 问题:层层嵌套 → 难以追踪(Wrapper Hell)
const EnhancedComponent = withAuth(withLogging(withTheme(MyComponent)));
// 2. Render Props(渲染属性)
class MouseTracker extends React.Component {
  state = { x: 0, y: 0 };
  handleMouseMove = (e) => this.setState({ x: e.clientX, y: e.clientY });
  render() {
    return <div onMouseMove={this.handleMouseMove}>
      {this.props.render(this.state)}  {/* 通过 render prop 传递状态 */}
    </div>;
  }
}

// 使用
<MouseTracker render={({ x, y }) => <h1>Mouse: {x}, {y}</h1>} />
// 3. 自定义 Hook(函数组件时代)- 最佳方案
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);
  
  return pos;
}

// 使用(扁平,无嵌套)
function MyComponent() {
  const { x, y } = useMousePosition();
  return <h1>Mouse: {x}, {y}</h1>;
}

HOC 层层嵌套问题的本质:HOC 在 React 中返回新的类组件,嵌套时创建多层组件树,调试困难、props 隐式传递(withAuth(MyComponent) 传入的 user props 可能和其他 HOC 冲突)。Hook 通过扁平函数调用解决了这个问题。

3.2 容器组件 vs 展示组件

 Container(聪明组件)                 Presentational(傻瓜组件)
 ┌────────────────────────┐          ┌──────────────────────────┐
 │ - 管理状态和逻辑        │          │ - 只负责渲染             │
 │ - 调用 API/Store       │  props   │ - 通过 props 接收数据和回调 │
 │ - 提供数据和方法给子组件 │ ──────→ │ - 本身无状态或只有 UI 状态 │
 │ - 可复用性低           │          │ - 可复用性高(纯 UI)     │
 └────────────────────────┘          └──────────────────────────┘
// Container
function TodoContainer() {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchTodos().then(data => { setTodos(data); setLoading(false); });
  }, []);

  const toggle = async (id) => {
    await toggleTodo(id);
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  };

  return <TodoList todos={todos} loading={loading} onToggle={toggle} />;
}

// Presentational(纯 UI,可脱离业务场景复用)
function TodoList({ todos, loading, onToggle }) {
  if (loading) return <Skeleton />;
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <Checkbox checked={todo.done} onChange={() => onToggle(todo.id)} />
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
        </li>
      ))}
    </ul>
  );
}

3.3 Compound Components(复合组件)

// React 复合组件
import { createContext, useContext } from 'react';

const ToggleContext = createContext();

function Toggle({ children, defaultState = false }) {
  const [on, setOn] = useState(defaultState);
  const toggle = () => setOn(prev => !prev);

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

Toggle.On = function On({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? children : null;
};

Toggle.Off = function Off({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? null : children;
};

Toggle.Button = function Button() {
  const { on, toggle } = useContext(ToggleContext);
  return <button onClick={toggle}>{on ? 'ON' : 'OFF'}</button>;
};

// 使用
<Toggle>
  <Toggle.On>The switch is on</Toggle.On>
  <Toggle.Off>The switch is off</Toggle.Off>
  <Toggle.Button />
</Toggle>

Vue 复合组件

<template>
  <Select v-model="selected">
    <SelectOption value="a">Option A</SelectOption>
    <SelectOption value="b">Option B</SelectOption>
  </Select>
</template>

<script setup>
// Select.vue 使用 provide/inject 实现隐式状态共享
provide('selectValue', selected);
provide('selectOnChange', (val) => { selected.value = val; });
</script>

3.4 Provider 模式(依赖注入)

// React Context(前端依赖注入的标准实现)
const ThemeContext = createContext('light');
const UserContext = createContext(null);
const I18nContext = createContext({ locale: 'zh-CN' });

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <UserContext.Provider value={{ id: 1, name: 'Alice' }}>
        <I18nContext.Provider value={{ locale: 'en' }}>
          <MainLayout />
        </I18nContext.Provider>
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 任意深度的子组件
function ProfileCard() {
  const theme = useContext(ThemeContext);  // "dark"
  const user = useContext(UserContext);     // { id: 1, name: 'Alice' }
  // ...
}

Provider 联级:多个 Provider 嵌套时,useContext 获取最近的 Provider 值。

3.5 Slot 模式

<!-- Vue 插槽 -->
<template>
  <Card>
    <template #header>
      <h2>Card Title</h2>          <!-- 具名插槽 -->
    </template>
    
    <p>Main content here</p>        <!-- 默认插槽 -->
    
    <template #footer="{ user }">
      <span>Created by {{ user.name }}</span>  <!-- 作用域插槽,子传父 -->
    </template>
  </Card>
</template>

<!-- Card 组件 -->
<div class="card">
  <div class="header"><slot name="header" /></div>
  <div class="body"><slot /></div>
  <div class="footer"><slot name="footer" :user="user" /></div>
</div>

React 等价

function Card({ header, children, footer }) {
  return (
    <div className="card">
      <div className="header">{header}</div>
      <div className="body">{children}</div>
      <div className="footer">{footer({ user })}</div>
    </div>
  );
}

4. 架构实践

4.1 组件分层

 src/components/
 ├── base/           # 基础组件(Button, Input, Select, Modal)
 │    只依赖 UI 规范,无业务逻辑
 ├── business/       # 业务组件(UserCard, OrderList, PaymentForm)
 │    组装基础组件,引入领域模型
 ├── page/           # 页面组件(HomePage, CheckoutPage, UserCenter)
 │    页面级编排,路由入口
 └── layout/         # 布局组件(Header, Sidebar, Footer)
      应用框架布局

分层原则

  • 下层不依赖上层:base/ 不能 import business/ 的组件
  • 同一层之间可以互相组合:business/UserCard 可以使用 business/Avatar
  • 数据请求只出现在 page/ 层或自定义 Hook 中

4.2 逻辑复用策略

复用粒度 方案 适用场景
工具函数 纯函数 + 导出 日期格式化、数据转换
组件逻辑 自定义 Hook 状态逻辑、副作用
组件 UI 组件组合 / Slot 通用的 UI 呈现
跨页面流程 HOC / Provider 鉴权、埋点、主题
跨应用 Monorepo 共享包 UI 库、API 客户端

4.3 目录结构设计

按功能组织(Feature-based):

 src/
 ├── features/
 │   ├── auth/
 │   │   ├── components/    # 登录/注册组件
 │   │   ├── hooks/         # useAuth, usePermission
 │   │   ├── api.ts         # auth API 调用
 │   │   ├── types.ts       # 类型定义
 │   │   └── index.ts       # 对外暴露
 │   ├── order/
 │   │   └── ...
 │   └── payment/
 │       └── ...
 ├── shared/          # 跨功能共享
 │   ├── components/  # 基础 UI 组件
 │   ├── utils/       # 工具函数
 │   └── api/         # 通用请求封装
 └── app/             # 应用入口、路由、全局状态

按类型组织(Type-based):

 src/
 ├── components/    # 所有组件
 ├── hooks/         # 所有 Hook
 ├── services/      # 所有 API 调用
 ├── store/         # 所有状态管理
 ├── utils/         # 所有工具函数
 └── types/         # 所有类型定义
组织方式 优点 缺点
按功能 内聚性高,功能移除时删除整个目录 跨功能复用需要抽出 shared
按类型 简单直观,适合小项目 功能散落在多个目录,大型项目维护成本高

推荐:中大型项目按功能组织,小型项目按类型组织。

4.4 BFF(Backend For Frontend)层

解决的问题

  • 前端需要一个接口返回的字段和格式与后端微服务 API 不同
  • 多个后端微服务的数据需要聚合
  • 后端返回了大量前端不需要的字段(网络传输浪费)
  • 协议转换(REST → GraphQL、gRPC → REST)
┌──────┐ HTTP/JSON   ┌──────────┐  gRPC/Thrift   ┌───────────┐
│ Frontend │ ──────→ │   BFF    │ ─────────────→ │  Microservice A│
│          │         │ (Node.js)│                 ├───────────┤
│          │         │          │  HTTP/REST      │ Microservice B│
│          │         │ ┌──────┐ │ ─────────────→ ├───────────┤
│          │         │ │数据聚合│ │                 │ Microservice C│
│          │         │ └──────┘ │                 └───────────┘
└──────────┘         └──────────┘
// BFF 中间层示例(Node.js)
app.get('/api/user/profile', async (req, res) => {
  const [user, orders, points] = await Promise.all([
    userService.getById(req.userId),      // gRPC → User Service
    orderService.listByUser(req.userId),   // gRPC → Order Service
    pointsService.getBalance(req.userId),  // REST → Points Service
  ]);

  // 数据聚合 + 字段裁剪
  res.json({
    id: user.id,
    name: user.name,
    avatar: user.avatar,
    orderCount: orders.length,
    recentOrder: orders[0]?.productName,
    points: points.balance,
    vipLevel: user.vipLevel,
  });
});

BFF 的好处

  • 前端无需知道后端微服务架构细节
  • 按端定制(移动端 BFF 和桌面端 BFF 可不同)
  • 减少前端网络请求数(N 个微服务请求 → 1 个 BFF 请求)
  • 轻量聚合逻辑在 BFF 层完成

4.5 GraphQL

适用场景:当数据需求多样(不同页面需要不同的字段集合)、前后端频繁协商接口时。

# Schema 定义
type User {
  id: ID!
  name: String!
  avatar: String
  orders: [Order!]!
}

type Order {
  id: ID!
  productName: String!
  amount: Float!
  status: OrderStatus!
}

# 查询
query UserProfile($id: ID!) {
  user(id: $id) {
    name
    avatar
    recentOrders: orders(first: 3) {
      productName
      amount
    }
  }
}

GraphQL vs REST BFF

维度 REST BFF GraphQL
字段裁剪 每个端独立部署 BFF 查询方自由选择字段
聚合逻辑 BFF 代码写死 Resolver 编排
缓存 HTTP 缓存天然支持(URL 级别) 需要额外实现(DataLoader + 持久化)
学习曲线
性能开销 需要处理 N+1 问题(DataLoader 解决)
类型安全 手动定义 Schema + Codegen 自动推导

选型建议

  • 前端简单、API 接口稳定的项目 → REST + 简单 BFF 即可
  • 多端(Web/iOS/Android)数据需求差异大 → GraphQL 优势明显
  • 后端微服务众多且持续演进 → BFF 或 GraphQL 均可,取决于团队对 GraphQL 的掌握