尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

前端接口抓取与Mock数据实战:Network面板、跨域代理和分页鉴权

前端接口抓取与Mock数据实战:Network面板、跨域代理和分页鉴权 上周帮一个做后台管理系统的朋友看代码他花了两天把仪表盘、表格、筛选器全画完了点进去却是一片“暂无数据”。后端同学还在排期他卡在这里既没法验收交互也没法截图放进作品集。我给的建议很直接先去目标页面上把接口的请求结构扒下来用本地代理转发再补一层贴近真实的假数据。半小时后他的表格里就有了十几行像模像样的记录分页、搜索、空状态全都能跑通。前端项目缺数据卡住的从来不只是调试。它还卡住了组件状态测试、接口字段对齐、加载与错误分支的验证以及作品集最后一次演示效果。这篇内容就是围绕前端、API、抓取这三件事展开的怎么从浏览器里看清一个真实请求怎么把它复现成自己能用的代码怎么处理跨域、鉴权、分页和签名这些拦路虎以及数据拿到之后该往哪放。适合手上有半成品项目、被数据卡住的前端同学也适合想搞明白“请求到底是怎么回事”的初学者。1. 页面空着不动数据来源其实只有三条路绝大多数人遇到“没数据”的第一反应是等后端这没错但很低效。真正在一线推进过项目的人脑子里通常同时握着三条路按场景挑一条走。第一条是自己造 Mock 数据。字段名、数量、边界值都由你定改起来最快缺点是容易脱离真实接口的形态等真接口上来一联调字段对不上、分页对不上、时间格式对不上返工一大片。第二条是用公开的开放接口。很多平台都提供面向开发者的公开数据比如天气、汇率、节假日、车牌限行这类注册拿个 key 就能用。它的好处是数据真实、有文档、有错误码能用来练真实请求链路坏处是数据内容固定和你的业务字段大概率对不上得写一层转换。第三条是从已有网页的请求里把接口结构摸清楚在本地复现出同样的请求。这条最能锻炼人因为你会看到一个接口是怎么被设计出来的参数怎么命名、鉴权怎么带、分页用什么形式、响应包成什么形状。学会这一套你不光能解决自己项目的假数据问题还能在评审会上听明白后端在说什么。需要提前把话说清楚下面讲的所有内容前提都是用于个人学习、本地调试、字段对齐抓取的对象应当是公开可见、不涉及个人隐私、不违反目标站点使用条款的数据。不要拿它去批量获取内容、不要采集个人信息、不要把别人家的数据原样重新分发。这是底线越过去性质就变了。很多人对第三条有误解以为要写爬虫、要装一堆库。其实对前端来说最省事的入口就在浏览器按下 F12 之后。接下来的内容就从那个 Network 面板讲起。2. 把一次真实请求拆到骨头里浏览器开发者工具里的 Network 面板是前端最被低估的一个工具。不少人只用它看过“接口报没报错”实际上它把一次请求的每一个组成部分都完整摊开了。2.1 Network 面板上先用起来的四个开关打开面板之后先别急着看内容把这几个开关处理掉效率会差好几倍。筛选器切到 Fetch/XHR。默认是 All会被一堆图片、字体、脚本淹没。切到 Fetch/XHR 之后只剩数据请求列表瞬间清爽。勾上 Preserve log保留日志。页面一跳转或者一刷新请求列表默认会被清空。做登录流程、跳转流程时不勾这个你会什么都抓不到。勾上 Disable cache禁用缓存。有时候你改了参数却拿到老结果就是缓存捣的鬼。调试阶段一律关掉。把时间线打开。请求是按时间顺序排的你可以直观看到哪个接口先发、哪个接口后发、哪些是并发的。很多页面的数据依赖关系看时间线比看代码快。顺手提一个经验如果列表里请求特别多可以在筛选框里直接输入关键字比如list、search、page按 URL 猜名字去过滤比一条条翻快得多。真实的接口命名大多有规律摸两下就能猜到。2.2 一条请求的五个关键区域点开任意一条请求右侧会出现几个标签页每个都有明确用途。我把它整理成一张表照着看就行。区域里面有什么你要拿走什么General请求方法、完整 URL、状态码、远端地址请求方式GET/POST、完整路径、是否有查询串Request Headers浏览器发出去的请求头鉴权字段、Content-Type、Referer、自定义头Payload / Query String请求体或查询参数业务参数的命名、类型、必填项Response Headers服务端返回的响应头跨域头、缓存策略、Cookie 写入Response / Preview原始响应与格式化预览数据层级结构、字段名、分页字段位置这里面最容易被忽略的是Request Headers。很多人复制完 URL 和参数就跑结果一调就 403问题往往就出在少了某个自定义头。比如有些接口会把身份放在Authorization里有些放在X-Token里还有些直接依赖Cookie。这些细节只在这一栏能看到。Payload 那一栏也值得多看两眼。同一个查询有的站点用 GET 拼查询串有的站点用 POST 把参数塞进 JSON body。后者在浏览器里看起来不太起眼但你复现的时候如果照样用 GET接口很可能返回参数校验失败。判断方式很简单看 General 里的 Request Method。2.3 一键复现Copy as cURL 是最高效的入口看完结构之后不用手敲。在请求上右键找到 Copy 菜单里面有三个特别有用的选项。Copy as cURL把这条请求完整地转成一条命令行。粘到终端里直接回车就能看到结果。它包含 URL、方法、全部请求头和请求体是保真度最高的一种复制。Copy as fetch直接给出一段浏览器风格的fetch代码粘到控制台或者你的项目里就能跑。Copy as Node.js fetch给出一段 Node 环境下可用的代码适合你打算在本地起个中转服务的时候用。拿到 cURL 之后我一般会先做一次“减法”。先把它精简到只保留必要部分再用它验证接口是否还通。举个例子的形态curl -s https://api.example.com/v1/list?page1size20 \ -H accept: application/json \ -H authorization: Bearer eyJhbGciOi... \ -H referer: https://www.example.com/ \ --compressed把它们粘到终端跑一下如果能正常返回 JSON说明鉴权头和 Referer 都还有效。下一步再逐条删头看删到哪个会挂你就知道哪些是必须的、哪些是浏览器自动加的。这个过程看起来笨但它能让你彻底搞懂这个接口到底依赖什么比盲目试错靠谱得多。一个提醒从 cURL 里带出来的authorization、cookie这类字段是真实凭据不要提交到 Git 仓库不要贴到公开的问答网站也不要写死在会被打包发布的前端代码里。放到本地的.env文件加进.gitignore这是基本习惯。3. 复制出来能跑放进代码就挂跨域与代理层这是新手遇到的第一个大坎。明明在 cURL 里跑得好好的请求写进前端代码一调控制台红着一片提示被跨域策略拦截。搞清楚这里面的机制比记住某个配置重要得多。3.1 浏览器能通、代码不通的真实原因关键在于同源策略。浏览器为了安全规定页面只能自由请求和自己“同源”的资源。同源的意思是协议、域名、端口三者完全一致。你的项目跑在http://localhost:5173目标接口在https://api.example.com协议不同、域名不同就是跨源。这里有个特别容易让人困惑的点为什么在地址栏直接输 URL 能打开在 Network 面板里也能看到别人站点在正常请求因为同源策略是浏览器对脚本发起的请求施加的限制不是对资源本身。页面自己能加载跨域图片、脚本但你的 JS 代码用fetch去读别人接口返回的内容时浏览器会检查对方有没有主动允许。允许的方式是服务端在响应头里带上Access-Control-Allow-Origin之类的字段。如果对方没带你的前端代码就必然被拦。这是设计如此不是 bug。关于这里常见的一个误区我想多说一句有人以为关掉浏览器安全策略就能解决。确实可以但那只解决了你一个人本地调试的问题一旦部署、一旦给别人看照样挂。而且这种做法会让你的项目一直带着一个不健康的依赖。3.2 用开发服务器代理绕开同源限制正确的做法是在开发阶段用代理。原理不复杂你的前端代码不去请求远程接口而是请求自己开发服务器上的一个路径比如/api/list开发服务器收到之后由它在服务端去请求真正的远程地址再把结果原样吐回来。因为这一步发生在服务端不受浏览器同源策略约束。Vite 项目的配置长这样// vite.config.js import { defineConfig } from vite export default defineConfig({ server: { proxy: { /api: { target: https://api.example.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, })这几个字段都值得解释一下不然出了问题是懵的。target是真正要转发到的地址。注意它后面不要带/api因为路径拼接由rewrite决定。changeOrigin: true会把发出去的请求头里的Host改成目标域名。很多站点会校验 Host不设置这个容易被拒。rewrite把路径前缀去掉。你前端写/api/list转发过去变成https://api.example.com/list。Webpack 的写法思路一致只是配置名不同// webpack.config.js module.exports { devServer: { proxy: { /api: { target: https://api.example.com, changeOrigin: true, pathRewrite: { ^/api: }, }, }, }, }代码里的fetch就可以写得很干净const res await fetch(/api/list?page1size20) const data await res.json() console.log(data)3.3 代理能解决的和代理解决不了的代理很好用但它不是万能钥匙。把边界划清楚能少走很多弯路。问题类型代理能否解决说明浏览器跨域拦截能请求发生在服务端不受同源策略约束Referer 校验部分能需要额外设置请求头伪装来源但不建议滥用Host 校验能靠changeOrigin处理凭据过期不能那是鉴权问题得重新获取有效凭据请求签名校验不能需要按算法重新生成签名参数目标站点限流封禁不能频率过高照样被拒只能控制节奏这张表建议存下来。以后遇到“代理配了还是不通”从上往下对照一遍能很快定位到是哪一类问题。我见过太多人把所有失败都归咎于跨域然后在代理配置里反复折腾其实请求早就打过去了只是被对方拒绝。4. 让请求真正可用凭据、分页、签名与类型代理解决的是“能不能发出去”接下来要解决的是“发出去能不能拿到想要的”。这一步涉及几个绕不开的点每一个都有它的脾气。4.1 三种常见的身份携带方式看 Request Headers 的时候身份信息基本逃不出这三种形态。第一种是Cookie。浏览器会自动带上你在 cURL 里也能看到cookie: xxx这一行。它的特点是会过期、会和会话绑定而且有的站点把关键身份放在HttpOnly的 Cookie 里脚本根本读不到。第二种是Bearer Token。通常在authorization: Bearer xxx这一行也有的站点用自定义头比如x-token、x-auth。这类凭据一般有明确的有效期过期后接口会返回 401。第三种是在查询串或请求体里带 token。这种相对少见但它有个明显问题——凭据会出现在日志和 URL 里不安全遇到时可以判断这个接口设计得比较随意。实操上的经验是把凭据放进.env通过构建变量注入。const token import.meta.env.VITE_API_TOKEN const res await fetch(/api/list, { headers: { authorization: Bearer ${token} }, })在项目根目录建一个.env.localVITE_API_TOKEN替换成你自己的值然后把.env.local加进.gitignore。这一步别偷懒我见过不止一次凭据被提交到公开仓库的事故。4.2 分页的三种形态看字段就能认出来分页是复现接口时最容易漏的一环。很多人复制了第一页的请求看着有数据就以为成了结果一点下一页就翻车。识别方式其实很简单看响应里的分页字段。常见的有三种。页码式参数是page和size或者pageNum和pageSize响应里通常有total、pages。这种最好处理翻页就是把page加一。偏移式参数是offset和limit响应里可能有hasMore。翻页时offset加上limit逻辑上等价于页码式。游标式响应里会返回一个cursor或nextCursor下一次请求要把它原样带上。这种常见于信息流类接口它的好处是数据插入时不会错位坏处是你不能跳页只能顺着往下走。在本地做假数据的时候我建议把接口真实的分页形态保留下来。也就是说你的 Mock 服务也返回total和hasMore也接受cursor。这样等真接口上来组件代码一行不用改。很多人图省事Mock 直接返回一个数组真接口上来是{list: [], total: 0}然后组件里到处改非常痛苦。4.3 遇到签名参数先别急着硬刚有些站点的请求里会带一堆看着像乱码的参数比如sign、nonce、timestamp、_t。这是典型的请求签名服务端会拿这些参数按某个规则重新算一遍对不上就拒绝。遇到这种情况我的建议是分三步判断。第一步先看这些参数是不是变了。刷新页面重新抓一次如果sign每次都不同、timestamp每次都更新那基本就是签名的形态。如果一直不变那可能只是个固定值或者版本号可以直接照抄。第二步看参数之间有没有明显关系。比如timestamp是不是当前毫秒数nonce是不是随机字符串。这类可以自己构造。真正难的是sign这种把多个参数拼起来再加盐做哈希的结果。第三步也是最重要的一步评估你需不需要它。如果只是要拿数据做本地演示你完全可以在本地把响应快照存下来用本地 Mock 服务返回根本不需要在运行时重新计算签名。如果确实需要动态获取那就要接受一个现实——你得复现它的签名算法而这需要阅读对方的混淆脚本工作量和维护成本都很高而且随着对方更新就会失效。我的态度很明确签名是为了防止滥用而设计的绕过它不是我们该做的事。碰到签名接口正确做法是换一个更开放的公开接口或者老老实实找数据来源方要授权。技术上能不能做到是另一回事该不该做是另一回事这条线要守住。4.4 顺手把响应变成类型定义前端项目用 TypeScript 的话从 Response 那一栏的内容你就能直接把类型写出来。手动写也可以用一个小技巧把响应 JSON 复制出来贴到一些在线转换工具里能自动生成 interface。interface ListResponse { list: Array{ id: number title: string createdAt: string status: draft | published } total: number hasMore: boolean }这一步看着不起眼但它会让你的 Mock 数据和真实数据结构严丝合缝。字段名写错一个字母编译期就报错不用等到运行时对着空表格发愣。5. 数据到手之后往哪放三种落地方式拿到数据只是开始接下来要决定它在你的项目里怎么活。这里有三条路线从轻到重适合不同阶段。5.1 一次性快照最快也最省事如果只是要给作品集做个演示、给设计稿配上真实内容最省事的做法就是把响应存成一个 JSON 文件直接放进项目里。// src/mocks/list.json import listData from ./mocks/list.json export function fetchList() { return Promise.resolve(listData) }这里有个小细节值得注意包一层 Promise。返回裸数组和返回 Promise 在调用方的写法完全不同前者你写const data fetchList()后者要写await fetchList()。如果你的真实接口是异步的Mock 也一定要是异步的这样组件里的 loading 状态、错误分支才有机会被真正执行到。我就踩过这个坑——Mock 同步返回组件跑得飞起结果真接口一上loading 状态根本没测过到处闪烁。5.2 MSW 和 json-server让假数据有真接口的手感快照方式的问题是不能按参数返回不同结果搜索、筛选、分页全都没法测。这时候可以上 Mock 服务。MSWMock Service Worker的思路很巧它不拦截你的fetch函数而是在 Service Worker 层拦截真实网络请求。也就是说你的业务代码完全不知道自己在跟假数据打交道fetch(/api/list)照样写。这对测试非常友好。import { http, HttpResponse } from msw import { setupWorker } from msw/browser export const handlers [ http.get(/api/list, ({ request }) { const url new URL(request.url) const page Number(url.searchParams.get(page) ?? 1) const size Number(url.searchParams.get(size) ?? 20) const all Array.from({ length: 137 }, (_, i) ({ id: i 1, title: 示例内容 ${i 1}, })) const start (page - 1) * size return HttpResponse.json({ list: all.slice(start, start size), total: all.length, hasMore: start size all.length, }) }), ] setupWorker(...handlers).start()json-server更简单一条命令起一个 REST 服务。npx json-server --watch db.json --port 3001给它一个db.json它自动给你生成列表、详情、筛选、分页这些接口。适合快速验证缺点是定制逻辑不太方便。我的习惯是接口参数复杂、要模拟各种异常状态的时候用 MSW只是要几个静态集合的时候用 json-server。两者都能让前端在没有后端的情况下完整跑通。5.3 自建中转层把“抓”变成“取”如果你确实需要一个能拿到实时数据的接口最干净的做法是自己搭一层很简单的中转服务。前端请求你自己的服务你的服务去请求数据源做一层字段转换和缓存再返回给前端。这样做有几个明显好处。第一凭据全部留在服务端前端拿不到安全边界清晰。第二可以在这一层做缓存减少对目标站点的请求压力。第三可以做字段归一化把对方五花八门的命名统一成你自己的规范。用 Node 写一个最简单的版本大概是这个形态import express from express const app express() const cache new Map() const TTL 5 * 60 * 1000 app.get(/api/list, async (req, res) { const key JSON.stringify(req.query) const hit cache.get(key) if (hit Date.now() - hit.time TTL) { return res.json(hit.data) } const target new URL(https://api.example.com/v1/list) Object.entries(req.query).forEach(([k, v]) target.searchParams.set(k, v)) const upstream await fetch(target, { headers: { accept: application/json, authorization: Bearer ${process.env.UPSTREAM_TOKEN}, }, }) if (!upstream.ok) { return res.status(upstream.status).json({ error: upstream failed }) } const raw await upstream.json() const data { list: (raw.data?.items ?? []).map((it) ({ id: it.item_id, title: it.item_title, createdAt: it.create_time, })), total: raw.data?.total ?? 0, } cache.set(key, { time: Date.now(), data }) res.json(data) }) app.listen(3000)注意里面的字段映射那一段这就是中转层真正的价值把上游的命名翻译成前端的语言。前端永远只看自己的规范上游换了字段名你只改这一处。6. 别把学习项目跑成事故节奏、缓存与边界技术上打通了接下来是让它稳定、合理、不惹麻烦。这部分内容平时很少有人讲但恰恰是区分“能跑”和“靠谱”的地方。6.1 请求频率和退避重试真实站点对请求频率都有承受上限。你在本地循环几百次请求对方看到的是异常流量轻则返回 429重则直接拒绝你的来源。这不是危言耸听是很基本的礼貌。在代码里加一层节流和重试是很简单的事async function fetchWithRetry(url, options {}, retries 3) { for (let i 0; i retries; i) { try { const res await fetch(url, options) if (res.status 429 || res.status 500) { throw new Error(HTTP ${res.status}) } return res } catch (err) { if (i retries - 1) throw err // 指数退避0.5s、1s、2s await new Promise((r) setTimeout(r, 2 ** i * 500)) } } }指数退避的意思是失败之后等待时间逐次翻倍而不是固定间隔狂试。这既给了对方喘息空间也提高了你自己最终成功的概率。批量请求之间再加一个几百毫秒的随机间隔整体会稳很多。6.2 缓存别只放一层缓存做得好能挡掉大半重复请求对双方都是好事。我一般分三层来放。内存层用Map一次会话内有效刷新就没了适合页面内多次复用同一份数据。本地存储层用localStorage或sessionStorage跨刷新有效适合那些变化不频繁的字典类数据。磁盘层如果是在中转服务里可以写文件或上 SQLite重启也不丢。一个带过期时间的本地缓存可以写成这样function cachedFetch(key, url, ttl 5 * 60 * 1000) { const raw localStorage.getItem(key) if (raw) { const { time, data } JSON.parse(raw) if (Date.now() - time ttl) { return Promise.resolve(data) } } return fetch(url) .then((r) r.json()) .then((data) { localStorage.setItem(key, JSON.stringify({ time: Date.now(), data })) return data }) }有个细节要注意localStorage存的是字符串容量有限一般几 MB。存大列表会爆遇到大响应要么只存第一页要么改用 IndexedDB。另外缓存一定要有 TTL不然数据永远不更新用户看到的是老内容。6.3 边界在哪里心里要有数前面说过这里再明确一次。做这类事情时几条红线建议记牢。只碰公开数据。需要登录才能看到的内容、涉及个人身份的信息不要采集。尊重目标站点的使用条款和 robots 约定。有些站点明确禁止自动化访问那就换来源。不要重新分发。拿到的数据用于自己本地演示可以转手发布、商用、批量搬运就不行。不要冲击对方服务。控制并发、加缓存、加退避这是基本素养。拿不准的时候走开放平台。现在公开数据的渠道很多注册一个开发者账号拿 key比自己去扒要稳得多也更省心。6.4 一张排查表遇到问题按顺序过调试过程中遇到的问题其实高度集中我把高频的几类整理成表遇到卡壳时从上往下过一遍。现象最可能的原因排查动作控制台报跨域未配置代理或代理路径不对检查代理target和rewrite返回 401 或 403凭据过期或缺失重新抓一次对比请求头差异返回 200 但数据为空分页参数不对或需要游标对比响应里的分页字段本地能跑部署后挂代理只在开发服务器有效生产环境需要自建中转层参数一致但结果不同缺少某个自定义头逐条删除请求头定位必需项频繁返回 429请求过快被限流加节流、加缓存、加重试这张表用久了你会发现大部分“诡异问题”其实都是这几类的变体。真正难的不是修复而是快速判断属于哪一类。7. 几个我实际踩过的坑说给你听聊点具体的。有次我给一个表格做分页Mock 服务返回的total写错了量级前端分页器给算出了几千页一点下一页请求直接打到不存在的页码上后端返回 400。问题在 Mock 里排查却在组件里找了半天。从那之后我养成一个习惯Mock 数据的分页字段一定和真实接口保持同量级、同类型total是数字就永远是数字不要一会儿是字符串一会儿是数字。还有一次是把带authorization的 cURL 直接粘到了项目的请求封装文件里一度提交到了仓库。虽然是个私人仓库但当时看到 commit 记录的时候后背还是发凉。后来我给自己定了条规矩所有凭据只走环境变量请求封装里永远不出现字面量。加个 pre-commit 钩子扫一遍更保险。第三个坑跟缓存有关。早期我用localStorage缓存列表数据忘了加 TTL。测试的时候一直显示旧内容我以为接口挂了折腾了快一个小时才发现是自己缓存的问题。现在的做法是缓存 key 里带上参数指纹和日期比如list:page1:size20:20260101到期自动换 key逻辑简单还不容易出错。最后一个体会是关于心态的。刚开始做这类事情的时候我总想一步到位把请求签名也复现出来把动态数据也接上觉得那样才叫“完整”。做了几个项目之后才明白大部分场景下你需要的不是实时数据而是一个结构正确、形态真实的数据源。一个字段对得上、分页对得上的本地 Mock价值远高于一个随时会失效的动态爬取。把精力放在接口结构的还原和字段对齐上联调那天你会感谢自己。如果后续想继续扩展可以考虑给中转层加一层统一的错误约定把所有上游的异常都归一成{ code, message }前端就只需要处理一种错误形态也可以把字段映射规则抽成配置换数据源的时候不用改代码。这些都不是必须的但做过一次之后你会发现整个数据链路清爽了不少。
返回列表