Skip to content

E10 Node.js / BFF 服务端 — 练习册 ​

目标:通过练习巩固 Node.js 运行时、BFF 架构、SSR/SSG 服务、Serverless 与边缘计算等核心能力。


难度分级 ​

  • 🟢 基础:理解 Node.js 基础概念,能写简单 API。
  • 🟡 进阶:能设计 BFF 服务,处理 SSR/SSG 选型。
  • 🔴 深入:能设计服务端架构,处理高并发、安全、性能等问题。

基础题 ​

题目 1:Node.js 为什么适合 I/O 密集型场景? ​

考察点:Node.js 事件循环与非阻塞 I/O。

题目: 请解释 Node.js 为什么适合 I/O 密集型场景,但不适合 CPU 密集型场景。

查看答案与解析

参考答案:

1. Node.js 的核心机制 ​

Node.js 采用以下核心设计:

  • 单线程事件循环:主线程通过一个事件循环处理所有请求。
  • 非阻塞 I/O:网络请求、文件读写等 I/O 操作不会阻塞主线程,完成后通过回调/Promise 通知事件循环。
  • 事件驱动:基于回调、事件发射器、Promise、async/await 处理异步结果。
        用户请求 A
            │
            ▼
    ┌───────────────┐
    │   事件循环     │
    └───────┬───────┘
            │
    ┌───────┴───────┐
    │  非阻塞 I/O    │  ← 将操作交给操作系统/线程池
    │  (网络/文件)  │
    └───────┬───────┘
            │
            ▼
    I/O 完成后回调事件循环
            │
            ▼
    处理响应并返回给用户

2. 为什么适合 I/O 密集型 ​

I/O 密集型操作的特点是:

  • 大部分时间花在等待网络/磁盘响应上。
  • 不需要大量 CPU 计算。

Node.js 的优势:

  • 一个请求发起 I/O 后,主线程可以立即处理其他请求。
  • 相同的硬件资源可以支撑大量并发连接(C10K 问题)。
  • 适合 Web 服务器、API 网关、BFF、实时通信等场景。

3. 为什么不适合 CPU 密集型 ​

CPU 密集型任务的特点:

  • 需要长时间占用 CPU 进行计算。
  • 会阻塞事件循环,导致其他请求无法被处理。

示例:

js
// 这是一个会阻塞事件循环的 CPU 密集型操作
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// 当 n 很大时,整个 Node.js 进程会被卡住
app.get('/fib/:n', (req, res) => {
  const result = fibonacci(Number(req.params.n));
  res.json({ result });
});

4. CPU 密集型任务的解决方案 ​

  • Worker Threads:使用 Node.js 的 worker_threads 模块,在独立线程中执行计算。
  • 子进程:通过 child_process 调用其他程序。
  • 外部服务:将计算任务交给 Python、Go、Rust 等更适合的语言/服务处理。
  • 任务队列:使用 Redis + Bull/BullMQ 等队列,将任务异步化。
js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  app.get('/fib/:n', async (req, res) => {
    const worker = new Worker(__filename, {
      workerData: Number(req.params.n),
    });
    worker.on('message', (result) => res.json({ result }));
    worker.on('error', (err) => res.status(500).json({ error: err.message }));
  });
} else {
  function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
  parentPort.postMessage(fibonacci(workerData));
}

题目 2:CommonJS 和 ES Module 有什么区别? ​

考察点:Node.js 模块系统。

题目: 请对比 CommonJS 和 ES Module,并说明现代 Node.js 项目应该如何选择。

查看答案与解析

参考答案:

1. 核心区别 ​

特性CommonJS (CJS)ES Module (ESM)
语法require / module.exportsimport / export
加载时机运行时动态加载编译时静态分析
同步/异步同步加载异步加载(顶层 import)
树摇优化难以 Tree Shaking天然支持 Tree Shaking
循环依赖允许,但行为复杂允许,规范更明确
顶层 thismodule.exportsundefined
文件扩展名.js / .cjs.mjs / .js(配合 "type": "module")
动态导入require() 本身动态import() 支持动态

2. CommonJS 示例 ​

js
// math.js
function add(a, b) {
  return a + b;
}
module.exports = { add };

// app.js
const { add } = require('./math.js');
console.log(add(1, 2));

3. ES Module 示例 ​

js
// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
console.log(add(1, 2));

4. 现代项目如何选择 ​

推荐优先使用 ES Module:

  1. 在 package.json 中设置 "type": "module"。
  2. 使用 .js 扩展名即可按 ESM 解析。
  3. 需要 CommonJS 的文件使用 .cjs 扩展名。

适用场景

场景推荐
新项目ESM
需要 Tree Shaking 的库ESM
前端工程化(Vite、Rollup、Webpack)ESM
需要动态加载的模块ESM + import()
兼容老旧 Node.js 模块CJS
一些配置文件(如 PostCSS、Tailwind)根据工具要求

5. 混合使用注意事项 ​

js
// ESM 中导入 CJS:可以,但默认导出可能是一个对象
import cjsModule from './legacy.cjs';

// CJS 中导入 ESM:必须用动态 import
async function loadEsm() {
  const { add } = await import('./math.js');
  console.log(add(1, 2));
}

6. TypeScript 项目配置 ​

json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022"
  }
}

题目 3:写一个最简单的 Express 服务 ​

考察点:基础服务端代码能力。

题目: 请用 Express 实现一个 GET 接口 /api/hello,返回 JSON { message: 'Hello, World!' }。

查看答案与解析

参考答案:

1. 完整代码 ​

ts
import express from 'express';

const app = express();
const PORT = 3000;

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello, World!' });
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

2. package.json 配置 ​

json
{
  "name": "simple-express-server",
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "typescript": "^5.3.0"
  }
}

3. 运行方式 ​

bash
npm install
npm run dev

访问 http://localhost:3000/api/hello 即可看到返回结果。

4. 扩展:添加中间件 ​

ts
import express from 'express';
import cors from 'cors';

const app = express();

// 解析 JSON 请求体
app.use(express.json());
// 启用跨域
app.use(cors());

app.get('/api/hello', (req, res) => {
  res.json({ message: 'Hello, World!' });
});

app.post('/api/echo', (req, res) => {
  res.json({ received: req.body });
});

// 全局错误处理中间件
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

app.listen(3000);

题目 4:BFF 的核心职责是什么? ​

考察点:BFF 基本概念。

题目: 什么是 BFF?它主要解决什么问题?请列举至少 4 个核心职责。

查看答案与解析

参考答案:

1. 什么是 BFF ​

BFF = Backend for Frontend,即“面向前端的后端”。

BFF 是介于前端应用和后端微服务/数据库之间的一层服务,专门为一个或多个前端应用(Web、H5、小程序、App、Admin 后台)提供定制化的 API。

┌─────────┐      ┌─────────┐      ┌─────────────────┐
│   Web   │      │   BFF   │      │  用户服务        │
├─────────┤      │  (Node) │      │  订单服务        │
│   H5    │ ───▶ │         │ ───▶ │  商品服务        │
├─────────┤      │ · 聚合   │      │  营销服务        │
│  小程序  │      │ · 适配   │      │  支付服务        │
├─────────┤      │ · 鉴权   │      └─────────────────┘
│  Admin  │      │ · 缓存   │
└─────────┘      └─────────┘

2. 主要解决的问题 ​

问题说明
多端接口耦合不同端对数据格式、字段、聚合粒度要求不同,直接调用后端服务难以兼顾。
多次请求一个页面需要调用多个后端服务,前端需要发多次请求。
数据格式不适配后端 DTO 不适合直接展示,需要转换。
鉴权限流分散各端重复实现登录校验、权限控制。
协议转换前端使用 REST/GraphQL,后端使用 gRPC/Thrift。

3. 核心职责 ​

  1. 接口聚合(Aggregation)

    • 将多个下游服务的调用合并为一个前端接口。
    • 使用 Promise.all 并行调用,减少响应时间。
  2. 数据适配(Adaptation)

    • 将后端 DTO 转换为前端需要的 ViewModel。
    • 字段映射、单位转换、格式化、脱敏等。
  3. 鉴权与限流(Auth & Rate Limit)

    • 统一校验用户身份、权限。
    • 对下游服务进行限流,防止被压垮。
  4. 缓存(Caching)

    • 对热点数据做 Redis/内存缓存。
    • 减少下游调用次数,提升响应速度。
  5. 协议转换(Protocol Transformation)

    • 将前端的 HTTP/GraphQL 请求转换为后端的 gRPC/Thrift/消息队列。
  6. 错误处理与降级(Error Handling & Degradation)

    • 下游服务失败时返回兜底数据或友好错误。
    • 避免单点故障拖垮整个页面。

4. BFF 不是 ​

  • 不是前端框架:BFF 是服务端层。
  • 不是简单的 API 代理:BFF 包含业务逻辑、数据转换、聚合等。
  • 不能完全替代后端服务:后端服务负责核心业务和数据持久化。

进阶题 ​

题目 5:设计一个 BFF 接口 ​

考察点:BFF 实战设计。

题目: 后端有两个服务:用户服务 /user/:id 返回用户信息,订单服务 /orders?userId= 返回订单列表。请设计一个 H5 端 BFF 接口 /api/user-summary,返回用户头像、昵称、订单总数、最近一笔订单金额。

查看答案与解析

参考答案:

1. 接口设计 ​

GET /api/user-summary?userId=123

Response:
{
  "avatar": "https://cdn.example.com/avatar/123.jpg",
  "nickname": "张三",
  "orderCount": 15,
  "lastOrderAmount": 299.00
}

2. 完整实现 ​

ts
import express from 'express';

const app = express();

// 模拟下游服务
const userService = {
  async get(userId: string) {
    const res = await fetch(`https://user-service.example.com/user/${userId}`);
    if (!res.ok) throw new Error('用户服务异常');
    return res.json();
  },
};

const orderService = {
  async getByUserId(userId: string) {
    const res = await fetch(`https://order-service.example.com/orders?userId=${userId}`);
    if (!res.ok) throw new Error('订单服务异常');
    return res.json();
  },
};

app.get('/api/user-summary', async (req, res, next) => {
  try {
    const userId = req.query.userId as string;

    if (!userId) {
      return res.status(400).json({ error: '缺少 userId 参数' });
    }

    // 并行请求用户和订单数据
    const [user, orders] = await Promise.all([
      userService.get(userId),
      orderService.getByUserId(userId),
    ]);

    // 数据适配:只返回前端需要的字段
    const summary = {
      avatar: user.avatar,
      nickname: user.nickname,
      orderCount: orders.length,
      lastOrderAmount: orders[0]?.amount || 0,
    };

    res.json(summary);
  } catch (err) {
    next(err);
  }
});

// 全局错误处理
app.use((err: Error, req: express.Request, res: express.Response) => {
  console.error(err);
  res.status(500).json({ error: '服务暂不可用,请稍后重试' });
});

app.listen(3000);

3. 生产环境增强 ​

ts
app.get('/api/user-summary', async (req, res, next) => {
  try {
    const userId = req.query.userId as string;
    if (!userId) return res.status(400).json({ error: '缺少 userId' });

    // 1. 先获取用户(用户是必要依赖)
    const user = await userService.get(userId);

    // 2. 订单服务失败时返回空数组,避免整个接口失败
    const orders = await orderService
      .getByUserId(userId)
      .catch((err) => {
        logger.warn('订单服务异常', err);
        return [];
      });

    // 3. 对订单排序(如果后端未排序)
    const sortedOrders = orders.sort(
      (a: any, b: any) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
    );

    // 4. 数据脱敏:头像 URL 加签名
    const avatarUrl = addCdnSignature(user.avatar);

    res.json({
      avatar: avatarUrl,
      nickname: user.nickname,
      orderCount: sortedOrders.length,
      lastOrderAmount: sortedOrders[0]?.amount || 0,
      lastOrderTime: sortedOrders[0]?.createdAt || null,
    });
  } catch (err) {
    next(err);
  }
});

4. 设计要点 ​

  • 并行调用:用户和订单无依赖关系,使用 Promise.all 减少等待时间。
  • 优雅降级:订单服务失败时不影响用户信息的返回。
  • 字段裁剪:只返回前端需要的字段,减少传输体积。
  • 错误处理:统一错误处理,不暴露下游服务细节。

题目 6:SSR 和 SSG 应该怎么选? ​

考察点:渲染模式选型。

题目: 请对比 CSR、SSR、SSG、ISR 四种渲染模式,并说明各自适用场景。

查看答案与解析

参考答案:

1. 四种模式对比 ​

方案首屏时间SEO实时性服务器成本复杂度典型场景
CSR慢差高低低后台系统、强交互应用
SSR快好高高高内容站、电商、门户网站
SSG最快最好低最低低博客、文档、营销页
ISR快好中中中新闻、商品详情、CMS 页面

2. CSR(Client-Side Rendering) ​

浏览器请求 HTML → 返回空壳 HTML + JS
                    │
                    ▼
            浏览器下载并执行 JS
                    │
                    ▼
            JS 请求数据并渲染页面
  • 优点:交互体验好,服务器压力小,部署简单。
  • 缺点:首屏慢,SEO 差。
  • 适用:Dashboard、后台管理系统、SPA 应用。

3. SSR(Server-Side Rendering) ​

浏览器请求页面 → 服务器实时获取数据
                       │
                       ▼
              服务器渲染完整 HTML
                       │
                       ▼
              浏览器展示 HTML + hydration
  • 优点:首屏快,SEO 好,实时性强。
  • 缺点:服务器压力大,开发复杂,需要处理hydration、状态同步等问题。
  • 适用:电商首页、商品详情、内容站、需要 SEO 的页面。

4. SSG(Static Site Generation) ​

构建时获取数据 → 生成静态 HTML
                       │
                       ▼
              部署到 CDN
                       │
                       ▼
              用户直接访问静态文件
  • 优点:首屏最快,SEO 最好,成本低。
  • 缺点:内容更新需要重新构建,实时性差。
  • 适用:博客、文档站、营销落地页、企业官网。

5. ISR(Incremental Static Regeneration) ​

首次请求:返回缓存的静态页面(可能稍旧)
              │
              ▼
      后台异步重新生成页面
              │
              ▼
      下次请求返回最新页面
  • 优点:兼顾 SSG 的性能和 SSR 的实时性。
  • 缺点:首次访问可能拿到旧数据,需要处理过期策略。
  • 适用:新闻详情、商品详情、CMS 驱动的页面。

6. 选型决策树 ​

需要 SEO 吗?
  ├─ 否 → CSR(强交互场景)
  └─ 是 → 数据变化频繁吗?
           ├─ 是 → SSR(实时内容)
           └─ 否 → 页面数量多吗?
                    ├─ 是 → ISR(大规模静态页面)
                    └─ 否 → SSG(固定内容)

7. 混合架构 ​

现代应用通常采用混合架构:

  • 营销页、帮助文档:SSG
  • 商品详情、文章页:ISR
  • 用户中心、购物车:CSR
  • 搜索页、SEO 关键页:SSR

题目 7:如何处理服务端错误? ​

考察点:服务端健壮性。

题目: 在 Express 中,如何处理异步函数抛出的错误?请给出代码示例。

查看答案与解析

参考答案:

1. 问题背景 ​

Express 默认不会捕获异步函数中的错误。如果 async 路由处理器中抛出异常,请求会挂起或进程崩溃。

ts
// ❌ 错误示例:异常不会被 Express 捕获
app.get('/api/data', async (req, res) => {
  const data = await fetchData(); // 如果这里报错,请求会挂起
  res.json(data);
});

2. 方案一:asyncHandler 包装器 ​

ts
import { Request, Response, NextFunction, RequestHandler } from 'express';

type AsyncRequestHandler = (req: Request, res: Response, next: NextFunction) => Promise<any>;

const asyncHandler = (fn: AsyncRequestHandler): RequestHandler => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

// 使用
app.get('/api/data', asyncHandler(async (req, res) => {
  const data = await fetchData();
  res.json(data);
}));

3. 方案二:Express 5 原生支持 ​

Express 5 开始原生支持 async 错误处理:

ts
app.get('/api/data', async (req, res) => {
  const data = await fetchData();
  res.json(data);
});

4. 方案三:完整的错误处理体系 ​

ts
import express, { Request, Response, NextFunction } from 'express';

const app = express();

// 自定义错误类
class AppError extends Error {
  constructor(
    public statusCode: number,
    public code: string,
    message: string
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// 异步包装
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// 业务路由
app.get('/api/user/:id', asyncHandler(async (req: Request, res: Response) => {
  const userId = req.params.id;
  if (!userId) {
    throw new AppError(400, 'INVALID_PARAM', '用户 ID 不能为空');
  }

  const user = await fetchUser(userId);
  if (!user) {
    throw new AppError(404, 'USER_NOT_FOUND', '用户不存在');
  }

  res.json(user);
}));

// 404 处理
app.use((req, res, next) => {
  next(new AppError(404, 'NOT_FOUND', '接口不存在'));
});

// 全局错误处理中间件
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      code: err.code,
      message: err.message,
    });
  }

  // 未知错误
  console.error('Unhandled error:', err);
  res.status(500).json({
    code: 'INTERNAL_ERROR',
    message: process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误',
  });
});

app.listen(3000);

5. 错误处理最佳实践 ​

  • 统一错误格式:所有错误返回 { code, message, details? } 结构。
  • 区分业务错误和系统错误:业务错误返回 400/404,系统错误返回 500。
  • 不泄露敏感信息:生产环境不返回堆栈信息。
  • 日志记录:所有 500 错误必须记录,便于排查。
  • 监控告警:核心接口错误率超过阈值时触发告警。

题目 8:Serverless 的优缺点是什么? ​

考察点:Serverless 理解。

题目: Serverless 有哪些优点和缺点?前端在什么场景下适合使用 Serverless?

查看答案与解析

参考答案:

1. Serverless 的优点 ​

优点说明
按需付费只有请求执行时才计费,无流量时成本接近零。
自动扩缩容云服务商根据请求量自动扩展,无需手动配置。
免运维不需要管理服务器、操作系统、补丁、容量规划。
快速部署只需上传代码即可运行,发布周期短。
事件驱动easily 与对象存储、消息队列、定时任务等事件源集成。

2. Serverless 的缺点 ​

缺点说明
冷启动延迟函数长时间未调用后首次调用会有延迟(几百毫秒到数秒)。
运行时长限制多数平台限制单次执行时间(如 15 分钟)。
本地调试复杂依赖云环境,本地难以完全模拟。
状态管理受限函数无状态,需要依赖外部存储。
供应商锁定不同平台的 API 和特性差异大,迁移成本高。
成本不可控高并发场景下,按调用次数计费可能比固定服务器更贵。

3. 前端适合使用 Serverless 的场景 ​

  1. BFF(Backend for Frontend)

    • 聚合多个后端接口,为前端提供定制化 API。
    • 示例:Vercel Functions、Netlify Functions、阿里云函数计算。
  2. 轻量 API

    • 表单提交、联系表单、投票、评论等简单接口。
  3. 边缘逻辑

    • 在 CDN 边缘节点执行 A/B 测试、地理定位、请求改写。
    • 示例:Vercel Edge Functions、Cloudflare Workers。
  4. 事件处理

    • 图片上传后自动压缩、Webhook 接收、消息推送。
  5. 低频任务

    • 定时报表生成、数据同步、邮件发送。

4. 不适合 Serverless 的场景 ​

  • 需要长时间运行的服务(如 WebSocket 长连接、视频流处理)。
  • 对延迟极其敏感的核心链路(冷启动问题)。
  • 高并发且持续运行的服务(成本可能更高)。
  • 需要大量本地状态或复杂依赖的应用。

5. 示例:Vercel Serverless Function ​

ts
// api/hello.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';

export default function handler(req: VercelRequest, res: VercelResponse) {
  res.json({ message: 'Hello from Serverless!' });
}

深入题 ​

题目 9:设计一个高可用 BFF 架构 ​

考察点:服务端架构设计。

题目: 一个电商平台的 BFF 每天处理 1000 万请求,高峰 QPS 5000。请设计一个高可用 BFF 架构,包括负载均衡、缓存、限流、降级方案。

查看答案与解析

参考答案:

1. 整体架构 ​

                              ┌─────────────┐
                              │   用户请求   │
                              └──────┬──────┘
                                     ▼
                         ┌───────────────────────┐
                         │   CDN / WAF / DNS     │
                         └───────────┬───────────┘
                                     ▼
                         ┌───────────────────────┐
                         │   负载均衡(L7/L4)    │
                         │   Nginx / 云 LB       │
                         └───────────┬───────────┘
                                     ▼
                    ┌────────────────────────────────┐
                    │        BFF 服务集群             │
                    │  ┌─────┐ ┌─────┐ ┌─────┐      │
                    │  │Pod 1│ │Pod 2│ │Pod N│      │
                    │  └─────┘ └─────┘ └─────┘      │
                    │       自动扩缩容 HPA            │
                    └────────────────────────────────┘
                                     │
           ┌─────────────────────────┼─────────────────────────┐
           ▼                         ▼                         ▼
   ┌───────────────┐        ┌───────────────┐        ┌───────────────┐
   │   Redis 缓存   │        │  限流/熔断中心 │        │  配置/注册中心 │
   │  · 热点数据    │        │  · Token Bucket│        │  · Nacos      │
   │  · 会话状态    │        │  · Circuit     │        │  · Consul     │
   └───────────────┘        │    Breaker     │        └───────────────┘
                            └───────────────┘
                                     │
           ┌─────────────────────────┼─────────────────────────┐
           ▼                         ▼                         ▼
   ┌───────────────┐        ┌───────────────┐        ┌───────────────┐
   │   用户服务     │        │   订单服务     │        │   商品服务     │
   └───────────────┘        └───────────────┘        └───────────────┘

2. 负载均衡 ​

  • 入口层:使用云 LB(如阿里云 SLB、AWS ALB)做 L4/L7 负载均衡。
  • 服务层:使用 Nginx 或 Service Mesh(Istio)做服务发现和流量分发。
  • 策略:轮询 + 最小连接数 + 健康检查,自动剔除异常节点。
  • 自动扩缩容:基于 CPU、内存、QPS 指标配置 HPA,高峰自动扩容,低峰缩容。

3. 缓存策略 ​

层级作用示例
浏览器缓存静态资源、接口响应Cache-Control: max-age=300
CDN 缓存静态资源、不常变化的数据商品分类、配置
BFF 本地缓存进程内缓存,极低延迟Caffeine、Node-Cache
Redis 缓存分布式缓存,热点数据用户信息、商品详情
下游接口缓存减少下游调用对响应时间较长的接口做缓存

缓存更新策略:

  • Cache-Aside:先读缓存,未命中再读数据库并写入缓存。
  • TTL 过期:设置合理的过期时间,避免数据长期不一致。
  • 主动失效:数据变更时通过消息队列通知缓存失效。

4. 限流方案 ​

  • 入口限流:在 Nginx/网关层按 IP、User-Agent 限流。
  • 接口限流:使用 Token Bucket 或 Leaky Bucket 算法,按用户、接口限流。
  • 下游保护:对下游服务调用做限流,防止把下游打垮。
ts
// Token Bucket 限流示例(概念)
class TokenBucket {
  constructor(private capacity: number, private refillRate: number) {}

  allowRequest(tokens: number = 1): boolean {
    this.refill();
    if (this.tokens >= tokens) {
      this.tokens -= tokens;
      return true;
    }
    return false;
  }
}

5. 降级方案 ​

场景降级策略
下游服务超时返回缓存数据或默认值
下游服务报错非核心数据返回空,核心数据返回错误提示
推荐服务不可用首页降级为固定推荐位
库存服务异常显示“库存紧张”,不允许下单
优惠券服务异常跳过优惠券计算,按原价展示

6. 熔断方案 ​

使用 Circuit Breaker 模式:

  • Closed:正常调用下游服务。
  • Open:失败率达到阈值后,直接快速失败,不再调用下游。
  • Half-Open:一段时间后允许少量请求试探,成功后恢复 Closed。

工具:Hystrix、Resilience4j、Polly、自研熔断器。

7. 监控与告警 ​

  • 指标:QPS、延迟 P99、错误率、CPU、内存、GC。
  • 日志:结构化日志,支持链路追踪。
  • 告警:错误率 > 1%、P99 > 500ms、CPU > 80% 时触发告警。

题目 10:Node.js 服务的性能优化 ​

考察点:服务端性能优化。

题目: 一个 Node.js BFF 服务响应变慢,请列出至少 6 个排查和优化方向。

查看答案与解析

参考答案:

1. 资源使用排查 ​

  • CPU:使用 top、htop、Node.js --prof 分析 CPU 占用。
  • 内存:使用 process.memoryUsage()、heap dump 检查内存泄漏。
  • 事件循环延迟:使用 clinic.js、0x 或内置 perf_hooks 检测事件循环阻塞。

2. 同步阻塞排查 ​

  • 检查是否有同步文件读写、大数据量 JSON 序列化、复杂正则表达式。
  • 同步阻塞会导致整个事件循环卡顿,影响所有请求。
js
// ❌ 同步阻塞示例
const data = fs.readFileSync('./large-file.json');
const parsed = JSON.parse(data); // 大 JSON 解析会阻塞

3. 数据库/外部 API 优化 ​

  • 检查慢查询,添加索引。
  • 使用连接池,避免频繁创建连接。
  • 对外部 API 设置超时,避免长时间等待。
  • 使用 Promise.all 并行调用无依赖的接口。

4. 缓存优化 ​

  • 对热点数据使用 Redis 缓存。
  • 对接口响应做内存缓存(注意 TTL 和并发更新)。
  • 使用 CDN 缓存静态资源。

5. 网络优化 ​

  • 启用 gzip/brotli 压缩响应。
  • 使用 HTTP/2 减少连接开销。
  • 减少响应体积,只返回必要字段。

6. 代码层面优化 ​

  • 避免重复计算,使用缓存结果。
  • 减少不必要的中间件和日志输出。
  • 使用 Stream 处理大文件,避免一次性读入内存。
  • 使用 cluster 模块或 PM2 多进程利用多核 CPU。
js
// 使用 cluster 利用多核
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  for (let i = 0; i < os.cpus().length; i++) {
    cluster.fork();
  }
} else {
  require('./app.js');
}

7. 架构层面优化 ​

  • 将 CPU 密集型任务拆分到 Worker Threads 或外部服务。
  • 使用边缘计算(Edge Function)减少中心服务器压力。
  • 对读写分离,读多写少场景使用缓存优先。

8. 监控与压测 ​

  • 使用 Artillery、k6 进行压力测试,找到瓶颈。
  • 建立性能基线,持续监控响应时间分位数(P50/P95/P99)。

题目 11:前后端共享类型的实践 ​

考察点:类型安全与工程效率。

题目: 如何在一个 Monorepo 中实现前后端共享 TypeScript 类型?请说明目录结构和实现方式。

查看答案与解析

参考答案:

1. 目录结构 ​

packages/
├── shared/                 # 共享类型和校验 Schema
│   ├── src/
│   │   ├── types/
│   │   │   ├── api.ts      # API 请求/响应类型
│   │   │   ├── models.ts   # 业务模型类型
│   │   │   └── index.ts
│   │   ├── schemas/
│   │   │   ├── user.ts     # Zod Schema
│   │   │   └── order.ts
│   │   └── index.ts
│   ├── package.json
│   └── tsconfig.json
├── web/                    # 前端项目
│   ├── src/
│   └── package.json
└── server/                 # 后端项目
    ├── src/
    └── package.json

2. shared 包配置 ​

json
// packages/shared/package.json
{
  "name": "@myapp/shared",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "dependencies": {
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0"
  }
}
ts
// packages/shared/src/types/api.ts
export interface GetUserResponse {
  id: string;
  nickname: string;
  avatar: string;
}

export interface CreateOrderRequest {
  productId: string;
  quantity: number;
}

export interface CreateOrderResponse {
  orderId: string;
  totalAmount: number;
}
ts
// packages/shared/src/schemas/user.ts
import { z } from 'zod';

export const userSchema = z.object({
  id: z.string(),
  nickname: z.string().min(1).max(50),
  avatar: z.string().url(),
});

export type User = z.infer<typeof userSchema>;

3. 前端使用 ​

json
// packages/web/package.json
{
  "dependencies": {
    "@myapp/shared": "workspace:*"
  }
}
ts
// packages/web/src/api/user.ts
import type { GetUserResponse } from '@myapp/shared';

export async function getUser(id: string): Promise<GetUserResponse> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

4. 后端使用 ​

json
// packages/server/package.json
{
  "dependencies": {
    "@myapp/shared": "workspace:*"
  }
}
ts
// packages/server/src/routes/user.ts
import { userSchema, type User } from '@myapp/shared';
import express from 'express';

const app = express();

app.get('/api/users/:id', (req, res) => {
  const user: User = {
    id: req.params.id,
    nickname: '张三',
    avatar: 'https://example.com/avatar.jpg',
  };

  // 运行时校验
  const result = userSchema.safeParse(user);
  if (!result.success) {
    return res.status(500).json({ error: '数据格式错误' });
  }

  res.json(result.data);
});

5. 关键实践 ​

  • Zod 同时提供类型和运行时校验:前端用类型,后端用 Schema 校验。
  • 构建顺序:Monorepo 中确保 shared 先构建。
  • 避免循环依赖:shared 包只包含类型和纯函数,不依赖 web/server。
  • 版本管理:使用 Changesets 管理 shared 包版本升级。

题目 12:设计一个高可用 BFF 架构(大促场景) ​

考察点:高可用 BFF 架构设计、缓存、限流、降级、异步化。

题目: 某电商平台大促期间,详情页 BFF 需要承载 10 倍日常流量,峰值 QPS 5 万。请设计一个高可用 BFF 架构,要求:

  1. 接口响应时间 P99 < 200ms。
  2. 下游服务故障时,核心数据不丢、非核心数据可降级。
  3. 写操作(如下单、领券)不丢失,库存扣减最终一致。
  4. 具备完整的可观测性。
查看答案与解析

参考答案:

1. 整体架构 ​

用户请求 → CDN / WAF / DNS → API Gateway / Nginx
                ↓
        ┌─────────────────┐
        │   BFF 服务集群    │  K8s HPA 自动扩缩容
        │  Node.js/NestJS  │
        └────────┬────────┘
                 │
    ┌────────────┼────────────┐
    ▼            ▼            ▼
 Redis 缓存   限流/熔断中心   消息队列(Kafka/RabbitMQ)
    │            │            │
    ▼            ▼            ▼
 下游微服务   配置/注册中心   异步任务处理

2. 分层策略 ​

层级职责具体措施
接入层流量清洗、全局负载均衡CDN + WAF + 云 LB
网关层路由、鉴权、入口限流Kong/Nginx + Redis 限流
BFF 层接口聚合、数据适配、降级NestJS + 熔断器 + 缓存
服务层业务逻辑下游微服务
数据层持久化主从数据库 + 读写分离
消息层异步削峰Kafka 订单流、BullMQ 任务

3. 缓存策略 ​

  • 浏览器/CDN 缓存:静态资源、商品基础信息。
  • BFF 本地缓存:商品类目、配置项,TTL 30 秒。
  • Redis 分布式缓存:热点商品详情、用户信息,命中率目标 90%。
  • 缓存更新:商品变更通过 Kafka 广播,BFF 本地缓存失效。
typescript
// 伪代码:缓存优先 + 降级
async function getProductDetail(id: string) {
  const cacheKey = `product:${id}`;
  let data = await redis.get(cacheKey);
  if (data) return JSON.parse(data);

  try {
    data = await productService.get(id);
    await redis.setex(cacheKey, 60, JSON.stringify(data));
    return data;
  } catch (err) {
    // 下游故障时返回过期缓存或兜底数据
    const stale = await redis.get(`product:${id}:stale`);
    if (stale) return JSON.parse(stale);
    throw new AppError(503, 'SERVICE_UNAVAILABLE', '商品服务暂不可用');
  }
}

4. 限流与熔断 ​

  • 入口限流:API Gateway 按 IP、用户 ID 限流,防止刷单。
  • 接口限流:核心接口(下单、领券)使用 Token Bucket,限制每秒请求数。
  • 下游熔断:使用 Resilience4j/opossum,失败率超过 50% 自动熔断,10 秒后半开试探。
typescript
// 基于 Redis 的分布式限流(概念)
const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'bff_limit',
  points: 100, // 每分钟 100 次
  duration: 60,
});

5. 异步化与最终一致 ​

  • 下单流程:BFF 校验后写入 Kafka,返回“处理中”状态;订单服务消费后异步处理。
  • 库存扣减:使用 Redis 预扣 + Kafka 异步同步到数据库,失败可回滚。
  • 非关键数据:埋点、日志、推荐点击通过 BullMQ 异步发送,不阻塞主链路。

6. 可观测性 ​

  • 指标:Prometheus + Grafana 监控 QPS、P99、错误率、CPU、内存。
  • 链路追踪:OpenTelemetry + Jaeger,traceId 全链路透传。
  • 日志:结构化 JSON 日志,ELK/Loki 集中收集。
  • 告警:P99 > 200ms、错误率 > 0.5%、CPU > 80% 触发告警。

题目 13:tRPC 与 gRPC 在 BFF 中如何选型? ​

考察点:现代接口模式理解、选型能力。

题目: 请对比 tRPC 和 gRPC,说明它们各自适用的场景。如果你要为一个全 TypeScript 栈的 SaaS 项目设计 BFF,会如何选择?如果后端是多语言微服务(Java、Go、Python),又该如何选择?

查看答案与解析

参考答案:

1. tRPC 特点 ​

  • 端到端类型安全:前后端共享 TypeScript 类型,无需手写 Swagger。
  • 开发体验好:与 React Query、Next.js 集成顺畅。
  • 仅 TypeScript 生态:服务端和客户端都必须是 TS。
  • 适合 BFF → 前端:BFF 层暴露 tRPC,前端直接调用。
typescript
// 服务端定义
const appRouter = router({
  user: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(({ input }) => db.user.findById(input.id)),
});

// 前端调用,自动有类型
const { data } = trpc.user.useQuery({ id: '123' });

2. gRPC 特点 ​

  • 高性能:基于 HTTP/2 + Protocol Buffers,二进制序列化。
  • 多语言支持:Java、Go、Python、Node.js 都有成熟客户端。
  • 适合微服务间通信:BFF 调用后端 gRPC 服务。
  • 浏览器不友好:前端需通过 gRPC-Web 或 BFF 代理。
protobuf
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest { string id = 1; }
message User { string id = 1; string name = 2; }

3. 选型建议 ​

场景推荐
全 TypeScript 栈,BFF → 前端tRPC
多语言后端,BFF → 微服务gRPC
需要强类型 + 高性能内部通信gRPC
快速迭代、小团队tRPC
大型企业、多团队协作gRPC + GraphQL Federation

4. 混合方案 ​

实际项目中 often 采用混合方案:

  • BFF 与前端之间用 tRPC 或 REST/GraphQL。
  • BFF 与后端微服务之间用 gRPC 或 HTTP。
  • 通过 API Gateway 统一接入,内部按团队技术栈选择。

题目 14:设计一个 BFF 的可观测性方案 ​

考察点:可观测性、OpenTelemetry、Prometheus、Jaeger、APM。

题目: 请为一套 Node.js BFF 服务设计完整的可观测性方案,要求覆盖日志、指标、链路追踪,并能在生产环境快速定位慢请求和错误根因。

查看答案与解析

参考答案:

1. 可观测性三大支柱 ​

支柱工具用途
日志Pino/Winston + ELK/Loki记录请求上下文、错误详情
指标Prometheus + GrafanaQPS、延迟、错误率、资源使用
链路追踪OpenTelemetry + Jaeger请求全链路耗时分析

2. Trace ID 透传 ​

在 BFF 入口生成 traceId,通过 HTTP Header 透传到下游服务。

typescript
import { AsyncLocalStorage } from 'async_hooks';
const context = new AsyncLocalStorage();

app.use((req, res, next) => {
  const traceId = req.headers['x-trace-id'] || uuid();
  context.run({ traceId, userId: req.userId }, next);
});

function log(level: string, message: string, meta?: object) {
  const ctx = context.getStore() || {};
  console.log(JSON.stringify({ ...ctx, level, message, ...meta, time: new Date().toISOString() }));
}

3. OpenTelemetry 接入 ​

typescript
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';

const sdk = new NodeSDK({
  serviceName: 'bff-service',
  traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4318/v1/traces' }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

4. Prometheus 指标 ​

typescript
import client from 'prom-client';
const register = new client.Registry();

const httpDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP 请求耗时',
  labelNames: ['route', 'method', 'status'],
});
register.registerMetric(httpDuration);

app.use((req, res, next) => {
  const end = httpDuration.startTimer();
  res.on('finish', () => {
    end({ route: req.route?.path || req.path, method: req.method, status: res.statusCode });
  });
  next();
});

app.get('/metrics', (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(register.metrics());
});

5. 关键 SLO 与告警 ​

指标目标告警阈值
P99 延迟< 200ms> 300ms
错误率< 0.1%> 0.5%
QPS按业务设定突增 300%
CPU< 70%> 80%
内存< 80%> 90%

6. 故障排查流程 ​

  1. 收到告警:Grafana 发现 P99 升高。
  2. 查看链路:Jaeger 找到慢请求的 span,定位到某个下游服务。
  3. 查看日志:通过 traceId 检索相关日志,发现下游超时。
  4. 查看指标:确认下游服务错误率上升。
  5. 采取措施:熔断、扩容、联系下游团队。

领域编号:E10 Node.js / BFF 服务端
最后更新:2026-06-24

基于 MIT 协议发布