某些请求在离开你的机器之前需要进行一些处理,而有些请求则需要在响应到达的那一刻进行操作。例如,支付 API 可能需要根据时间戳和你的密钥计算 HMAC 签名;登录接口会返回一个后续所有调用都需要用到的 Token;结账流程必须断言响应确实返回了 200 状态码和正确的订单 ID。你可以手动完成所有这些操作,但很快就会变得繁琐,而且一旦你与团队成员共享请求,这种方式就会失效。
脚本可以解决这些问题。在 Apifox 中,你可以为请求附加一小段自动运行的 JavaScript 代码:一组在请求发送前运行,另一组在响应返回后运行。如果你以前写过 Postman 脚本,那么这些经验可以无缝迁移,因为 Apifox 的引擎与相同的 pm 对象 API 兼容。本指南将通过一个真实的示例带你了解这两个阶段:在前置脚本中对请求进行签名,然后在后置脚本中提取 Token。这些行为在 Apifox 脚本编写文档中有详细记录,但标签名称和一些行为与 Postman 有所不同,这些差异非常重要。
前置操作和后置操作的具体作用
Apifox 在两个阶段运行脚本,并对其进行了清晰的命名。
前置操作在请求发送到服务端之前运行。这是你进行准备工作的地方:生成时间戳、计算签名、设置随机订单 ID,或者读取变量并将其格式化为 header。此时响应尚不存在,因此任何检查响应的操作在这里都是无效的。
后置操作在收到响应后运行。这是你通过断言验证返回内容的地方,也是你从 body 中提取值以便后续复用的地方。例如提取 auth Token、获取新创建的资源 ID、检查状态码、保存用于分页的游标。
基于这种划分,有两条规则:首先,pm.response(及其属性 code、status、headers、responseTime、responseSize、text() 和 json())仅在后置操作中有效。在发送之前没有响应可读,因此在前置操作中调用它没有意义。其次,变量是这两个阶段相互通信的桥梁。前置操作设置一个值,请求使用它,而后置操作可以读取或覆盖它。
如果你是从 Postman 迁移过来的,请先注意标签名称的变化。Apifox 的标签是前置操作和后置操作,而不是 “Pre-request Script” 和 “Tests”。虽然行为非常接近,但屏幕上的名称是不同的。
设置:打开请求并找到标签页
下载 Apifox 以跟随本教程。它是免费的,支持 macOS、Windows 和 Linux,如果你还没有安装,请从 下载 Apifox 页面获取。
在 Apifox 中打开你想要编写脚本的 API 请求。每个接口除了常规的 Params、header 和 body 标签外,都有前置操作标签和后置操作标签。要在这两个阶段添加逻辑,请打开相应标签并选择添加自定义脚本。这将打开一个代码编辑器,你可以针对 pm 对象编写纯 JavaScript 代码。
在编写任何代码之前,了解变量的存储位置很有帮助。Apifox 按以下优先级顺序解析变量:
临时变量 > 环境变量 > 项目内共享的全局变量 > 团队内共享的全局变量。
因此,临时变量会覆盖同名的环境变量,依此类推。当值不符合预期时,请记住这一点:优先级较高的变量可能遮蔽了它。如果你希望值在多个请求中持久存在,Apifox 中的全局参数优先级较低,是存放稳定的、项目范围设置的理想位置。
前置操作示例:使用 HMAC 对请求进行签名
假设你调用一个支付接口,该接口使用 HMAC-SHA256 签名对每个请求进行鉴权。这与许多服务商用于 webhook 验证的模式相同,Stripe 的签名文档对此有很好的描述:服务端期望一个时间戳以及基于该时间戳加请求 body 计算出的签名,并使用你的 API secret 作为密钥。你需要在每次发送时重新生成这两个值。
Apifox 将 crypto-js 作为内置库提供,因此你无需安装任何内容。打开前置操作标签,选择添加自定义脚本,并编写以下代码:
// Pre Processor: sign the request before it is sent
const CryptoJS = require('crypto-js');
// current unix timestamp in seconds
const timestamp = Math.floor(Date.now() / 1000).toString();
// read the secret from an environment variable
const secret = pm.environment.get('payments_api_secret');
// build the string to sign: timestamp + newline + raw body
const body = pm.request.body ? pm.request.body.toString() : '';
const payload = timestamp + '\n' + body;
// compute the HMAC-SHA256 signature, hex encoded
const signature = CryptoJS.HmacSHA256(payload, secret).toString(CryptoJS.enc.Hex);
// stash both values as environment variables for the request to use
pm.environment.set('x_timestamp', timestamp);
pm.environment.set('x_signature', signature);
pm.console.log('Signed request at ' + timestamp);
该脚本计算签名并将时间戳和签名都保存到环境变量中。现在将它们关联到请求中。在 header 标签中,使用 {{variableName}} 语法引用存储的值:
X-Timestamp: {{x_timestamp}}
X-Signature: {{x_signature}}
发送请求时,Apifox 会首先运行**前置操作**,设置这两个**变量**,然后将它们替换到 **header** 中。这样服务端每次都能接收到发送时有效的签名,无需手动操作。请注意,`require('crypto-js')` 调用会引入整个模块。你需要导入整个库而不是子模块,因此 `require('crypto-js')` 可以工作,而 `require('crypto-js/sha256')` 则不行。
需要注意的一点是:**变量**操作仅触及**本地值**,而不会修改你可能在**环境**编辑器中输入的**远程值**。这正是你所需要的,因为签名应该是临时性的。如果你也需要了解 Postman 端的签名模式,关于 Postman pre-request scripts 的指南涵盖了相同的概念,并且可以完美映射到 Apifox 的**前置操作**中。
## 后置操作示例:提取 Token 并断言
现在来看另一个阶段。想象一个登录**请求**返回了后续所有鉴权调用所需的 **Token**:
json { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…", "user": { "id": 4812, "email": "dana@example.com" }, "expires_in": 3600 }
打开**后置操作**标签页,选择**添加自定义脚本**,从 **body** 中提取 **Token** 并保存供后续**请求**使用:
javascript // 后置操作:校验响应,然后提取 Token pm.test('Status is 200', function () { pm.response.to.have.status(200); });
const jsonData = pm.response.json();
pm.test('Response returns a token', function () { pm.expect(jsonData.token).to.be.a('string').and.not.empty; });
pm.test('User id is present', function () { pm.expect(jsonData.user.id).to.be.a('number'); });
// 存储 Token 以便其他请求发送 pm.environment.set('auth_token', jsonData.token); pm.console.log('Saved token for user ' + jsonData.user.email);
这里发生了两件事。`pm.test()` 代码块使用 Apifox 原生支持的 Chai 风格 `pm.expect()` 匹配器来**断言****响应**格式符合预期。而 `pm.environment.set('auth_token', jsonData.token)` 将 **Token** 保存到**环境**中。此后任何**请求**都可以直接发送 `Authorization: Bearer {{auth_token}}`,无需手动复制任何内容。
**断言**部分本身也值得关注。良好的响应后检查能将手动点击查看转变为真正的**测试**,我们的 Apifox 接口**断言**指南深入探讨了值得掌握的匹配器和模式。如果你还想在这些流程中注入真实的伪造数据,Apifox 中的 Faker.js 与上述**变量**设置功能配合得非常好。
在**后置操作**中有几点行为需要理清:`pm.iterationData`(你的**测试**数据)是只读的,因此你可以读取数据驱动的值,但不能通过**脚本**回写;`pm.cookies` 返回的是来自**响应**的 cookie(即服务端返回的 cookie),而不是随**请求**一起发送的 cookie。
## 使用公共脚本复用逻辑
一旦你编写了那个 HMAC 签名代码块,你可能希望在多个请求中使用它。将其复制粘贴到 10 个接口中意味着当算法改变时,需要修改 10 个地方。Apifox 的解决方案是**公共脚本**:只需编写一次即可在需要处附加的可复用代码片段。
在**设置 > 公共脚本**下创建一个脚本,然后将其添加到请求的**前置操作**或**后置操作**页签中。在操作列表中,公共脚本和自定义脚本并排显示,且顺序很重要:在同一个列表中,公共脚本会在自定义脚本之前执行;如果你有多个公共脚本,它们会按照列表顺序从上到下运行。
这里有一个需要注意的一点。如果你想在自定义脚本中调用公共脚本中定义的函数,该函数必须是全局的。普通的 `function` 声明或 `const` 绑定的函数仅在其所属脚本内有效(局部),对后续脚本不可见。通过不使用 `var`、`let` 或 `const` 的赋值方式来声明它:
javascript // In the Public Script: make sign() global by omitting the keyword sign = function (payload, secret) { const CryptoJS = require('crypto-js'); return CryptoJS.HmacSHA256(payload, secret).toString(CryptoJS.enc.Hex); };
javascript // In the Custom Script below it: call the global function by name const timestamp = Math.floor(Date.now() / 1000).toString(); const secret = pm.environment.get('paymentsapisecret'); pm.environment.set('xtimestamp', timestamp); pm.environment.set('xsignature', sign(timestamp, secret));
先添加公共脚本,再将自定义脚本放在其下方,调用即可生效。如果顺序错误或添加了 `const`,你将会收到函数未定义的错误。
## 库、外部包与调试
上述 `crypto-js` 的导入是 Apifox 内置的一系列无需配置即可使用的库之一。你可以直接 `require()` 其中的任何一个:
* [`crypto-js`](https://github.com/brix/crypto-js?ref=apifox.com) (v3.1.9-1) 用于哈希和 HMAC
* `jsrsasign` (v10.3.0) 用于 JWT 和 RSA 相关工作,需要 Apifox 1.4.5 或更高版本
* [`chai`](https://www.chaijs.com/?ref=apifox.com) (v4.2.0) 用于断言匹配器
* `lodash`、`moment`、`uuid`、`xml2js`、`cheerio`、`postman-collection`、`atob`、`btoa`、`csv-parse/lib/sync`、`tv4`、`ajv`
* Node 内置模块,如 `path`、`assert`、`buffer`、`util`、`url`、`querystring`、`stream` 和 `events`
如果你需要列表之外的内容,可以在运行时使用 `$$.liveRequire()` 动态引入,它会即时获取包:
javascript $$.liveRequire('nanoid', (nanoid) => { const id = nanoid.nanoid(); pm.environment.set('request_id', id); });
这种方式需要互联网连接,因为 Apifox 会在脚本运行时下载该包。内置库则不需要。
当脚本运行异常时,可以通过日志来排查。`pm.console.log()` 和普通的 `console.log()` 都会打印到 Apifox 的控制台,因此你可以打印计算出的签名或提取的字段,以便在请求发出前或返回后准确查看脚本生成的内容。
还有两个值得注意的限制,以免让你感到困惑。用于在脚本中发起额外 HTTP 调用的 `pm.sendRequest()` 使用的是回调模式而非 Promise,因此请使用回调函数编写,而不是 `await`。此外,Apifox 不支持用于链接请求的 Postman `pm.nextRequest()`。当你需要真正的带有分支和条件步骤的工作流编排时,Apifox 会使用**测试场景**,你可以通过“条件”和“If-Else”步骤可视化地编排请求序列。如果你经常编写脚本,内置的 Apifox 脚本生成器也可以根据自然语言描述起草脚本,为你提供一个起点。
## 使用 Apifox CLI 自动化工作流
脚本不仅在你点击“发送”时运行。当你将请求和断言封装到保存的**测试场景**中时,Apifox CLI 会以无头模式运行整个场景,包括**前置操作**和**后置操作**,这使得你的签名和 Token 提取逻辑成为 CI 的一部分,而不是手动步骤。
安装 CLI 并进行身份验证,然后通过 ID 运行场景:
bash npm install -g apifox-cli apifox login --with-tokenapifox run --access-token $APIFOXACCESSTOKEN -t-e-r cli ```
-t 参数是测试场景 ID,-e 是环境 ID,-r 用于选择报告器(cli、html 或 junit,多个值用逗号分隔)。在 Apifox 账号设置中生成访问令牌,并将其导出为 APIFOX_ACCESS_TOKEN,这样原本在桌面版上运行的场景现在就可以在 CI 中运行,且包含前置操作和后置操作。
一个诚恳的提醒:如果脚本依赖于仅存在于你本地机器上的东西(例如本地文件或你曾经加载过的包),它可能会在桌面版中通过,但在 CLI 中失败,因为 runner 没有该依赖项。请确保脚本仅使用内置库或 $$.liveRequire(),以便它们在任何地方都能运行一致。
常见问题
Apifox 脚本是否兼容我现有的 Postman 脚本?
大部分兼容。Apifox 的引擎使用相同的 pm 对象 API,因此 pm.environment.set()、pm.response.json()、pm.test() 和 pm.expect() 的行为都与你熟悉的一致。需要记住的两个区别是页签名称:是前置操作和后置操作,而不是 Pre-request Script 和 Tests;以及一些不支持的调用,如 pm.nextRequest()。大多数脚本直接粘贴即可运行。
为什么 pm.response 在我的前置脚本中返回 undefined?
因为此时还没有响应。前置操作在请求发送之前运行,所以没有返回内容可供检查。任何读取 pm.response(其状态、body、headers)的代码都属于后置操作。如果你在预处理阶段需要某个值,请改为从 pm.request、变量或库中获取。
如何在多个请求之间共享同一个脚本?
使用“设置 > 公共脚本”下的公共脚本。编写一次逻辑,将其添加到每个请求的前置操作或后置操作标签页中。请记住,在同一个列表中,公共脚本会在自定义脚本之前运行。要从自定义脚本调用公共脚本函数,请通过不使用 var、let 或 const 赋值来将其声明为全局变量。
我可以导入 Apifox 未内置的 npm 包吗?
可以,通过 $$.liveRequire('package-name', (pkg) => { ... }) 实现,它会在运行时下载该包,因此需要互联网连接。对于内置列表中的任何内容(如 crypto-js、moment 或 uuid),请使用普通的 require(),无需联网。请注意,你只能 require 整个模块,而不能 require 子模块路径。
我在哪里可以查看脚本打印的内容?
使用 pm.console.log() 或 console.log(),并在发送请求后在 Apifox 的控制台中查看输出。这是在测试场景中正式使用之前,确认签名计算或 Token 提取是否正确的最快方法。
总结
前置操作和后置操作将静态请求转变为能够自我准备和自我检查的动态请求。在发送前签名,在接收后提取并断言,并将共享逻辑提取到公共脚本中,实现一次编写多次复用。pm API 和内置库意味着你从 Postman 学到的大部分知识都可以直接迁移。打开 Apifox,选择任一请求,添加你的第一个自定义脚本,即可在单次发送中观察这两个阶段的运行。免费开始使用,无需信用卡。