使用 registerComponent 组织屏幕
ink-cartridge 使用一棵屏幕树来组织你的页面结构:每个屏幕只能有一个父屏幕,但可以有多个子屏幕。通过 skip、gotoScreen 等方法可以在树中不断切换当前屏幕(本章只讲"如何把屏幕注册进树",导航方法见"屏幕导航"一章)。
这一章会了解 ink-cartridge 最核心的方法之一:registerComponent。
屏幕树:父子关系
在 ink-cartridge 中,每一个 React 组件都可以是一个屏幕。屏幕之间的关系由你声明,registerComponent 的 parent 参数决定了"这个屏幕的父亲是谁"。
例如下面的树:
主菜单
├── 设置
└── 关于主菜单没有父屏幕,它是根屏幕;设置、关于的父屏幕都是主菜单;- 导航时只能在父子之间或沿着树跳转(
skip向下、back向上、gotoScreen跨分支)。
前置知识
| 文章 | 为什么需要 |
|---|---|
| 快速开始 | 屏幕树与 Provider 的整体结构在这里建立 |
屏幕注册进树之后,需要把它真正渲染出来,这由两个组件配合完成:
ScenarioManagementProvider—— 屏幕系统的上下文 Provider,包裹整个应用:维护屏幕树的状态(当前路径、图层、模态层等)。defaultScreen指定应用启动时的初始屏幕(必须是已注册的组件,否则会抛错),fullScreen让屏幕占满终端高度。CurrentScreen—— 读取 Provider 的状态并渲染当前激活的屏幕。Provider 本身不渲染任何页面,两者必须搭配使用。
tsx
<ScenarioManagementProvider defaultScreen={Home} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>如果你需要使用键盘系统,
KeyboardProvider必须嵌套在ScenarioManagementProvider内部,否则键盘绑定会失效。
这里只作简要介绍,两者的完整细节将在后续文章详细讲解。
注册你的第一个屏幕
注册一个屏幕只需要一行代码:
tsx
import React from 'react';
import { Box, Text, render } from 'ink';
import {
CurrentScreen,
ScenarioManagementProvider,
registerComponent,
} from 'ink-cartridge';
function Home() {
return (
<Box flexDirection="column">
<Text bold>🏠 主页</Text>
</Box>
);
}
// 把 Home 注册为屏幕:没有声明 parent,所以它是根屏幕
// `Home` 不需要 props,所以模板可以省略(默认为 {})
registerComponent(Home);registerComponent 是模块级函数,可以在 .ts 或 .tsx 的任意位置调用(通常紧跟组件定义)。
注册之后,用 ScenarioManagementProvider 指定默认屏幕并渲染:
tsx
render(
<ScenarioManagementProvider defaultScreen={Home} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>
);运行后终端会显示 🏠 主页。其中:
component(第一个参数):当前屏幕是哪个组件。组件本身作为注册的唯一标识,同一个组件只能注册一次。template(第二个参数,可选):初始模板,用作屏幕的默认 props。导航时传入的 props 会与它合并。Home不需要 props,所以可以省略(默认为{})。
构建一棵分支树
下面注册一棵"主菜单 → 设置 / 关于"的树,演示一父多子:
tsx
import React from 'react';
import { Box, Text, render } from 'ink';
import {
CurrentScreen,
ScenarioManagementProvider,
registerComponent,
} from 'ink-cartridge';
function MainMenu() {
return (
<Box flexDirection="column">
<Text bold>🏠 主菜单</Text>
<Text>设置 / 关于 都是我的子屏幕</Text>
</Box>
);
}
registerComponent(MainMenu, {});
function Settings() {
return <Text>⚙️ 设置</Text>;
}
// parent 指向 MainMenu:Settings 成为 MainMenu 的子屏幕
registerComponent(Settings, {}, { parent: MainMenu });
function About() {
return <Text>ℹ️ 关于</Text>;
}
// About 同样挂在 MainMenu 下面
registerComponent(About, {}, { parent: MainMenu });
render(
<ScenarioManagementProvider defaultScreen={MainMenu} fullScreen>
<CurrentScreen />
</ScenarioManagementProvider>
);注册完成后,屏幕树长这样:
MainMenu(根屏幕)
├── Settings
└── About注意注册顺序:先注册父屏幕,再注册子屏幕。如果 parent 尚未注册,registerComponent 会直接抛错(见下文注意事项第 2 条)。
API 参考
registerComponent 的函数签名:
typescript
function registerComponent<C extends React.ComponentType<any>>(
component: C,
template?: React.ComponentProps<C>,
options?: RegisterOptions,
): voidcomponent{React.ComponentType<any>} 注册为屏幕的 React 组件,同时作为注册的唯一标识。[template]{React.ComponentProps<C>} 初始模板,即屏幕的默认 props;导航时与传入的 props 合并。 默认值:{}。[options][parent]{React.ComponentType<any>} 父屏幕组件;不声明则该屏幕是根屏幕(候选)。
RegisterOptions 的完整定义:
typescript
interface RegisterOptions {
parent?: ComponentType<any> | undefined;
}注意事项
- 同一个组件只能注册一次。 重复调用
registerComponent会抛出错误:[Ink-Cartridge] Component "xxx" is already registered. Duplicate registration is not allowed.(避免在热更新、循环等场景中重复注册。) parent必须先注册。 声明parent时,该父组件必须已经注册过,否则抛错并提示你先注册父组件:Register the parent first with registerComponent(...)。- 不声明
parent即根屏幕。 根屏幕没有父节点,通常作为defaultScreen传入ScenarioManagementProvider,作为应用的默认页和主页。 - 可以有多个根屏幕。 每个没有
parent的组件都是一棵独立树的根,彼此互不影响,各自拥有自己的子树。 template是默认 props,不是"当前值"。 它只描述屏幕被创建时的默认属性;导航时传入的 props 会与之合并,template本身不会被修改。它是可选的——省略等同于传{}。
