学习普通图层的概念,了解 openLayer 等方法
在我们的真实应用中,有许多悬浮在屏幕上的工具面板、看板、工具栏、弹窗等。Ink V7 引入了绝对定位功能,并允许 Box 容器脱离文档流,这为 ink-cartridge 实现图层提供了入口。基于此,ink-cartridge 提供了一个完整的图层系统。
图层是悬浮在屏幕之上、独立于屏幕树的容器。它不参与导航,始终渲染在当前屏幕之上,多个图层之间依靠 zIndex 决定堆叠与交互的优先级——zIndex 越大,视觉上越靠前,键盘与鼠标的优先级也越高。
图层系统分为两类:
- 普通图层:通过
openLayer开启、closeLayer关闭,适合工具面板、看板、工具栏等常驻浮层; - 模态层:通过
openModalLayer开启、closeModalLayer关闭,渲染在普通图层之上,且只有zIndex最高的模态层能接收键盘事件,适合弹窗、确认框等需要独占键盘的场景。
开启一个图层后,还需要用 applyElement 向图层内挂载元素,用 eraseElement 移除元素;activateElement / deactivateElement 控制元素的键盘是否激活;closeAllLayer 一次性关闭所有普通图层。
本章聚焦普通图层:先介绍图层的基本概念,再逐个讲解 openLayer、applyElement、closeLayer 等核心方法。
前置知识
| 文章 | 为什么需要 |
|---|---|
| 组织屏幕 | 图层悬浮在屏幕之上,先要知道屏幕是什么 |
| 屏幕导航 | 图层能否跨页保留与跳转直接相关 |
图层的概念
在开始之前,先弄清一个关键事实:openLayer 只是开启了一个空容器,它本身不会渲染任何内容。一个图层要真正显示出来,需要两步:
- 开启图层:用
openLayer向屏幕系统注册一个图层; - 挂载元素:用
applyElement把组件挂载进这个图层。
图层本身是一个铺满终端的绝对定位容器。CurrentScreen 会按照当前屏幕 → 普通图层 → 模态层的顺序渲染:每个图层先渲染成一整块铺满终端的容器,容器内的元素(也就是你用 applyElement 挂载进去的组件)再借助 Ink V7 的绝对定位把自己摆在屏幕的任意位置。
图层是独立于屏幕树的。它不参与 skip / back / gotoScreen 的导航,而是始终悬浮在当前屏幕之上。多个图层之间依靠 zIndex 决定堆叠顺序与交互优先级:zIndex 越大,视觉上越靠前,键盘与鼠标的优先级也越高。
下面是最小可运行的例子:在 Home 屏幕上按 a 键,开启一个工具面板图层。
import React, { useEffect } from 'react';
import { Box, Text, render } from 'ink';
import {
CurrentScreen,
KeyboardProvider,
ScenarioManagementProvider,
registerComponent,
useKeyboard,
useScreenSystem,
} from 'ink-cartridge';
function Home() {
const { openLayer, applyElement } = useScreenSystem();
const { boundKeyboard } = useKeyboard();
useEffect(() => {
// 按 a 键开启图层,并往图层里挂载一个元素
return boundKeyboard(['a'], () => {
openLayer('tool-panel', 10);
applyElement('tool-panel', {
element: ToolPanel,
elementId: 'tool-panel-body',
});
});
}, [boundKeyboard, openLayer, applyElement]);
return (
<Box flexDirection="column">
<Text bold>🏠 Home</Text>
<Text>按 a 开启工具面板</Text>
</Box>
);
}
registerComponent(Home, {});
// 图层内的元素也是一个普通的 React 组件,用绝对定位把自己悬浮起来
function ToolPanel() {
return (
<Box
position="absolute"
top={2}
left={30}
width={30}
height={8}
borderStyle="bold"
borderColor="yellow"
>
<Text>🧰 工具面板</Text>
</Box>
);
}
render(
<ScenarioManagementProvider defaultScreen={Home} fullScreen>
<KeyboardProvider>
<CurrentScreen />
</KeyboardProvider>
</ScenarioManagementProvider>
);按下 a 键后,终端右上角会出现一个黄色边框的浮动面板,悬浮在 Home 屏幕之上。其中:
openLayer('tool-panel', 10)开启了一个名为tool-panel的图层,zIndex为 10;applyElement('tool-panel', { element: ToolPanel, elementId: 'tool-panel-body' })把ToolPanel组件挂载进这个图层;ToolPanel没有调用registerComponent,它只是一个普通组件,通过绝对定位把自己摆到了右上角。
通过 useScreenSystem() 的 allLayers 可以随时读取当前已开启的所有普通图层,常用于判断某个图层是否已经存在。
使用 openLayer 方法开启一个图层
openLayer 的签名:
openLayer(layerId: string, zIndex: number, options?: LayerOptions): voidlayerId{string} 图层的唯一标识,同一个 ID 只能开启一次。zIndex{number} 图层的优先级;越大越靠前,键盘与鼠标优先级也越高。[options]{LayerOptions} 可选配置,见下。
LayerOptions 的可选字段:
[crossPage]{boolean} 为true时,图层在页面切换(skip/back/gotoScreen)时不会被自动清除。 默认值:false。[automaticTakeoverKeyboard]{boolean | ComponentType[]} 控制图层键盘绑定的作用范围(详见后续文章)。 默认值:false。
Note:
openLayer只开启一个空容器。开启之后还需要用applyElement挂载元素,否则屏幕上不会显示任何内容。
使用 applyElement 方法向图层挂载元素
applyElement 把元素挂载进一个已开启的图层:
applyElement<C extends ComponentType<any>>(targetLayerId: string, layerElement: LayerElementInput<C>): voidlayerElement 需要提供:
element{ComponentType<C>} 挂载进图层的组件。elementId{string} 元素在图层内的唯一标识。[props]{ComponentProps<C>} 传给元素的 props,与skip()的params一样会做类型校验。[active]{boolean} 为false时元素暂停接收键盘事件。 默认值:true。
props 的类型会严格校验:传入的 props 必须与 element 组件声明的 props 一致,否则编译期就会报错。例如:
applyElement('tool-panel', {
element: ToolPanel,
elementId: 'tool-panel-body',
props: { title: '我的工具' }, // 必须与 ToolPanel 的 props 类型一致
});function ToolPanel({ title }: { title: string }) {
return (
<Box
position="absolute"
top={2}
left={30}
width={30}
height={8}
borderStyle="bold"
borderColor="yellow"
>
<Text>{title}</Text>
</Box>
);
}使用 LayerElementContext 读取图层信息
挂载进图层的组件,会被包裹在 LayerElementContext 的 Provider 中。组件内部可以通过 useContext(LayerElementContext) 读取自己所属图层的信息,例如图层的 ID。
import React, { useContext, useEffect } from 'react';
import { Box, Text } from 'ink';
import { LayerElementContext, useKeyboard, useScreenSystem } from 'ink-cartridge';
function ToolPanel() {
const { closeLayer } = useScreenSystem();
const { boundKeyboard } = useKeyboard();
const layerCtx = useContext(LayerElementContext);
useEffect(() => {
// 按 Escape 键关闭自己所在的图层
return boundKeyboard(['escape'], () => {
if (layerCtx?.layer) {
closeLayer(layerCtx.layer.layerId);
}
});
}, [boundKeyboard, closeLayer, layerCtx]);
return (
<Box
position="absolute"
top={2}
left={30}
width={30}
height={8}
borderStyle="bold"
borderColor="yellow"
>
<Text>🧰 工具面板(按 Esc 关闭)</Text>
</Box>
);
}layerCtx.layer 是当前元素所属的图层对象,layerCtx.layer.layerId 就是它的 ID。借助它,一个元素不需要提前知道自己在哪个图层里,就能在回调里拿到自己的图层并执行操作。
使用 closeLayer 方法关闭图层
closeLayer 关闭一个图层,并清空它上面的所有元素:
closeLayer('tool-panel');关闭后,图层里挂载的所有元素都会随之消失。对未注册的 ID 调用 closeLayer 是无操作(开发环境会给出告警)。
使用 eraseElement 与 closeAllLayer 移除元素
eraseElement 只移除图层里的单个元素,图层本身仍然保留:
// 移除 tool-panel 图层里的 tool-panel-body 元素,图层保持开启
eraseElement('tool-panel', 'tool-panel-body');closeAllLayer 一次性关闭所有普通图层:
closeAllLayer();如果同一时间开启了多个图层,closeAllLayer 可以快速清理全部内容。
API 参考
| 方法 | 签名 | 说明 |
|---|---|---|
openLayer | openLayer(layerId, zIndex, options?) | 开启一个普通图层 |
applyElement | applyElement(targetLayerId, layerElement) | 向图层挂载一个元素 |
closeLayer | closeLayer(targetLayerId) | 关闭一个图层及其全部元素 |
eraseElement | eraseElement(targetLayerId, targetElementId) | 从图层移除单个元素 |
closeAllLayer | closeAllLayer() | 关闭所有普通图层 |
activateElement | activateElement(targetLayerId, targetElementId) | 重新激活元素的键盘事件(见后续文章) |
deactivateElement | deactivateElement(targetLayerId, targetElementId) | 暂停元素的键盘事件(见后续文章) |
注意事项
layerId必须唯一。 重复openLayer同一个 ID 是无操作(开发环境会告警),需要先closeLayer才能重新开启。普通图层与模态层共用 ID 命名空间,复用模态层的 ID 会直接抛错。- 必须先
openLayer,再applyElement。 向未开启的图层挂载元素会抛错,错误信息会提示你先调用openLayer注册图层。 elementId在同一图层内必须唯一。 重复applyElement同一个elementId是无操作(开发环境会告警)。openLayer只开启一个空容器。 没有挂载任何元素时,图层不会显示任何内容。- 普通图层默认会在页面切换时被清除。 执行
skip/back/gotoScreen时,crossPage为false的图层会被自动清除;设置为true即可跨页面保留(详见后续文章)。 - 图层方法既可以作为钩子使用,也可以模块级导入。 上述方法既可以从
useScreenSystem()获取,也可以从ink-cartridge直接导入使用。
