@expo/hub-client

中文文档
1.12.2发布于 2026/11/14MITTypeScriptES Module构建来源已验证近 30 天下载 5.6万

设备客户端反应钩和类型,用于在浏览器中通过Expo Device Hub镜像活iOS模拟器和Android模拟器.

中文使用文档
文档目录17 节
  1. 所需资源
  2. 安装
  3. 连接到设备
  4. 添加提供者
  5. 读取状态和控件
  6. 读取设备设置
  7. 显示屏幕
  8. 以设备形状显示屏幕
  9. 折叠设备
  10. 显示每个可折叠的显示
  11. 显示 CPU 使用情况
  12. 保持更新焦点
  13. 连接到带有符号的枢纽
  14. 调用设备控制
  15. 远程连接
  16. iOS 嵌入
  17. 许可证

@ expo/ hub- 客户端

React app中的Render活iOS模拟器和Android模拟器. 连接通过 博览设备枢纽设备 DeviceScreen,并发送触控,键盘,设备控制.

所需资源

  • 一个运行中的 Expo 设备枢纽服务器 。 要么启动一个有 expo-device-hub 安装或运行 DevTools 插件 npx expo-device-hub 独立. 供应商连接到枢纽 /vendor/serve-sim 财务报告和审定财务报表 /vendor/serve-emu 路线.
  • 反应18或更新.
  • 一个显示视频并发送输入的浏览器.

安装

npm install @expo/hub-client

连接到设备

将屏幕、控件和度量标准包入 DeviceClientProvider他们共享一个 连接,每个组件都会读取它所需要的客户端属性.

添加提供者

包装需要设备连接的组件 。 下面的组件全部使用此功能 提供者 :

import { DeviceClientProvider } from "@expo/hub-client";

export function LiveSession({ udid }: { udid: string }) {
  return (
    <DeviceClientProvider
      key={udid}
      platform="ios"
      options={{
        baseUrl: "http://localhost:3400/vendor/serve-sim",
        device: udid,
        streamMode: "webrtc",
      }}
    >
      <Screen />
      <Controls />
      <Cpu />
    </DeviceClientProvider>
  );
}

用于Android, 使用 platform="android",您的服务-emu URL,以及诸如 'emulator-5554' 页:1 device。 。 。 .

使用 Expo DevTools 插件时,服务器 URL 包括 /_expo/plugins/expo-device-hub 在此之前 /vendor/serve-sim 或者说 /vendor/serve-emu。 。 。 .

streamMode 需要: 'mjpeg', (中文(简体) ), 'h264',或 'webrtc'. iOS支持所有三个. Android 映射一种它无法使用的模式.

每个会话使用一个提供者 。 当切换会话时, 设置其 key 会话编号 所以旧的连接关闭。 设定 options.enabled 改为 false 切换或使用 platform={null} 没有选择设备.

读取状态和控件

解构你需要的一切 useDeviceClient() 电话 :

import { useDeviceClient } from "@expo/hub-client";

function Controls() {
  const { status, pressButton, rotate, reload } = useDeviceClient();
  const connected = status === "streaming";

  return (
    <div>
      <span>{status}</span>
      <button disabled={!connected} onClick={() => pressButton("home")}>
        Home
      </button>
      <button disabled={!connected} onClick={rotate}>
        Rotate
      </button>
      <button disabled={!connected} onClick={reload}>
        Reload
      </button>
    </div>
  );
}

状态和控制变化更新此组件。 FPS, 度量衡和日志变化没有. 状态可以是 'idle', (中文(简体) ), 'connecting', (中文(简体) ), 'reconnecting', (中文(简体) ), 'streaming' 或者说 'error'。 。 。 . 'connecting' 表示屏幕尚未显示帧: 显示装入 UI 。 它变成 'error' 30秒后没有帧,客户端继续重试. 之后 第一帧,丢失的流是 'reconnecting': 最后一个框架保留, 所以显示不装入 UI . 在iOS上,一个持续30秒的再连接会变成 'error',客户端继续重试. framePresented 显示屏幕是否显示框架 。 一个新的设备,服务器,代号, 运输,或显示表面重新开始于 'connecting'读取 error 最后一个 失败消息 .

读取设备设置

deviceSettingsStatus 独立设置两个平台的可用性 视频和应用程序活动。 没错 'idle' 被禁用时, 'loading' 发现时 然后,第一个读,然后 'ready' 或者说 'error'设置在5后读取超时 秒数; 'ready' 包括空设置.

import { useDeviceClient } from "@expo/hub-client";

function DeviceSettingsSummary() {
  const { deviceSettingsStatus, deviceSettings } = useDeviceClient();
  if (deviceSettingsStatus === "idle") return null;
  if (deviceSettingsStatus === "loading") return <span>Loading settings…</span>;
  if (deviceSettingsStatus === "error") return <span>Settings unavailable</span>;

  return <span>Appearance: {deviceSettings?.appearance ?? "Unavailable"}</span>;
}

刷新失败报告 'error' 并保留缓存值。 使用 'ready' 启用 控件; 恢复它而不再次显示初始装入状态.

显示屏幕

使用 useDeviceScreenClient 与 DeviceScreen。它跟踪屏幕和输入变化 不更新参数或日志. DeviceScreen 填充其父母,因此给父母一个大小 position: relative。 。 。 . displayScreen 计算旋转后的屏幕大小. DeviceScreen 没有绘制背景、 装入状态或出错: 自己绘制, 结束 它,从 status, (中文(简体) ), error,以及 framePresented。 。 。 .

import { DeviceScreen, displayScreen, useDeviceScreenClient } from "@expo/hub-client";

function Screen() {
  const client = useDeviceScreenClient();
  const screen = displayScreen(client.screen);
  const aspectRatio = screen ? `${screen.width} / ${screen.height}` : "9 / 19.5";
  const loading = client.status !== "error" && client.status !== "idle" && !client.framePresented;

  return (
    <div style={{ position: "relative", width: 360, aspectRatio, background: "#eee" }}>
      <DeviceScreen client={client} />
      {loading && <Spinner />}
      {client.status === "error" && <p role="alert">{client.error}</p>}
    </div>
  );
}

以设备形状显示屏幕

导入 DeviceShape 从 @expo/hub-client/shapes 将屏幕剪接到形状 每个iPhone、iPad和像素模型的角,以及形状 显示 iPhone Duo 显示。 喜欢 DeviceScreen,它需要客户端, 它转 与装置. 它只是剪辑,所以它画不出来,而指针输入则继续工作. A 类 没有已知形状的模型没有剪辑。 不导入的应用程序 @expo/hub-client/shapes 不要捆绑形状.

import { DeviceScreen, useDeviceClient } from "@expo/hub-client";
import { DeviceShape, useScreenShape } from "@expo/hub-client/shapes";

function ShapedScreen({ model }: { model: string }) {
  const client = useDeviceClient();
  // The shape as the viewer sees it, turned with the device, for the box's proportions.
  const shape = useScreenShape({ client, model });
  const aspectRatio = shape ? `${shape.width} / ${shape.height}` : "9 / 19.5";
  return (
    <div style={{ position: "relative", width: 360, aspectRatio }}>
      <DeviceShape client={client} model={model} style={{ position: "absolute", inset: 0 }}>
        <DeviceScreen client={client} />
      </DeviceShape>
    </div>
  );
}

客户端的设备名称是默认模式, 但模拟器可以报告它的名称 迟了 过 model 当您知道时: iOS 上的模拟设备类型名称, 例如 "iPhone 17 Pro",或者AVD硬件配置文件在Android上的代号,例如 "pixel_9"。 。 。 . platform, (中文(简体) ), display, (中文(简体) ), orientation,以及 shape 也覆盖客户端,以及 没有客户,他们是唯一的来源,例如录音:

<DeviceShape
  platform="ios"
  model="iPhone Duo"
  display="inner"
  style={{ position: "absolute", inset: 0 }}
>
  <video src={recordingUrl} />
</DeviceShape>

useScreenShape 返回布局的相同形状,例如框上方的宽比. 夹子保持角向右 以形状的自己的比例, 所以给盒子这些 比例,或者通过框的 aspectRatio 改为 DeviceShape。 。 。 . screenShapeClipPath 返回 CSS clip-path 用于您无法包接的元素.

折叠设备

client.fold 具有可折叠设备的折叠状态,例如 iPhone Duo,并且是 null 其他设备. client.capabilities.fold 列出设备的命令 接受 : 角度范围、 命名的姿势及其连接角度和表格模式。 没错 false 当设备不提供折叠命令时。 只发送它列出的.

import { useDeviceClient } from "@expo/hub-client";

function FoldButtons() {
  const { fold, capabilities } = useDeviceClient();
  const controls = capabilities.fold;
  if (!fold || !controls) return null;
  const range = controls.angle;

  return (
    <>
      <span>
        {fold.angle ?? "?"}° {fold.pose ?? "custom"} {fold.pending ? "(moving)" : ""}
      </span>
      {controls.poses.book && (
        <button onClick={() => fold.setControl({ control: "pose", value: "book" })}>Book</button>
      )}
      {range && (
        <button onClick={() => fold.setControl({ control: "angle", value: range.max })}>
          Most open
        </button>
      )}
      {controls.tableMode && fold.tableMode?.available && (
        <button onClick={() => fold.setControl({ control: "tableMode", value: true })}>
          Table Mode
        </button>
      )}
    </>
  );
}

fold.tableMode 财务报告和审定财务报表 fold.physical 只存在于拥有它们的设备上。 这个 显示一个词汇, FOLD_POSES数字 : closed, (中文(简体) ), open, (中文(简体) ), laptop, (中文(简体) ), book,以及 tent。 。 。 . fold.observe 从请求到命令 最终结果,无论哪个控制发送。 队列中的命令, 更新的将只获得 请求.

显示每个可折叠的显示

在一个iPhone Duo上, client.displays 列出 cover 编辑 inner 显示。 附加 每个显示的元素类型 client.videoKind 名称, 要流出每个显示 你自己来 虽然任何显示都有元素,但客户端会停放平流 status 跟踪当前显示。 将输入发送给显示 id,在那个中 显示坐标。 客户端放弃了从显示开始的手势 设备不显示( e)client.screen.display) (中文(简体) ).

import { useDeviceClient } from "@expo/hub-client";

function Displays() {
  const client = useDeviceClient();
  // A tap in the middle of the inner display.
  const tapInner = () => {
    client.sendTouch({ phase: "begin", display: "inner", x: 0.5, y: 0.5 });
    client.sendTouch({ phase: "end", display: "inner", x: 0.5, y: 0.5 });
  };

  return (
    <>
      {client.displays?.map((display) =>
        client.videoKind === "video" ? (
          <video key={display.id} ref={display.attachVideo} autoPlay muted playsInline />
        ) : client.videoKind === "canvas" ? (
          <canvas key={display.id} ref={display.attachVideo} />
        ) : (
          <img key={display.id} ref={display.attachVideo} alt="" />
        ),
      )}
      <button onClick={tapInner}>Tap the inner display</button>
    </>
  );
}

deviceModelUrl 是设备的3D模型,Xcode用于iPhone Duo. 枢纽的三维 预览用它 displays 财务报告和审定财务报表 fold.observe。 。 。 .

显示 CPU 使用情况

使用 useDeviceClientSelector 当您需要大对象的一个值时。 本构成部分 当最新的CPU百分比变化时更新,即使内存或网络读取有变化.

import { useDeviceClientSelector } from "@expo/hub-client";

function Cpu() {
  const cpuPct = useDeviceClientSelector((client) => client.activity?.samples.at(-1)?.cpuPct);

  return <span>{cpuPct == null ? "Waiting for metrics" : `CPU ${cpuPct.toFixed(1)}%`}</span>;
}

阅读 activity 与 useDeviceClient 任何部分随时更新 联 合 国 activity 变化.

保持更新焦点

  • 读取您组件体内的状态, 以便更新设备 。 数据仅读取于 a 事件处理器可能过时 。 诸如: client.rotate() 使用最新的回调.
  • 解结构时选择命名属性 。 传播客户端或使用 { status, ...rest } 订阅所有财产。 阅读 activity.samples 全体订阅者 activity 对象.
  • 保持选择者简单。 返回数字、字符串、布尔或已存在的对象。 狦 构建新对象,通过比较函数作为第二个参数.
  • 将客户端及其数据视为只读 .

连接到带有符号的枢纽

一个枢纽开始于 npx expo-device-hub --require-token 需要它的会话符号 请求。 Hub仪表板本身不需要额外的: 打开与符号的链接 浏览器是覆盖每个请求的饼干。 在另一个来源的一页 没有这样的饼干, 因此,把信使交给提供者:

<DeviceClientProvider
  key={udid}
  platform="ios"
  options={{
    baseUrl: "http://127.0.0.1:3400/vendor/serve-sim",
    device: udid,
    streamMode: "mjpeg",
    token,
  }}
>
  <Screen />
</DeviceClientProvider>

用于Android, 使用 platform="android" 编辑 /vendor/serve-emu URL (中文(简体) ). 客户端发送符号为 Authorization: Bearer <token>作为 serve-sim.token.<token> 或者说 serve-emu.token.<token> 和作为 ?token= 仅当浏览器无法设置信头时( MJPEG) <img>, (中文(简体) ), EventSource) (中文(简体) ).

该符号不取代 CORS 。 以 --cors-origin 和您的页面来源, 每个来源一次:

npx expo-device-hub --require-token --cors-origin https://example.com

旗帜适用于iOS和Android. 没有它,只有回路源的一页,例如 http://localhost:5173,可以读取Hub的答复. 对于Android,Hub也必须听 一个回转地址。 在该页面中,Android拒绝 HTTP 请求更改设备状态,例如 作为旋转、截图和设备设置。 iOS 拒绝 service-sim 的控制套接字,它携带 日志、事件、衡量标准、设备设置、位置和应用程序动作.

调用设备控制

每个控制者都生活在 DeviceClient控制是无操作的,而没有任何连接, 而每个后端都会忽略其平台没有的按钮.

// Hardware buttons: 'home' | 'back' | 'recents' | 'power' | 'appSwitcher' | 'hideKeyboard'
client.pressButton("home");

client.rotate();
// client.capabilities.orientation is false, or { orientations: DeviceOrientation[] } with the
// poses that setOrientation accepts. client.orientation shows the choice at once, then the
// backend's report; read client.orientationPending and client.orientationError like location.
client.setOrientation("landscape_left");
// Observed iOS devices separate app screenOrientation from physical deviceOrientation.
// A null observation is unknown; omitted fields mean the helper lacks this contract.
// screen.orientation follows the app interface in this mode; displayScreen/streamGeometry
// use physical pose for framing and map video/input accordingly. Duo and older helpers
// retain their existing geometry. client.orientation follows physical pose on observed iOS,
// including Duo, so portrait-only apps do not reset the Rotate cursor;
// Native-pose Duo keeps its clockwise physical order; older Duo uses legacy names.
// Ordinary observed iOS uses CLOCKWISE_ORIENTATIONS; older helpers use DEVICE_ORIENTATIONS.
// framesUpright means the backend already sends upright frames (serve-emu).
// While an Android recording could break, client.recordingLock is "status-unknown" or
// "recording" and rotate, setOrientation and the stream setters are silently ignored. Disable
// those controls from it. client.orientationUnavailable gives the reason orientation cannot
// change now, also while the iOS input socket cannot take input.
client.reload(); // reload the running React Native bundle
client.setAppearance("dark"); // 'light' | 'dark'; read it back from client.appearance

// { blob, artifact, metadata? }, or null when capture fails. `artifact` is the session artifact
// outcome: { status: 'saved' } | { status: 'disabled' } | { status: 'failed', error?: string }
// | null (null for a backend that does not report one). `metadata` is the capture's
// { width, height, displayRotation, screen? }; it is absent on older servers, and a zero width
// or height or a null displayRotation means unknown.
const capture = await client.screenshot();
if (capture?.artifact?.status === "failed")
  console.warn("not saved to session artifacts", capture.artifact.error);

// Input is normalized to 0..1 of the screen, so it works for every device size.
client.sendTouch({ phase: "begin", x: 0.5, y: 0.5 });
client.sendTouch({ phase: "end", x: 0.5, y: 0.5 });
client.sendKey({ phase: "down", code: "KeyA", key: "a", repeat: false });
client.sendKey({ phase: "up", code: "KeyA", key: "a", repeat: false });

// Logs are off until you attach them.
client.attachLogs();
client.logs; // DeviceLog[]
client.detachLogs();

设备设置、相机种子、无障碍树、位置、, 和应用程序权限只在某些后端上可用. 检查 client.capabilities 在此之前 你显示他们的控制。 全数 DeviceClient 对每个字段都有评论的合约是: 内 src/types.ts。 。 。 .

远程连接

设定 options.baseUrl 打开 DeviceClientProvider 到远程流线服务器 :

  • iOS: (英语) https://hub.example.test/device-hub/vendor/serve-sim
  • 机器人: https://hub.example.test/device-hub/vendor/serve-emu

iOS 嵌入

将 iOS 屏幕嵌入到另一个站点时, 通过服务对象的 公用 service- sim Mount /api 财务报告和审定财务报表 /helper 作为 baseUrl,例如 https://sim.example.test/preview/session。 。 。 . 流和输入 URL 然后使用该服务器。 开始服务图像 --cors-origin <origin> 用于嵌入页的出处,例如 --cors-origin http://localhost:8081没有它,执行-ws套接字关闭,和日志, 事件、参数和用户界面请求停止,即使两个服务器运行 localhost。 。 。 .

前景应用图标不需要exec-ws. 中心客户端从服务器上读取 /api/apps/icon 路径,一个普通的获取,何时 /api 宣传路线。 旧服务器发送 图标.

开始一个服务图像 --require-token,例如EAS模拟预览会话,需要 它的会话在每次请求时都具有象征意义。 通过它作为 options.token数字 :

<DeviceClientProvider
  key={udid}
  platform="ios"
  options={{
    baseUrl: "https://sim.example.test/preview/session",
    device: udid,
    streamMode: "mjpeg",
    token,
  }}
>
  <Screen />
</DeviceClientProvider>

符号不替换 --cors-origin。 。 。 .

许可证

麻省理工学院

版本与发布记录

@expo/hub-client 共发布 169 个版本,首次发布于 2026/09/16;过去 12 个月发布了 169 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.12.2latest2026/11/14今天
1.12.2-canary-20261113-1e5f890canary2026/11/14今天
1.12.2-canary-20261113-e1691412026/11/14今天
1.12.2-canary-20261113-ab2ba422026/11/131 天前
1.12.2-canary-20261113-9ea899f2026/11/131 天前
1.13.0-canary-20261112-7e85a192026/11/121 天前

关于 @expo/hub-client 的常见问题

@expo/hub-client 是什么?

设备客户端反应钩和类型,用于在浏览器中通过Expo Device Hub镜像活iOS模拟器和Android模拟器.

如何安装 @expo/hub-client?

在项目目录运行 npm install @expo/hub-client,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@expo/hub-client 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@expo/hub-client 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@expo/hub-client 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@expo/hub-client 可以商用吗?

@expo/hub-client 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@expo/hub-client 的下载量有多少?

根据 npm 官方下载统计,@expo/hub-client 在 2026/10/14 – 2026/11/12 这 30 天内被下载 56,297 次,日均约 1,877 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@expo/hub-client 还在维护吗?

最新版本 1.12.2 发布于 2026/11/14(今天);至今共发布 169 个版本,其中过去 12 个月发布了 169 个;首次发布于 2026/09/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。