# @expo/hub-client 中文文档

规范地址：https://npmcn.com/package/%40expo/hub-client/

版本：1\.12\.2
npm 元数据更新时间：2026-11-13T20:35:53.849Z
中文译文更新时间：2026-11-14T05:29:32.179Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40expo/hub-client
原文对照：https://npmcn.com/package/%40expo/hub-client/?lang=original
项目主页：https://github.com/expo/expo-device-hub/tree/main/packages/@expo/hub-client
源码仓库：https://github.com/expo/expo-device-hub

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：2.9 MB，558 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=18
- 运行依赖：1 个
- peer 依赖：react \>=18\.0\.0
- 首次发布：2026-09-16T11:30:32.820Z
- 最新版本发布：2026-11-13T20:35:53.390Z
- 近 30 天下载量：56297（2026-10-14 至 2026-11-12，npm 官方下载统计）
- 已发布版本数：169（近 12 个月 169 个）

## 中文简介

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

## 安装

```sh
npm install @expo/hub-client
```

也可以使用：`pnpm add @expo/hub-client`、`yarn add @expo/hub-client`、`bun add @expo/hub-client`

## README

# @ expo/ hub- 客户端

React app中的Render活iOS模拟器和Android模拟器. 连接通过 [博览设备枢纽](https://github.com/expo/expo-device-hub)设备 `DeviceScreen`,并发送触控,键盘,设备控制.

## 所需资源

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

## 安装

```sh
npm install @expo/hub-client
```

## 连接到设备

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

### 添加提供者

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

```tsx
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()` 电话 :

```tsx
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'` 包括空设置.

```tsx
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`。 。 。 .

```tsx
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` 不要捆绑形状.

```tsx
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` 也覆盖客户端,以及 没有客户,他们是唯一的来源,例如录音:

```tsx
<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` 当设备不提供折叠命令时。 只发送它列出的.

```tsx
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`) (中文(简体) ).

```tsx
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百分比变化时更新,即使内存或网络读取有变化.

```tsx
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仪表板本身不需要额外的: 打开与符号的链接 浏览器是覆盖每个请求的饼干。 在另一个来源的一页 没有这样的饼干, 因此,把信使交给提供者:

```tsx
<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` 和您的页面来源, 每个来源一次:

```sh
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`控制是无操作的,而没有任何连接, 而每个后端都会忽略其平台没有的按钮.

```ts
// 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`](https://npmcn.com/package/%40expo/hub-client/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`数字 :

```tsx
<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 是什么？

设备客户端反应钩和类型,用于在浏览器中通过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 和提交记录判断。

