首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Mock和Spy的真面目:你的测试是在真的隔离依赖还是在自欺欺人?

Mock和Spy的真面目:你的测试是在真的隔离依赖还是在自欺欺人?

作者头像
前端达人
发布2026-03-12 13:20:25
发布2026-03-12 13:20:25
210
举报
文章被收录于专栏:前端达人前端达人

上周,我和一个小伙伴因为一个bug大吵了一架。他写了一个用户注册功能,所有单元测试都通过了,绿灯一片。但部署到测试环境后,邮件服务直接挂掉了。

我问他:"你测试里是怎么处理邮件发送的?"

他挠挠头:"我mock了呀,这样就不会真的去调用邮件API啊。"

我接着问:"那你的mock是真的在隔离依赖,还是在欺骗自己——你的测试根本没在测什么?"

这个对话促使我深度思考mock和spy两个工具。很多开发者把它们当成了测试中的"魔法道具",会用但不知道在做什么。更恐怖的是,很多人写出来的测试根本没在测试真正重要的东西。

核心问题:Mock和Spy到底在做什么?

在开始讲代码之前,我想从哲学角度定义一下这两个工具的本质。

Mock(模拟) 的本质是:用一个假的东西完全替换掉真实的东西,让被测试的代码即使调用了它也不会对系统造成影响

Spy(间谍) 的本质是:在不改变真实实现的前提下,偷偷观察真实对象怎么被使用的

这两个工具解决的是同一个核心问题:依赖隔离。但它们的隔离策略完全不同。

来画个图理解一下:

代码语言:javascript
复制
    真实应用的一次调用链路
    ┌─────────────────────────────────────┐
    │  registerUser() 被调用              │
    │     │                               │
    │     ├─> 验证邮箱                    │
    │     │                               │
    │     └─> 调用 sendEmail()            │
    │            │                        │
    │            └─> 真实邮件API调用      │
    └─────────────────────────────────────┘

    用Mock的测试
    ┌─────────────────────────────────────┐
    │  registerUser() 被调用              │
    │     │                               │
    │     ├─> 验证邮箱                    │
    │     │                               │
    │     └─> 调用 sendEmail() (假的)     │
    │            │                        │
    │            └─> 不调用真实API        │
    └─────────────────────────────────────┘

    用Spy的测试
    ┌─────────────────────────────────────┐
    │  registerUser() 被调用              │
    │     │                               │
    │     ├─> 验证邮箱                    │
    │     │                               │
    │     └─> 调用 sendEmail() (真实)     │
    │            │                        │
    │            ├─> 记录:被调用了!     │
    │            │                        │
    │            └─> 继续执行原逻辑       │
    └─────────────────────────────────────┘

第一部分:Mock—完全替换,绝对隔离

我先从一个真实的场景入手。假设你在做一个用户注册系统,核心逻辑在这里:

代码语言:javascript
复制
// emailService.js
export function sendEmail(to, subject, body) {
  // 真实情况下,这里会调用 SendGrid、AWS SES 或其他外部API
  console.log(`正在通过真实邮件服务发送邮件到 ${to}...`);
  // 实际代码会是几百行的HTTP请求、重试逻辑、错误处理
}
代码语言:javascript
复制
// userController.js
import { sendEmail } from'./emailService.js';

exportfunction registerUser(user) {
if (!user.email) thrownewError('缺少邮箱地址');

// 注册逻辑...

  sendEmail(user.email, '欢迎来到我们的平台!', '感谢你的注册。');
return { success: true, userId: 123 };
}

这里的问题是:sendEmail 依赖了一个外部邮件服务。在单元测试中,我们绝对不能真的去调用它。为什么?因为:

  1. 太慢了 —— 每次测试都等待网络请求,单元测试会变得超级慢
  2. 不稳定 —— 网络可能波动,邮件服务可能宕机,你的测试就会flaky
  3. 有副作用 —— 你真的会往测试邮箱发送邮件(如果配置不当还会发给真实用户)
  4. 无法模拟异常 —— 你怎么测试邮件服务返回500的情况?每次都等着它真的挂掉?

这就是Mock要解决的核心痛点。用Jest的Mock,做法是这样的:

代码语言:javascript
复制
// 这行代码告诉Jest:我要把 './emailService' 这个模块完全替换掉
jest.mock('./emailService', () => ({
sendEmail: jest.fn()  // jest.fn() 创建一个假的函数
}));

// 现在当registerUser导入sendEmail时,它拿到的已经不是真的了
import { registerUser } from'./userController';
import { sendEmail } from'./emailService';

test('用户注册时应该发送欢迎邮件', () => {
const user = { email: 'test@example.com' };
  registerUser(user);

// 关键:验证这个被假的sendEmail被正确调用了
  expect(sendEmail).toHaveBeenCalledWith(
    'test@example.com',
    '欢迎来到我们的平台!',
    '感谢你的注册。'
  );
});

这里的魔法在于 jest.fn() —— 它创建的是一个会记录自己被调用信息的假函数。这个假函数:

  • ✅ 不会真的去调用邮件API
  • ✅ 会记录每次被调用的参数
  • ✅ 可以返回你指定的值
  • ✅ 可以模拟异常

看起来很完美对吧?但这里藏着我之前说的那个坑。

Mock的第一个坑:过度隔离

很多人写出来的Mock测试,根本不像真实场景。比如这样:

代码语言:javascript
复制
jest.mock('./emailService', () => ({
  sendEmail: jest.fn()  // 什么都不做,默认返回undefined
}));

test('注册成功', () => {
  const result = registerUser({ email: 'test@example.com' });
  expect(result.success).toBe(true);
  expect(sendEmail).toHaveBeenCalled();
});

看起来测试通过了。但你有没有想过一个问题:如果emailService.js那边的sendEmail突然改了签名,或者抛异常了,你这个测试根本发现不了

你的测试只验证了"注册函数调用了sendEmail",但没验证"注册函数正确处理了sendEmail的返回值"。真实场景中,如果邮件服务返回error,注册流程该怎么处理?你的test根本没测。

Mock的第二个坑:测试实现细节而不是行为

看这个例子:

代码语言:javascript
复制
test('调用sendEmail三次...不对,两次...等等是多少次?', () => {
  const user1 = { email: 'user1@example.com' };
  const user2 = { email: 'user2@example.com' };
  
  registerUser(user1);
  registerUser(user2);
  
  // 这就是典型的测试实现细节
  expect(sendEmail).toHaveBeenCalledTimes(2);
});

这个测试会因为你改了registerUser内部的任何实现细节而失败,即使外部行为根本没变。这就是脆弱的测试。

Mock的第三个坑:模拟返回值时不够真实

Mock最强大的地方是可以模拟各种场景。但很多人只模拟了成功的情况:

代码语言:javascript
复制
// 模拟邮件服务返回成功
jest.mock('./emailService', () => ({
  sendEmail: jest.fn(() => Promise.resolve({ sent: true }))
}));

test('邮件发送成功', async () => {
  const result = await registerUser({ email: 'test@example.com' });
  expect(result.success).toBe(true);
});

这完全没有测试邮件服务失败的情况。在真实的生产环境中,邮件发送可能会失败,registerUser需要处理这种情况。但你的测试从来没验证过。

第二部分:Spy—保留原貌,植入观察者

现在换个思路。有时候你不想替换真实实现,你只是想观察它怎么被使用。这就是Spy的职责。

假设有一个Logger类:

代码语言:javascript
复制
class Logger {
  log(message) {
    console.log(`[LOG] ${message}`);
  }
}

function doSomething(logger) {
  logger.log('开始处理数据');
  // 做一些事情...
  logger.log('处理完成');
}

用Jest的spyOn,你可以这样测试:

代码语言:javascript
复制
test('应该在合适的时机记录日志', () => {
const logger = new Logger();

// 植入间谍,监听 log 方法
const spy = jest.spyOn(logger, 'log');

  doSomething(logger);

// 验证log被调用了多少次
  expect(spy).toHaveBeenCalledTimes(2);

// 验证调用时的具体参数
  expect(spy).toHaveBeenNthCalledWith(1, '开始处理数据');
  expect(spy).toHaveBeenNthCalledWith(2, '处理完成');

// 重要:清理spy,否则会污染其他测试
  spy.mockRestore();
});

Spy的妙处在于:Logger的log方法的真实实现(那个console.log)仍然在运行。你的spy只是在"监听"而已。

代码语言:javascript
复制
   Spy的工作原理

原始方法调用链:
  └─> logger.log('消息')
       └─> console.log(`[LOG] 消息`)
       └─> 实际打印出来

加了Spy后:
  └─> logger.log('消息')
       ├─> Spy记录:被调用了!参数是'消息'
       └─> console.log(`[LOG] 消息`)
           └─> 实际打印出来

     (真实逻辑继续运行,Spy只是偷偷记录)

第三部分:Jest vs Sinon—两大测试库对比

到目前为止我都在讲Jest。但在大型项目中,有些人用Sinon。两个库的哲学有点不同。

Jest的风格:自动化Mock

Jest会在模块加载时自动处理mock。你在测试文件顶部写 jest.mock('./emailService'),之后所有这个模块的导入都会被替换。

代码语言:javascript
复制
jest.mock('./emailService', () => ({
  sendEmail: jest.fn()
}));

import { registerUser } from './userController';
import { sendEmail } from './emailService';

test('Jest风格的Mock', () => {
  registerUser({ email: 'test@example.com' });
  expect(sendEmail).toHaveBeenCalled();
});

这个做法优点是方便,缺点是有点"魔法"—— 如果你不了解Jest hoisting的原理,会很困惑。

Sinon的风格:手动控制

Sinon是独立的库,不依赖特定的测试框架。它给你更多的控制权:

代码语言:javascript
复制
import sinon from'sinon';

const obj = {
  greet(name) {
    return`你好,${name}`;
  }
};

// 创建stub(Sinon的术语,类似Mock)
const stub = sinon.stub(obj, 'greet').returns('Hi!');

obj.greet('Alice');

console.log(stub.called);        // true
console.log(stub.calledWith('Alice')); // true
console.log(obj.greet('Bob'));   // "Hi!" —— 完全被替换了

stub.restore();  // 清理

Sinon还有一个强大的特性叫做Stub,它比Jest的Mock更灵活:

代码语言:javascript
复制
const stub = sinon.stub(obj, 'fetchData');

// 可以指定返回值
stub.withArgs('success').returns({ data: 'ok' });

// 可以指定不同参数返回不同值
stub.withArgs('error').throws(new Error('服务器错误'));

// 可以链式调用
stub.onFirstCall().returns(1);
stub.onSecondCall().returns(2);
obj.fetchData() // 返回 1
obj.fetchData() // 返回 2

第四部分:真实场景—行为驱动设计

现在来看一个真正复杂的场景,说明Mock和Spy怎么配合使用才能写出有意义的测试。

假设你在做一个订单系统:

代码语言:javascript
复制
export class OrderService {
constructor(paymentAPI, emailService, logger) {
    this.paymentAPI = paymentAPI;
    this.emailService = emailService;
    this.logger = logger;
  }

async placeOrder(order) {
    try {
      this.logger.log(`订单创建: ${order.id}`);
      
      // 步骤1:调用支付API
      const payment = awaitthis.paymentAPI.charge(order.amount);
      
      // 步骤2:如果支付成功,发邮件通知
      if (payment.success) {
        awaitthis.emailService.sendOrderConfirmation(order.id);
      }
      
      this.logger.log(`订单完成: ${order.id}`);
      return { success: true };
    } catch (error) {
      this.logger.error(`订单失败: ${error.message}`);
      throw error;
    }
  }
}

如果用过度Mock的方式测试,你会写出这样的代码(这是坏的):

代码语言:javascript
复制
// ❌ 不推荐的做法:过度Mock
test('订单流程应该调用支付API', () => {
const paymentAPI = { charge: jest.fn(() =>Promise.resolve({ success: true })) };
const emailService = { sendOrderConfirmation: jest.fn() };
const logger = { log: jest.fn(), error: jest.fn() };

const service = new OrderService(paymentAPI, emailService, logger);

  service.placeOrder({ id: 1, amount: 100 });

// 只在乎是否被调用了,不在乎真实的业务逻辑
  expect(paymentAPI.charge).toHaveBeenCalled();
  expect(emailService.sendOrderConfirmation).toHaveBeenCalled();
});

这个测试的问题:

  1. 没有验证调用的参数是否正确
  2. 没有验证订单的实际状态变化
  3. 没有测试支付失败的情况
  4. 没有验证正确的调用顺序

正确的做法应该是这样的(称为行为驱动):

代码语言:javascript
复制
// ✅ 推荐的做法:行为驱动
test('订单完成时应该发送确认邮件', () => {
const paymentAPI = { charge: jest.fn(() =>Promise.resolve({ success: true })) };
const emailService = { sendOrderConfirmation: jest.fn(() =>Promise.resolve()) };
const logger = { log: jest.fn(), error: jest.fn() };

const service = new OrderService(paymentAPI, emailService, logger);

// 关键:准备一个真实的订单对象
const order = { id: '12345', amount: 100, email: 'customer@example.com' };

// 执行业务逻辑
const result = await service.placeOrder(order);

// 验证外部行为
  expect(result.success).toBe(true);

// 验证支付API被正确调用(参数重要)
  expect(paymentAPI.charge).toHaveBeenCalledWith(100);

// 验证确认邮件被发送(只在支付成功时)
  expect(emailService.sendOrderConfirmation).toHaveBeenCalledWith('12345');

// 验证日志记录(用Spy可以加强这一点)
  expect(logger.log).toHaveBeenCalledWith('订单创建: 12345');
  expect(logger.log).toHaveBeenCalledWith('订单完成: 12345');
});

test('支付失败时不应该发送邮件', async () => {
const paymentAPI = { 
    charge: jest.fn(() =>Promise.reject(newError('支付被拒'))) 
  };
const emailService = { sendOrderConfirmation: jest.fn() };
const logger = { log: jest.fn(), error: jest.fn() };

const service = new OrderService(paymentAPI, emailService, logger);
const order = { id: '12345', amount: 100 };

// 执行
try {
    await service.placeOrder(order);
  } catch (e) {
    // 预期会抛异常
  }

// 关键验证:邮件不应该被发送
  expect(emailService.sendOrderConfirmation).not.toHaveBeenCalled();

// 验证错误被正确记录
  expect(logger.error).toHaveBeenCalledWith(expect.stringContaining('支付被拒'));
});

看这个例子,模拟外部依赖(paymentAPI、emailService)是必要的,因为它们有副作用。但我们验证的不是"这些方法被调用了",而是"在订单成功的情况下邮件被发送了,在支付失败的情况下没有发送"——这是真实的行为。

第五部分:常见的测试陷阱

陷阱1:Mock泄露了实现细节

代码语言:javascript
复制
// ❌ 坏的例子
exportfunction calculateTotal(items) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
const total = subtotal + tax;
return total;
}

test('计算总价', () => {
const items = [{ price: 100 }, { price: 50 }];
  expect(calculateTotal(items)).toBe(165);
// 这个测试很脆弱,如果你后来改了税率计算逻辑,测试就会失败
});

陷阱2:Spy没有清理导致污染其他测试

代码语言:javascript
复制
// ❌ 坏的例子
test('第一个测试', () => {
const spy = jest.spyOn(console, 'log');
console.log('hello');
  expect(spy).toHaveBeenCalled();
// 忘记调用 spy.mockRestore()
});

test('第二个测试', () => {
console.log('world');
// 第一个测试的spy仍然在生效,可能导致意外结果
});

陷阱3:Mock隐藏了真实问题

代码语言:javascript
复制
// ❌ 坏的例子
jest.mock('./database', () => ({
  query: jest.fn(() => Promise.resolve([]))
}));

test('用户查询', async () => {
  const result = await userService.findById(1);
  expect(result).toBeDefined();
});

// 这个测试完全没有测试SQL构建的正确性
// 在真实数据库中可能会因为ORM的问题而失败

陷阱4:过度信任Mock的返回值

代码语言:javascript
复制
// ❌ 坏的例子
const apiMock = jest.fn(() => ({ 
userId: 123, 
name: 'John'
}));

test('API返回用户信息', () => {
const result = apiMock();
  expect(result.userId).toBe(123);
// 这个测试其实在测Mock本身,不是在测你的代码
});

第六部分:实战建议—何时用Mock,何时用Spy

让我总结一个决策树,帮你在真实项目中选择:

代码语言:javascript
复制
你需要验证一个函数吗?
  │
  ├─ 需要隔离外部依赖(API、数据库、文件系统)?
  │   └─ 是 → 用 Mock
  │       理由:外部依赖有副作用,不能真的调用
  │
  ├─ 你想保持真实实现但需要验证调用方式?
  │   └─ 是 → 用 Spy
  │       理由:关心的是怎么被调用,不想改变行为
  │
  └─ 你想测试完整的集成流程?
      └─ 是 → 最少化Mock,只Mock最外层的依赖
          理由:太多Mock会让测试脱离现实

一个核心原则:Mock应该Mock你不拥有的代码(第三方库、外部服务),Spy应该Spy你自己的代码

第七部分:性能和可维护性思考

到这里,我想讨论一个更深层的问题:过度的Mock和Spy会让你的测试成为"假的通过"。

考虑这个场景:你有一个复杂的用户注册流程,涉及验证邮箱、检查用户名、创建账户、发送欢迎邮件。如果你Mock了所有外部调用:

代码语言:javascript
复制
// 假如这样写
jest.mock('./emailService');
jest.mock('./database');
jest.mock('./usernameValidator');

test('用户注册', () => {
  // 所有依赖都被Mock了,测试"通过"了
  // 但真实场景中数据库的schema改了,你根本发现不了
});

一个更健康的方案是:

  • 在单元测试中,Mock外部的、不可控的服务(邮件API、支付网关)
  • 在集成测试中,使用真实的数据库(或者测试数据库)
  • 保持一些端到端测试,验证完整流程
代码语言:javascript
复制
        单元测试          集成测试         E2E测试
         ─────────────────────────────────────
Mock程度  高              中              低
速度     很快            中等            较慢
真实性   低              中              高
成本     低              中              高

现代开发中的最佳实践是找到一个平衡点。不要所有东西都Mock(那样的测试毫无意义),也不要什么都不Mock(那样的测试会很慢)。

最后一个真实案例

我来分享一个从我的真实项目中提取出来的例子。这是一个用户认证模块的测试:

代码语言:javascript
复制
// auth.service.js
exportclass AuthService {
constructor(userRepository, emailService, jwtService) {
    this.userRepository = userRepository;
    this.emailService = emailService;
    this.jwtService = jwtService;
  }

async register(email, password) {
    // 验证邮箱格式(这是我自己的代码,应该真实测试)
    if (!this.isValidEmail(email)) {
      thrownewError('邮箱格式不对');
    }

    // 检查邮箱是否已存在(涉及数据库,可以Mock)
    const exists = awaitthis.userRepository.findByEmail(email);
    if (exists) {
      thrownewError('邮箱已注册');
    }

    // 创建用户(涉及数据库,可以Mock)
    const user = awaitthis.userRepository.create({ email, password });

    // 发送确认邮件(涉及外部服务,应该Mock)
    awaitthis.emailService.sendVerificationEmail(email);

    // 生成token(这是我自己的代码,应该真实测试)
    const token = this.jwtService.sign({ userId: user.id });

    return { user, token };
  }

  isValidEmail(email) {
    // 实现邮箱验证逻辑
    return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

这是一个正确的测试写法:

代码语言:javascript
复制
describe('AuthService', () => {
let service;
let userRepository;
let emailService;
let jwtService;

  beforeEach(() => {
    // Mock数据库和邮件服务
    userRepository = {
      findByEmail: jest.fn(),
      create: jest.fn()
    };
    emailService = {
      sendVerificationEmail: jest.fn()
    };
    // 注意:jwtService是我们自己的代码,不Mock
    jwtService = new JwtService('secret');

    service = new AuthService(userRepository, emailService, jwtService);
  });

  test('邮箱格式验证失败应该抛异常', async () => {
    await expect(service.register('invalid-email', 'password'))
      .rejects.toThrow('邮箱格式不对');
    
    // 关键:不应该调用任何依赖
    expect(userRepository.findByEmail).not.toHaveBeenCalled();
  });

  test('邮箱已存在应该抛异常', async () => {
    userRepository.findByEmail.mockResolvedValue({ id: 1 });

    await expect(service.register('test@example.com', 'password'))
      .rejects.toThrow('邮箱已注册');
    
    // 验证查询了数据库
    expect(userRepository.findByEmail).toHaveBeenCalledWith('test@example.com');
    // 但不应该创建新用户
    expect(userRepository.create).not.toHaveBeenCalled();
  });

  test('注册成功应该创建用户并发送邮件', async () => {
    const newUser = { id: 1, email: 'test@example.com' };
    userRepository.findByEmail.mockResolvedValue(null);
    userRepository.create.mockResolvedValue(newUser);

    const result = await service.register('test@example.com', 'password');

    // 验证用户被创建
    expect(userRepository.create).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password'
    });

    // 验证邮件被发送
    expect(emailService.sendVerificationEmail)
      .toHaveBeenCalledWith('test@example.com');

    // 验证返回了正确的结果(包括token)
    expect(result.user).toEqual(newUser);
    expect(result.token).toBeDefined();
    // token应该包含正确的userId
    const decoded = jwtService.verify(result.token);
    expect(decoded.userId).toBe(1);
  });

  test('邮件发送失败应该触发异常', async () => {
    userRepository.findByEmail.mockResolvedValue(null);
    userRepository.create.mockResolvedValue({ id: 1 });
    emailService.sendVerificationEmail.mockRejectedValue(
      newError('邮件服务故障')
    );

    await expect(service.register('test@example.com', 'password'))
      .rejects.toThrow('邮件服务故障');
  });
});

看这个例子:

  • ✅ 用Mock隔离了数据库和邮件服务(它们有副作用)
  • ✅ 没有Mock jwtService(这是我们自己的代码,应该真实测试)
  • ✅ 验证的是真实的业务行为,不是实现细节
  • ✅ 测试了成功和失败两种路径
  • ✅ 清楚地表达了"当A时,B应该发生"

总结与反思

我在最开始提到的那个bug,其实就是因为他的测试Mock了sendEmail但从来没验证过在邮件服务异常时是怎么处理的。他的代码根本没有任何错误处理逻辑,但因为Mock隐藏了这一点,测试通过了。

Mock和Spy是强大的工具,但"强大的工具往往被滥用"。关键是:

  1. 理解你在Mock什么:是在隔离依赖,还是在欺骗自己?
  2. 验证真实的行为:不是验证实现细节,而是验证在特定条件下应该发生什么
  3. 保持平衡:既要Mock外部依赖,也要保留足够的真实性
  4. 定期反思:如果测试太容易通过,那可能是Mock太多了

如果你的测试只验证了函数被调用了几次,但从没验证过返回值是否正确、是否处理了异常、是否满足了业务逻辑,那你写的不是测试,是自欺欺人。

如果这篇文章对你有帮助,请别忘记点赞、分享和推荐给更多需要的开发者。 让我们一起把更多想要提升测试水平的程序员聚集在这里,形成一个真正思考技术本质的社区。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 核心问题:Mock和Spy到底在做什么?
  • 第一部分:Mock—完全替换,绝对隔离
    • Mock的第一个坑:过度隔离
    • Mock的第二个坑:测试实现细节而不是行为
    • Mock的第三个坑:模拟返回值时不够真实
  • 第二部分:Spy—保留原貌,植入观察者
  • 第三部分:Jest vs Sinon—两大测试库对比
    • Jest的风格:自动化Mock
    • Sinon的风格:手动控制
  • 第四部分:真实场景—行为驱动设计
  • 第五部分:常见的测试陷阱
    • 陷阱1:Mock泄露了实现细节
    • 陷阱2:Spy没有清理导致污染其他测试
    • 陷阱3:Mock隐藏了真实问题
    • 陷阱4:过度信任Mock的返回值
  • 第六部分:实战建议—何时用Mock,何时用Spy
  • 第七部分:性能和可维护性思考
  • 最后一个真实案例
  • 总结与反思
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档