# Component 开发文档 Component 由两部分构成,一个是 Spec,一个是 Implementation。如果把 Component 想象成一个类的话,Spec 就是类的接口,Implementation 就是类的实现。 - Spec:用来描述 Component 的元信息、参数、状态、行为的一个数据结构 - Implementation:具体负责渲染 HTML 元素的函数。 ## Component 开发教程 下面我们通过一个 Input Component 的例子,来学习如何开发一个 Component。这个 Input 组件有下面这些能力: - 可以配置 placeholder,disabled 等参数 - 可以让外界访问当前值 - 可以发出 onBlur 等事件 - 可以让外界更新自己的值。 - 可以插入子组件,如前后缀等等。 ### 编写 Component Spec Spec 本质上是一个 JSON,它的作用是描述组件的参数、行为等信息。我们上述的所有能力都将会体现在 Spec 中。 首先我们来看一下这个 Input Component Spec 示例: ```javascript { version: "arco/v1", metadata: { name: "input", displayName: "Input", exampleProperties: { placeholder: "Input here", disabled: false, }, }, spec: { properties: Type.Object({ placeholder: Type.String(), disabled: Type.Boolean(), }), state: Type.Object({ value: Type.String(), }), methods: { updateValue: Type.Object({ value: Type.String(), }), }, slots: { prefix: { slotProps: Type.Object({}), }, suffix: { slotProps: Type.Object({}), }, }, styleSlots: ["content"], events: ["onBlur"], }, }; ``` 一开始面对这么多字段可能会比较迷茫,下面我们来逐一解释每个字段的含义。 > 详细的每个参数的类型和说明可以参考后面的 API 参考。 #### Component Spec Metadata `metadata` 是一个 Component 的元信息,包括名称等信息。 首先我们来看一下 Spec 的完整的类型定义: #### Component Spec Properties `properties` 描述了 Component 能够接受的参数名称和类型。这里定义了两个参数,`placeholder`和`disabled` ,类型分别是 String 和 Boolean。 ```` properties: Type.Object({ placeholder: Type.String(), disabled: Type.Boolean(), }) ```` 你可能对这种声明类型的方法感到陌生。前文已经说过,Spec 本质是一个 JSON,但 JSON 不像 Typescript 可以声明类型,所以当我们要在 Spec 中声明类型时,我们使用 [JSONSchema](https://json-schema.org/)。JSONSchema 本身也是 JSON,但是可以用来声明一个 JSON 数据结构的类型。 但手写 JSONSchema 比较困难,所以我们推荐使用 [TypeBox](https://github.com/sinclairzx81/typebox) 库来辅助生成 JSONSchema。示例中的写法就是调用了 TypeBox。 #### Component Spec State `state`描述了 Component 暴露的状态。Input Component 只会暴露一个 `value`。定义方式和 `properties` 类似。 state: Type.Object({ value: Type.String(), }) #### Component Spec Method `methods` 描述了 Component 暴露的方法。我们的 Input 打算暴露 `updateValue` 方法,以便让外界更新自己的值。 在 Spec 中 `updateValue` 这个键所对应的值是 `updateValue`可以接受的参数,同样是用 TypeBox 定义的。 methods: { updateValue: Type.Object({ value: Type.String(), }), } #### Component Spec 的其他属性 `slots `代表 Component 预留的 Slot。每个 Slot 都可以插入子 Component 。其中 `slotProps`代表可以这个插槽会传递给子 Component 的额外的 props,以供子 Component 渲染时使用。 `styleSlots` 代表 Component 预留的可以插入样式的 Slot。一般每个 Component 都需要预留一个 `content` 的 styleSlot。 `events `代表 Component 可以发出的事件,以便外界监听。 slots: { prefix: { slotProps: Type.Object({}), }, suffix: { slotProps: Type.Object({}), }, }, styleSlots: ["content"], events: ["onBlur"], #### Component Spec 示例解析 现在我们再来看一下一开始的示例,对照着解释一遍。 ```javascript const InputSpec = { version: 'arco/v1', metadata: { name: 'input', displayName: 'Input', exampleProperties: { placeholder: 'Input here', disabled: false, }, }, spec: { properties: Type.Object({ placeholder: Type.String(), disabled: Type.Boolean(), }), state: Type.Object({ value: Type.String(), }), methods: { updateValue: Type.Object({ value: Type.String(), }), }, slots: { prefix: { slotProps: Type.Object({}), }, suffix: { slotProps: Type.Object({}), }, }, styleSlots: ['content'], events: ['onBlur'], }, }; ``` 这份 Spec 声明了一个 Input Component。它是 arco/v1 组件库的一部分,名字是 input。它的唯一标志符就是 arco/v1/input。 该 Component 的 properties 是用 TypeBox 声明的。它的 properties 包含两个参数,分别是 placeholder 和 disabled。它还会暴露一个状态 value 给外部访问。 在行为方面,它有一个 updateValue 方法,可以允许外部更新自己的 value。同时,作为一个 Input,它还会发出 onBlur 事件。 它还有两个 slot 可以插入子 Component,分别代表前缀和后缀。这两个 slot 没有要额外传递的 property。还有一个 content 的 styleSlot,可以添加自定义样式。 这就是这个 Input 组件的所有逻辑了,下面我们来看一下如何实现这个 Component 的 Implementation。 ### Component Implementation 完成了 Component 的 Spec 之后,我们就要开发 Component 的具体实现,我们称之为 Component Implementation。理论上说,一份 Spec 可以对应很多个 Component,就好比一个接口可以对应多个类的实现一样。 Component Implementation 负责具体的渲染工作。它本质上是一个函数。它的参数比较复杂,我们按照 Input Component 的实际需求逐一介绍。 > 目前 Component Implementation 必须是一个 React 函数式组件,但以后不一定是一个 React 组件。以后我们计划让 Sunmao 支持用任何技术栈的组件,只要这个函数返回 DOM 元素就可以。 #### 读取 Component 的参数 首先,Component Implementation 应该要接受 Spec 中定义的 `properties`,也就是 `placeholder` 和 `disabled`。我们可以从参数中直接获取。然后,我们把这个参数传递给一个 input 的 JSX 元素,并返回。 ```jsx const InputImpl = props => { const { disabled, placeholder } = props; return ; }; ``` 就这么简单!其实这已经是一个完整的 Component Implementation,但我们还有很多功能没有实现。 #### 暴露 Component 的状态 我们的 Input 将会暴露自己的状态,这就需要用到一个 Sunmao 内置的函数 `mergeState`。这个方法会被自动注入到 Component Implementation 中,可以像读取 `properties` 一样读取,调用方式如下。 ```tsx const InputComponent = props => { const { mergeState } = props; const [value, setValue] = useState(''); // 当 value 改变时,调用 mergeState 方法。 // 每次调用 mergeState 时,最新的 value 值将会合并到 Sunmao 状态树中,以供其他 Component 访问。 useEffect(() => { mergeState({ value, }); }, [mergeState, value]); return setValue(newVal)} />; }; ``` #### 暴露 Component 的方法 我们的 Input 还会暴露自己的方法 `updateValue`。这也需要用到一个内置的方法 `subscribeMethods`。 ```typescript const InputComponent = props => { const { subscribeMethods } = props; const [value, setValue] = useState(''); // 当dom元素挂载后,调用 subscribeMethods,注册 updateValue 方法。 // 这样外界的 Component 就可以调用 updateValue,来改变 input 的 value了。 useEffect(() => { subscribeMethods({ updateValue: ({ value: newValue }) => { setValue(newValue); }, }); }, [subscribeMethods]); return ; }; ``` #### 发布 Event 我们的 Input 还会发布 onBlur 事件,来通知其他 Component 自己失焦了。这就需要用到另一个内置参数 `callbackMap`。 ```jsx const InputComponent = props => { const { callbackMap } = props; // callbackMap 中已经带有了对应事件的回调函数,在对应的时机直接调用即可。 const onBlur = () => { if (callbackMap.onBlur) { callbackMap.onBlur(); } }; return ; }; ``` #### 预留 Slot 和 StyleSlot 的位置 Slot 和 StyleSlot 都是可以插入自定义内容的插槽,它们的位置需要事先预留。它们也同样需要对应的参数,分别是:`slotsElements`和`customStyle`。两个都是 js 对象,通过 slot 和 styleSlot 的名称就可以访问到对应的内容。 `slotsElements` 的内容一个函数,这个函数接受 `slotProps`,返回 JSX 元素。`slotProps` 是可选的,根据 spec 中的定义来传递。 `customStyle`是一个 styleSlot 和 CSS 字符串的 map。因为是 CSS 字符串,所以需要经过一定处理才能使用。我们推荐使用 `emotion` 作为 CSS-in-JS 的处理方案。 ```tsx const InputImpl = props => { const { slotsElements, customStyle } = props; return (
{slotsElements.prefix()} {slotsElements.suffix()}
); }; ``` > 其实`customStyle`和`callbackMap`其实都是来自于 Trait 的参数,但目前你并不需要知道这一点,具体可以参考后面的 API 文档。 #### 暴露 DOM 元素给 Sunmao #### elementRef & getElement 最后还有一步,这一步和 Component 自身的逻辑无关,但是 Sunmao 需要获取到 Componet 运行时的 DOM 元素,才能在 Editor 中获得这个组件。所以这一步需要把 Component 的 DOM 元素传递给 Sunmao。 Sunmao 提供了两个方法来传递 DOM 元素:`elementRef `和` getElement`。两个方法的作用是一样的,只是适合场景不同,只需选择一个实现即可。 如果 Component 是用 React 实现的,那么使用 `elementRef` 比较方便,只需要把 `elementRef` 传给 React 组件的 `ref `属性。如果这个方法不行,则只能用通用的 `getElement` 方法来注册组件的 DOM 元素了。 ```typescript const InputComponent = props => { const { getElement } = props; const ref = useRef(null); useEffect(() => { const ele = ref.current?.dom; if (getElement && ele) { getElement(ele); } }, [getElement, ref]); return ; }; // 或者 const InputComponent = props => { const { elementRef } = props; return ; }; ``` #### 完整的 Component Implementation 最后我们把所有功能结合起来,实现一开始的 Input Component Spec 的所有逻辑。 ```tsx const InputImpl = props => { const { disabled, placeholder, elementRef, slotsElements, customStyle, callbackMap, mergeState, subscribeMethods, } = props; const [value, setValue] = useState(''); useEffect(() => { mergeState({ value, }); }, [mergeState, value]); useEffect(() => { subscribeMethods({ updateValue: newValue => { setValue(newValue); }, }); }, [subscribeMethods]); const onChange = e => { setValue(e.target.value); }; const onBlur = () => { if (callbackMap.onBlur) { callbackMap.onBlur(); } }; return (
{slotsElements.prefix()} {slotsElements.suffix()}
); }; ``` ### 封装 Spec 和 Implementation 写完 Component 的 Spec 和 Implementation 以后,离成功只差最后一步,就是把二者封装成 Sunmao runtime 能接受的格式。这一步很简单,只需要调用 `implementRuntimeComponent` 函数即可。 ```javascript import { implementRuntimeComponent } from '@sunmao-ui/runtime'; const InputComponent = implementRuntimeComponent(InputSpec)(InputImpl); ``` 最后,这个组件添加到 lib 中即可,并在 Sunmao 启动时传给 `initSunmaoUI`就大功告成了。 ```javascript const lib: SunmaoLib = { components: [InputComponent], traits: [], modules: [], utilMethods: [], }; ``` ## Component API 文档 ### Component Spec Spec 的第一层字段比较简单明了。 | 参数名 | 类型 | 说明 | | -------- | ------------- | ----------------------------------------------------------------------------------------------------------- | | version | string | Component 在 Sunmao 中的分类。同一套 Component 的`version`通常是一样的。格式为 "xxx/vx" ,例如"`arco/v1`"。 | | kind | `"Component"` | 固定不变,表示这是一个 Component Spec。 | | metadata | | 详见下文 | | spec | | 详见下文 | #### Component Spec 的 Metadata Metadata 中包含了 Component 的元信息。 | 参数名 | 类型 | 备注 | | ----------------- | -------------------- | ------------------------------------------------------------------------------------- | | name | string | Component 的名字。Component 的 `version` 和 `name`共同构成了 Component 的唯一标志符。 | | description | string? | | | displayName | string? | 在 Editor 中的 Component 列表中展示的名字。 | | exampleProperties | Record | Component 在 Editor 中被创建时的初始 `properties`。 | | annotations | Record? | 可以自定义声明一些字段。 | #### Component Spec 其余字段  定义 `properties` 和 `state` 时,我们使用 [JSONSchema](https://json-schema.org/)。JSONSchema 本身也是 JSON,但是可以用来声明一个 JSON 数据结构的类型。有了类型的帮助,Sunmao 就可以对参数和表达式进行校验和和输入提示。 | 参数名 | 类型 | 备注 | | ---------- | --------------------------------------- | --------------------------------------------------------------------------- | | properties | JSONSchema | Component 接受的参数。 | | state | JSONSchema | Component 对外暴露的 State。 | | methods | Record | Component 对外暴露的 Method。key 是 Method 的名字,value 是 Method 的参数。 | | events | string[] | Component 会发出的 Event。数组元素是 Event 的名字。 | | slots | Record | Component 预留的可以插入子 Component 的插槽。 | | styleSlots | string[] | Component 预留的可以添加样式的插槽。 | ### Component Implementation 参数 Component Implementation 的参数本质上一个 object,但是其实是由好几个组成部分合并而成的。大致可以分为: - Component Spec 中声明的 Properties。这部分完全是 Component 自定义的。 - Sunmao Component API。这是 Sunmao 注入到 Component 中的。 - Trait 执行结果。这是 Trait 传递给组件的结果。 - services。这是 Sunmao 运行时的各个服务实例。 | 参数名 | 类型 | 备注 | 来源 | | --------------- | -------------------------------------------------------------- | ----------------------------------- | -------- | | component | ComponentSchema | Component 的 Schema | API | | app | ApplicationSchema | 整个 Application 的 Schema | API | | slotsElements | Record ReactElement[]> | 子 Component 列表,详见下文 | API | | mergeState | (partialState: object) => void | 详见下文 | API | | subscribeMethod | (methodsMap: Record void>) => void | 详见下文 | API | | elementRef | React.Ref | 详见下文 | API | | getElement | (ele: HTMLElement) => void | 详见下文 | API | | services | object | Sunmao 的各种服务实例,详见下文 | services | | customStyle | Record | 来自于 Trait 的自定义样式,详见下文 | Trait | | callbackMap | Record | 来自于 Trait 的回调函数,详见下文 | Trait | #### Services Services 是 Sunmao 的各种服务的实例,包括状态管理、事件监听、组件注册等等。这些 Service 都是全局唯一的实例。 | 参数名 | 类型 | 备注 | | ---------------- | ------------------------ | ------------------------------------------------------------ | | registry | Registry | Registry 上注册了 Sunmao 所有的 Component、Trait、Module,您可以在其中找到它们所对应的 Spec 和 Implementation。 | | stateManager | StateManager | StateManager 管理着 Sunmao 的全局状态 Store,而且还具 eval 表达式的功能。 | | globalHandlerMap | GlobalHandlerMap | GlobalHandlerMap 管理着所有 Component 的 Method 实例。 | | apiService | ApiService | ApiService 是全局事件总线。 | | eleMap | Map | eleMap 存放所有 Component 的 DOM 元素。 | > ⚠️ 一般情况下,您不需要使用这些服务。只有在实现一些特殊需求时,才可能会用到它们。 #### Sunmao Component API ##### `mergeState` Component 可以拥有自己的局部状态,但是如果 Component 把自己的局部状态暴露给 Sunmao 的其他组件,就要通过`mergeState`函数把状态合并到 Sunmao 的全局状态 store 中。 当`mergeState`被调用时,所有引用了该状态的表达式都会立刻更新,其对应的组件也会立即更新。 ##### `subscribeMethods` `subscribeMethods` 的作用就是把组件的行为,以函数的形式注册到 Sunmao 中,以供其他 Component 调用。 Component 注册的 Method 并没有限制,它可以接受自定义参数,参数类型应该已经 Component Spec 中声明过了。参数在调用时会由 Sunmao 负责传递。 #### Trait 执行结果 所有 Trait 的执行结果都会作为参数传递给 Component。这些参数都是按照约定的接口生成的。Trait 和 Component 之间只能通过这个接口进行交互。**Component 必须正确处理下面这些参数**,否则,Component 就不能和别的 Trait 交互。 ##### `customStyle` 在 Sunmao 中,样式的表现形式是 CSS。customStyle 是一个 styleSlot 和 CSS 的 map。您需要自己决定如何使用 CSS。Sunmao 使用的是 emotion 作为运行时的 CSS-In-JS 方案,您也可以选择自己喜欢的方案。 **我们约定一个 Component 必须要至少实现一个 `content`的 styleSlot,作为默认的 styleSlot。** ##### `callbackMap` `callbackMap` 是组件对外暴露事件的方式。它是一个 Event 名称和回调函数的 Map。如果有其他 Component 监听了某个 Component 的 Event,那么事件回调函数就会通过 `callbackMap` 传递给该 Component。您需要在 Event 对应的代码的位置调用这个回调函数,这样其他 Component 才能成功监听该 Component 的 Event。 #### Sunmao Runtime API Sunmao 不会限制 Component 内部的逻辑和实现方式,但是有一些接口必须要实现,否则 Component 将无法与 Sunmao 交互。这些接口都会以参数的方式传给 Component Implementation,参数如下: ##### slotsElements slotsElements 是每个 Slot 中的子 Component 的列表。Component 可以声明自己的 Slot,每个 Slot 就是子 Component 插入的位置。 >  如果 Component 只有一个 slot,我们约定这个 slot 名字是 content。 ##### elementRef & getElement 这两个 API 的作用是将 Component 渲染的 DOM 元素注册到 Sunmao 中。Sunmao 必须获取到每个 Component 的 DOM 元素才能实现一些功能,比如编辑器中高亮 Component 的功能。别的 Component 和 Trait 也可以利用 Component 的 DOM 元素实现功能。