时长:40-50 分钟

5.1 功能概述
5.1.1 环境切换的业务需求
- 内部测试与联调中,被测 App 往往需要连接不同的后端环境:
- 测试环境(test):日常回归、功能验证
- 预发/联调环境(stage):与后端联调、压测
- 生产镜像环境(prod-like):上线前验证
- 环境信息通常由 nohost / 代理服务 管理:用户/环境切换后,请求会路由到对应后端。
- 测试同学需要明确知道 当前设备/App 实际连的是哪个环境,才能:
- 正确选择数据银行树中的账号与环境
- 与快捷登录、环境切换功能配合使用
- 在问题反馈时准确描述“当时连的是哪个环境”
5.1.2 当前环境获取的重要性
- 避免“说的和做的不一致”:
有时在 PC 上选择了“测试环境”,但设备因网络/缓存等原因仍连到旧环境; 通过 在设备侧发起请求 获取当前环境,可以以设备真实网络为准。
- 与数据银行树、快捷登录形成闭环:
工具展示“当前环境”后,用户可对比已选用户/环境,必要时再点“切换环境”或“刷新”,减少误操作。
5.1.3 用户体验优化
- 在设备面板显眼位置展示 当前环境(如“测试环境 / test”),并支持一键刷新。
- 加载中显示 loading,失败时给出明确提示(设备未连接、Companion 未就绪、接口异常等)。
- 支持 静默刷新(如切换环境后轮询直到匹配),避免频繁弹通知打扰用户。
5.2 环境获取的两种方式
5.2.1 方式一:设备端请求(推荐)
通过 Companion 服务在设备上发起请求
- 思路:由 iOS 设备上的 Companion App(如 PhotoCompanion)在设备网络环境下发起 HTTP 请求,再把响应返回给 PC 工具。
- 好处:
- 请求走的是 设备的网络(Wi-Fi/蜂窝),与 App 实际使用的网络一致。
- 能反映 nohost 在设备侧的解析结果,即 设备真实当前环境。
- 前提:
- Companion 已安装并在前台运行(或至少 URL Service 已连接)。
- PC 与设备之间已通过 go-ios tunnel + 端口转发建立连接(与第 4 期快捷登录同一条链路)。
fetchUrlOnDevice() 实现要点(iOS)
在 src/utils/ios.js 中:
async fetchUrlOnDevice(url, deviceId = '') {
// 1. 检查 URL Service(Companion)是否可用
if (!window.electronAPI?.urlService?.httpRequest) {
return { success: false, error: 'URL Service httpRequest 不可用' }
}
// 2. 选择目标设备并设置到 urlService
const devices = await this.getDevices()
const targetDevice = deviceId ? devices.find(d => d.id === deviceId) || devices[0] : devices[0]
await window.electronAPI.urlService.setDevice({
udid: targetDevice.id,
name: targetDevice.name || 'iPhone',
type: 'iPhone',
version: targetDevice.version || '17.0'
})
// 3. 通过 Companion 在设备上发起 HTTP 请求
const resp = await window.electronAPI.urlService.httpRequest({
url,
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
},
timeoutMs: 10000
})
// 4. 解析响应(可能是 { status, headers, body } 或直接 JSON)
// 若 body 是 HTML,从 <pre> 中提取 JSON
// ...
return { success: true, data: parsed }
}
- 防缓存:请求头带
Cache-Control: no-cache、Pragma: no-cache,避免环境刚切换时仍返回旧的curEnv。 - 响应解析:Companion 返回可能是
{ status, headers, body }(body 为字符串),也可能是已解析的 JSON;若 body 是带
`<pre>` 的 HTML,会从 `<pre>`
的 HTML,会从
中提取 JSON 再解析。
优势小结
- 以 设备真实网络 为准,与 App 行为一致。
- 不依赖本机是否能访问 nohost(例如本机未配代理时,设备端仍可正常请求)。
5.2.2 方式二:本机请求(回退方案)
直接在本机发起 HTTP 请求
- 思路:在 PC 浏览器/渲染进程 中直接
fetch(url),请求从本机发出。 - 适用场景:
- 设备端请求失败(未连接设备、Companion 未就绪、URL Service 不可用)。
- 仅需“大致当前环境”时(例如页面初次加载时先展示本机结果,再在后台尝试设备请求)。
- 局限:
- 本机网络与设备网络可能不一致(例如本机走公司代理、设备走 Wi-Fi),此时本机请求得到的“当前环境”与设备上 App 实际环境可能不一致。
fetchCurrentDeviceEnv() 中的回退逻辑
在组件中(以 iOS 面板为例):
// 优先通过设备请求
const result = await DeviceService.ios.fetchUrlOnDevice(url, device.id)
if (result.success && result.data) {
const curEnv = result.data.curEnv || result.data.curenv
if (curEnv) {
currentDeviceEnv.value = { name: curEnv.name || '', envName: curEnv.envName || '' }
return // 成功则直接返回
}
}
// 设备请求失败或无 curEnv,回退到本机请求
const response = await fetch(url)
const data = await response.json()
// 再用统一的数据解析逻辑提取 curEnv,更新 currentDeviceEnv
5.3 API 接口分析
5.3.1 http://nohost.oa.com/cgi-bin/list 接口
- 作用:查询当前 nohost 解析出的 用户列表 / 当前环境 等信息(具体以实际后端约定为准)。
- 请求方式:GET。
- 防缓存:URL 带时间戳参数,例如
http://nohost.oa.com/cgi-bin/list?_=${Date.now()},避免命中缓存导致环境切换后仍返回旧数据。
5.3.2 返回数据结构分析
- 接口可能返回 纯 JSON,也可能返回 HTML 页面(例如错误页、管理页),HTML 中可能把 JSON 放在<pre>里。
- 工具侧对两种形态都做了兼容:
- 若响应是 JSON,直接解析。
- 若响应是 HTML,则用正则从<pre>...</pre>中取出内容再
JSON.parse。
5.3.3 curEnv 字段解析
- 含义:表示“当前环境”的对象,通常包含当前用户/环境名称等信息。
- 常见字段(不同后端可能命名不一):
curEnv.name/curEnv.envName:环境名称或用户名称- 也可能出现
curenv、currentEnv、current_env、currentEnvironment等键名
- 工具会做 多字段兼容 与 递归查找,见 5.4。
5.4 数据解析优化
5.4.1 多种字段名支持
- 第一层优先查找:
data.curEnv || data.curenv || data.currentEnv || data.current_env || data.currentEnvironment
- 若直接键名都没有,再进入 递归查找,避免因后端字段名大小写或命名风格变化导致解析失败。
5.4.2 递归查找机制(findCurEnv)
在 iOS / Harmony 等组件中使用的 findCurEnv 逻辑(示意):
function findCurEnv(obj, depth = 0) {
if (depth > 3 || !obj || typeof obj !== 'object') return null
// 先检查当前对象的所有键(不区分大小写)
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
const lowerKey = key.toLowerCase()
if (['curenv', 'currentenv', 'current_env', 'currentenvironment'].includes(lowerKey)) {
return obj[key]
}
}
// 再递归子对象(仅普通对象,不进入数组)
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue
const value = obj[key]
if (value && typeof value === 'object' && !Array.isArray(value)) {
const found = findCurEnv(value, depth + 1)
if (found) return found
}
}
return null
}
- 深度限制:最多递归 3 层,避免深层嵌套或异常结构导致性能问题。
- 键名兼容:对键名转小写后匹配,兼容
curEnv、curenv、currentEnv等写法。
5.4.3 容错处理
- 解析失败时:不抛错,将
currentDeviceEnv置为null,并在控制台输出警告和原始数据结构,便于排查。 - 设备请求失败时:自动回退到本机请求,并在 UI 上通过通知提示“已回退为本机请求”。
5.4.4 环境名称格式兼容
- 从
curEnv中取展示用文案时,兼容多种字段名:
envName = curEnv.envName || curEnv.env_name || curEnv.name || curEnv.env || '' name = curEnv.name || curEnv.envName || curEnv.env_name || ''
- 前端只关心“名称 + 环境名”两个展示字段,不依赖后端固定键名。
5.5 完整实现流程
组件挂载 / 刷新按钮点击
→ 检查设备是否连接
→ 优先通过设备端请求(Companion httpRequest)
→ 解析返回数据(JSON 或 HTML 中的 <pre>)
→ 提取 curEnv(多字段名 + 递归 findCurEnv)
→ 更新 UI 显示(currentDeviceEnv)
→ 若设备请求失败,则回退到本机 fetch
→ 同样解析并更新 currentDeviceEnv
→ 显示当前环境信息或错误提示
5.5.1 页面加载时的行为
- 设备列表就绪后(例如选中首台设备),自动调用
fetchCurrentDeviceEnv()。 - 先尝试设备端请求,成功则用设备结果更新“当前环境”;失败则静默回退到本机请求,一般不在此时弹错误通知,避免打扰用户。
5.5.2 用户点击“刷新当前环境”
- 调用
refreshCurrentEnv(device, options)。 - 仍优先设备端请求;成功时可选择弹出“已通过设备请求并解析当前环境”的提示。
- 若未从设备拿到有效
curEnv,再执行fetchCurrentDeviceEnv()(内部会走本机请求),并视情况提示“已回退为本机请求”。
5.5.3 切换环境后的轮询(refreshCurrentEnvAfterChange)
- 用户在执行“切换 nohost 环境”后,需要确认设备侧 list 接口返回的
curEnv已更新。 refreshCurrentEnvAfterChange会在短时间内多次调用refreshCurrentEnv(..., { silent: true }),直到:- 当前展示的环境名与期望环境一致,或
- 达到最大重试次数。
- 成功匹配时再弹出“切换完成”类提示,避免切换尚未生效就提示成功。
5.6 前端展示优化
5.6.1 加载状态显示
- 请求进行中:
currentEnvLoading = true,在“当前环境”区域显示 loading(如图标或骨架)。 - 请求结束:在
finally中置currentEnvLoading = false,避免 loading 常驻。
5.6.2 占位符显示(显示已选择的用户和环境)
- 在未获取到“当前环境”或加载中时,可显示占位符(如“加载中…”、“未获取”)。
- 若已有数据银行树选中的用户/环境,可在附近展示“已选:xxx / xxx”,与“当前环境”区分,方便用户对照。
5.6.3 刷新按钮交互
- 提供明确的“刷新当前环境”按钮(如图标按钮 + title)。
- 刷新时按钮可置为 loading 或禁用,防止重复点击导致多次并发请求。
5.6.4 错误提示优化
- 设备请求失败:在控制台打日志,必要时用
ElNotification.warning提示“设备请求失败,已回退为本机请求”。 - 本机请求也失败(如网络错误):可提示“获取当前环境失败,请检查网络或 nohost 服务”。
- 避免在页面首次加载时对“回退到本机”做过于刺眼的提示,以静默或轻量提示为主。
5.7 代码实现详解
5.7.1 fetchCurrentDeviceEnv() 函数解析
- 职责:在“有设备”的前提下,获取一次当前环境并更新
currentDeviceEnv。 - 流程:
- 若无设备列表则直接 return。
- 构造带时间戳的 list URL,优先调用
DeviceService.ios.fetchUrlOnDevice(url, device.id)。 - 若返回成功且有
result.data,从中取curEnv或curenv,赋值currentDeviceEnv后 return。 - 否则用本机
fetch(url)请求同一接口,对返回的 JSON 做多字段 +findCurEnv解析,再更新currentDeviceEnv。 - 任何异常时置
currentDeviceEnv = null,在finally中关闭 loading。
5.7.2 refreshCurrentEnv() 函数解析
- 职责:用户主动刷新当前环境,可带提示。
- 参数:
device(当前设备)、options = {}(如options.silent表示不弹通知)。 - 流程:
- 若指定了 device,先调用
IOSManager.fetchUrlOnDevice(url, device.id)。 - 若得到
curEnv,更新currentDeviceEnv,并可根据options.silent决定是否弹出“刷新成功”类通知。 - 若未从设备拿到有效结果,再调用
fetchCurrentDeviceEnv()(内部会走本机请求),并视情况提示“已回退为本机请求”或“刷新成功”。
5.7.3 数据解析逻辑
- 设备端返回:
fetchUrlOnDevice已统一把响应处理成{ success, data },其中data为解析后的对象(可能来自 JSON 或 HTML <pre>)。 - 组件侧:只关心
data.curEnv/data.curenv;本机请求分支则对data做多键名 +findCurEnv,再从中取出name、envName等展示字段。
5.7.4 UI 更新机制
- 使用 Vue 响应式变量(如
currentDeviceEnv、currentEnvLoading)绑定到模板。 - 获取成功或失败后统一更新
currentDeviceEnv,界面自动刷新;通知通过 Element Plus 的ElNotification统一风格展示。
5.8 调试技巧
5.8.1 日志分析方法
- 设备请求:在控制台搜索
[当前环境]、[fetchUrlOnDevice]、[刷新环境],可看到: - 是否走了设备请求、请求 URL、是否成功。
- 解析后的
result.data或本机请求的data,便于确认是否包含curEnv。 - 本机请求:搜索
[当前环境] 使用本机发起请求,确认回退是否触发。 - URL Service:主进程/Companion 侧日志会包含 httpRequest 的请求与响应,便于排查连接或超时问题。
5.8.2 数据结构排查
- 若“当前环境”一直为空,可临时在解析逻辑后打印完整
data和Object.keys(data),确认后端实际返回的键名。 - 检查是否有
curEnv被包在多层嵌套里(如data.result.curEnv),必要时调整findCurEnv的深度或键名列表。
5.8.3 常见问题解决
| 现象 | 可能原因 | 建议 |
| 始终显示“未获取”或占位 | 设备未连接 / Companion 未就绪 / URL Service 未连接 | 确认设备列表有设备,Companion 在前台,并查看主进程日志是否报连接失败 |
| 设备请求失败后本机请求也失败 | 本机无法访问 nohost(网络/代理) | 检查本机浏览器能否直接打开 list URL,或使用抓包工具看请求是否发出 |
| 环境切换后“当前环境”迟迟不更新 | list 接口有缓存或 nohost 更新延迟 | 确认 URL 带时间戳、请求头带 no-cache;必要时在切换后多等几秒或多次点击刷新 |
| 解析到 curEnv 但展示为空 | 字段名与预期不符(如 `env_name` 而非 `envName`) | 对照 5.4.4 的兼容列表,或在解析处打印 `curEnv` 查看实际结构 |
> 本期围绕“如何获取移动设备当前环境”这一需求,讲解了设备端请求与本机请求两种方式、 > nohost list 接口与 curEnv 解析、以及前端展示与调试技巧,便于在 iOS(及 Android/Harmony)设备面板上稳定展示“当前环境”,并与数据银行树、快捷登录等功能配合使用。







Comments | NOTHING