第5期:iOS 获取移动设备当前环境 ⭐

发布于 3 天前  21 次阅读


时长: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-cachePragma: 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 &amp;&amp; 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:环境名称或用户名称
    • 也可能出现 curenvcurrentEnvcurrent_envcurrentEnvironment 等键名
  • 工具会做 多字段兼容递归查找,见 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 &gt; 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 &amp;&amp; typeof value === 'object' &amp;&amp; !Array.isArray(value)) {
      const found = findCurEnv(value, depth + 1)
      if (found) return found
    }
  }
  return null
}
  • 深度限制:最多递归 3 层,避免深层嵌套或异常结构导致性能问题。
  • 键名兼容:对键名转小写后匹配,兼容 curEnvcurenvcurrentEnv 等写法。

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
  • 流程
  1. 若无设备列表则直接 return。
  2. 构造带时间戳的 list URL,优先调用 DeviceService.ios.fetchUrlOnDevice(url, device.id)
  3. 若返回成功且有 result.data,从中取 curEnvcurenv,赋值 currentDeviceEnv 后 return。
  4. 否则用本机 fetch(url) 请求同一接口,对返回的 JSON 做多字段 + findCurEnv 解析,再更新 currentDeviceEnv
  5. 任何异常时置 currentDeviceEnv = null,在 finally 中关闭 loading。

5.7.2 refreshCurrentEnv() 函数解析

  • 职责:用户主动刷新当前环境,可带提示。
  • 参数device(当前设备)、options = {}(如 options.silent 表示不弹通知)。
  • 流程
  1. 若指定了 device,先调用 IOSManager.fetchUrlOnDevice(url, device.id)
  2. 若得到 curEnv,更新 currentDeviceEnv,并可根据 options.silent 决定是否弹出“刷新成功”类通知。
  3. 若未从设备拿到有效结果,再调用 fetchCurrentDeviceEnv()(内部会走本机请求),并视情况提示“已回退为本机请求”或“刷新成功”。

5.7.3 数据解析逻辑

  • 设备端返回fetchUrlOnDevice 已统一把响应处理成 { success, data },其中 data 为解析后的对象(可能来自 JSON 或 HTML <pre>)。
  • 组件侧:只关心 data.curEnv / data.curenv;本机请求分支则对 data 做多键名 + findCurEnv,再从中取出 nameenvName 等展示字段。

5.7.4 UI 更新机制

  • 使用 Vue 响应式变量(如 currentDeviceEnvcurrentEnvLoading)绑定到模板。
  • 获取成功或失败后统一更新 currentDeviceEnv,界面自动刷新;通知通过 Element Plus 的 ElNotification 统一风格展示。

5.8 调试技巧

5.8.1 日志分析方法

  • 设备请求:在控制台搜索 [当前环境][fetchUrlOnDevice][刷新环境],可看到:
  • 是否走了设备请求、请求 URL、是否成功。
  • 解析后的 result.data 或本机请求的 data,便于确认是否包含 curEnv
  • 本机请求:搜索 [当前环境] 使用本机发起请求,确认回退是否触发。
  • URL Service:主进程/Companion 侧日志会包含 httpRequest 的请求与响应,便于排查连接或超时问题。

5.8.2 数据结构排查

  • 若“当前环境”一直为空,可临时在解析逻辑后打印完整 dataObject.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)设备面板上稳定展示“当前环境”,并与数据银行树、快捷登录等功能配合使用。


一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。