Lesson 02 · 请求链路
从一个下拉框,到 /1000y/engine/cmd。
中间有四个地方会悄悄改你的请求。
gameboxclient 工作区快照。
1. 界面上按钮点了没反应,DevTools 控制台干干净净。下一步最该做什么?
答案:B。「渲染层控制台没错误」不等于「没出错」。
主进程的错误在启动 Electron 的那个终端里,子进程的错误连终端都没有。
A 不算错,但顺序反了 —— 先判断层,再选工具,能省掉大量无效插桩。
2. 为什么在这个项目的渲染层里能直接写
import Fs from 'node:fs'?
答案:C。electron/main/config/options.ts:6-8 把
nodeIntegration 打开、contextIsolation 关掉。
B 是标准做法但本项目没有 —— preload 的构建在
vite.config.ts:58-72 整段被注释了,那个文件从没被加载过。
这是最容易记反的一条。
策划打开「NPC」页,从地图下拉框里选了一张地图,列表刷新出这张地图上的所有 NPC。
就这么一个动作。但如果有一天他跟你说「选了地图但列表是空的」, 你需要知道这中间有几个环节可能出问题。 这一课就是把这条链路完整走一遍。
先看图
这条链路画在架构图 FIG.04, 可以逐步播放。建议先看一遍图,再回来看代码 —— 图给的是骨架, 下面给的是每一节骨头上的行号。
这一步听起来多余,但它是本项目最容易翻车的地方。页面上有两个东西看起来都像「搜索」, 只有一个会真的发请求。
打开
src/views/GameEditor/Npc/index.vue,先看模板里的事件绑定:
| 界面元素 | 绑的是 | 会发请求吗 |
|---|---|---|
| 「搜索」按钮(Npc/index.vue:24) | @click="searchNpc" |
不会。searchNpc() (:84)只是在已经拿到的 npcList 上做
filter,纯内存操作
|
| NPC 名称下拉、类型复选框 | @change="searchNpc" |
不会,同上 |
| 地图下拉框 |
@change="search" / @clear="search"
|
会。search() (:117)里 await byMapIdGetNpc(...)
|
| 页面刚打开 | onBeforeMount(:149) |
会,而且发两次。先
getExcelProperty 拉地图列表,再调
search() 拉 NPC
|
❌ 别学
两个只差三个字母的函数,语义完全不同。
search() 发网络请求,searchNpc()
只做本地过滤, 而且 search() 的最后一行还会调
searchNpc() (Npc/index.vue:145)。
规范做法:命名要体现副作用。
fetchNpcListByMap() 和 filterNpcList()
一眼就能分清谁碰网络。
给你的实操规矩(这条会救你很多次): 描述用户动作之前,先在模板里找到那个事件绑定。 不要从函数名猜。
页面不直接用 axios。它调 src/api/ 下的封装:
// src/views/GameEditor/Npc/index.vue:130
const res = await byMapIdGetNpc({
mapId: mapID.value,
pageSize: 9999,
pageNum: 1,
type: 0
})
而 byMapIdGetNpc 长这样(src/api/game/index.ts:14):
export async function byMapIdGetNpc(params: GetNpcApiModel) {
const { serverId, ...data } = params
return request.post({
url: '/1000y/engine/cmd',
data: {
type: 1,
...data,
reverse: true,
serverId: serverId || getStorage('defaultServerId'),
cmd: 'npc_search_ByMapId' // ← 关键在这
}
})
}
这里有两件事值得停下来看:
cmd 里
这不是 REST。几乎所有业务请求都打到同一个地址 /1000y/engine/cmd,服务端靠请求体里的 cmd 字段决定干什么:
cmd |
做什么 | 定义在 |
|---|---|---|
npc_search_ByMapId |
按地图查 NPC | api/game/index.ts:14 |
excel_search_name |
查某张表有哪些字段 / 数据 | api/game/index.ts:35 |
excel_search_DateById |
按 id 查一条数据 | api/game/index.ts |
search_excel_max_id |
查当前最大 id(新增数据时用) | api/game/index.ts |
excel_modify |
写入:改一条配置数据 | api/gm/index.ts:259 |
这条对排障的影响非常大
打开 DevTools Network
面板,你会看到一整屏长得一模一样的请求: 全是
POST cmd,状态全是 200。
想知道某个请求在干什么,必须点开它、切到 Payload、看 cmd 字段。按 URL 筛选在这个项目里毫无用处。
小技巧:在 Network 的搜索框里直接搜 excel_modify,
可以筛出所有写操作 —— 排查「数据被改坏了」时特别有用。
serverId 有个静默兜底
serverId: serverId || getStorage('defaultServerId') ——
调用方没传就用本地存的默认服。
这意味着「选服」这个动作会影响之后所有请求。
如果用户报「数据不对」,先问他选的哪台服。 这个值没选过的话可能是
undefined,请求照发, 服务端返回空 ——
界面上看起来就是「列表是空的」。
request.post 来自
src/config/axios/index.ts, 是一层很薄的包装:把
{ url, method, params, data, headersType }
转成 axios 的调用形式,顺便塞一个默认的
Content-Type: application/json。
真正干活的是 service(src/config/axios/service.ts:22):
const service: AxiosInstance = axios.create({
baseURL: BASE_PATH_URL, // import.meta.env.VITE_BASEPATH
timeout: SERVICE_TIMEOUT // 2 分钟
})
config.ts 里
request_timeout: 2 * 60 * 1000)。
对配置表这种可能返回上万条的接口是合理的,
但代价是:接口挂了,用户要盯着转圈整整两分钟才看到「请求超时」。
src/config/axios/service.ts:115:
service.interceptors.request.use((config) => {
const token = getStorage(appStore.getToken)
token && config.headers.setAuthorization(`Bearer ${token}`) // ①
handleGetParams(config) // ②
handlePostParams(config) // ③
handle1000yRequest(config) // ④
return config
})
① 和 ② ③ 都是常规操作(拼 GET 查询串、按 Content-Type 决定要不要
qs.stringify)。
值得细看的是 ④。
handle1000yRequest:只对含 1000y 的 URL 生效
src/config/axios/service.ts:88,它做三件事:
算出 pcode。调 getPcode()(:30) —— 依次尝试 getStorage('pCode')、
gameEnv.getEnvKey('VITE_API_PCODE')、
getStorage('GameENV')['VITE_API_PCODE']、 构建期的
ENV.VITE_API_PCODE,取第一个非空的。
加密成 excelToken 塞进请求头。
Crypto.default.encrypt(pcode)(AES), 结果写进
config.headers.excelToken。
这是服务端的鉴权凭据之一 —— pcode 不对,请求会被拒。
按 pcode 换服务器。getBaseURL(pcode) (:79)看 pcode
里有没有 _dev 这个子串:有就打测试
GM,没有就打正式 GM。
❌ 别学
「打测试服还是正式服」由一次字符串包含判断决定, 而且两个域名硬编码在源码里。
// src/config/axios/service.ts:79
function getBaseURL(pcode: string): string {
if (pcode.includes('_dev')) {
return 'http://common-gm-460qntest.3975app.com/n1000y-api/'
}
return 'http://common-gm-460qnprod.3975app.com/n1000y-api/'
}
三个问题:
_dev 后缀 —— 请求就直接打到正式服。
这个默认方向是反的:不确定时应该落在最安全的环境,不是最危险的。
http 不是 https。
带着 token 和 excelToken 明文过网络。
规范做法:环境判定用显式的环境标识(一个
env: 'test' | 'prod' 字段),不要靠子串匹配;
地址走配置下发(这个项目已经有 /user/info 下发机制了,
VITE_API_BASEPATH 和 VITE_API_BASEPATH_DEV
本来就在下发内容里);默认值选安全的那个。
排障提示:用户说「我在测试服改的,怎么正式服也变了」——
先看 getPcode() 那一串兜底里,实际生效的是哪一个。
在控制台敲 localStorage.getItem('pCode') 最快。
src/config/axios/service.ts:131:
service.interceptors.response.use((response) => {
if (response.config.responseType === 'blob') return response
if (response.config.responseType === 'stream') return response
if (response.data.code === result_code || response.data.code === 1) {
return response.data // ← 注意:返回的是 data,不是 response
}
return Promise.reject(response) // ← 业务码不对 = reject
})
两个必须记住的行为:
成功时返回的是 response.data,不是完整
response。
所以页面里写的 res.data?.data 里那两个 data ——
第一个是业务包裹层,第二个才是真正的数组。看着别扭,但是对的。
业务码不是 200 或 1 就 reject。
也就是说 HTTP 200 但业务失败,也会走到你的 catch 里。
Npc/index.vue:139-142 的 catch 就是干这个的。
⚠️ 隔离
成功码有两个:200 和 1。
result_code 在 config.ts 里是 200,
但拦截器额外接受了一个硬编码的 1。
大概率是某些老接口返回 code: 1,加了个兼容。
这类「魔法数兼容」的问题在于没人知道哪些接口属于哪一类, 以后再有第三种也只能继续加。
规范做法:要么统一服务端返回, 要么把兼容规则写成一张明确的表(哪些 URL 走旧协议),别散在条件判断里。
① 用户换地图下拉 →
@change="search" (Npc/index.vue:117)
② byMapIdGetNpc() 组装 body,塞进
cmd: 'npc_search_ByMapId' 和 serverId (api/game/index.ts:14)
③ request.post → axios 实例 (config/axios/index.ts)
④ 请求拦截器加 Authorization、算 pcode、
加密 excelToken、按 pcode 决定打测试还是正式
(service.ts:115)
⑤ POST /1000y/engine/cmd 上路
⑥ 响应拦截器检查业务码,成功返回
response.data(service.ts:131)
⑦ 页面把结果排序后写进 Pinia:
ViewData.NPC.npcList = res.data?.data.sort(...)
(Npc/index.vue:136)
⑧ 再调 searchNpc() 做本地过滤 → 界面更新
(Npc/index.vue:145)
现在你有了完整链路,可以把这个模糊报障拆成有序的检查点:
| # | 检查 | 怎么查 | 是它的话 |
|---|---|---|---|
| 1 | 请求发出去了吗 | Network 里搜 cmd |
没发 → 回到第 0 步,用户点的可能是纯本地过滤的那个 |
| 2 | 打到哪台服务器了 |
看请求的 Request URL 域名,qntest 还是
qnprod
|
错了 → 查 pcode:localStorage.getItem('pCode') |
| 3 | 参数对吗 | Payload 里看 mapId 和 serverId |
serverId 是 undefined → 用户没选服 |
| 4 | 服务端返回了什么 | Response 面板 | 返回了数据但界面空 → 问题在 ⑦⑧,是前端过滤逻辑 |
| 5 | 凭据齐吗 |
JSON.parse(localStorage.getItem('GameENV'))
|
空的 → 登录没成功或服务端没下发,见课 01 |
/game/npc。观察:页面一打开就发了至少两个请求, 先
excel_search_name(拉地图列表)后
npc_search_ByMapId
cmd 字段。 确认两个请求的 URL 完全一样,只有 cmd 不同
excelToken, 确认它是一串密文而不是明文 pcode
qntest 还是 qnprod。
然后在控制台敲 localStorage.getItem('pCode'),
验证它里面有没有 _dev
—— 两者应该对得上
把结论记进你自己的分诊表
这一课产出了三条可复用的检查点,去 故障分诊表 看它们被记成了什么样子。 分诊表是逐课累积的,到课 09 会攒成一张完整的表。