sunmao-ui/docs/zh/component.md
2022-06-10 14:06:00 +08:00

22 KiB
Raw Permalink Blame History

Component 开发文档

Component 由两部分构成,一个是 Spec一个是 Implementation。如果把 Component 想象成一个类的话Spec 就是类的接口Implementation 就是类的实现。

  • Spec用来描述 Component 的元信息、参数、状态、行为的一个数据结构
  • Implementation具体负责渲染 HTML 元素的函数。

Component 开发教程

下面我们通过一个 Input Component 的例子,来学习如何开发一个 Component。这个 Input 组件有下面这些能力:

  • 可以配置 placeholderdisabled 等参数
  • 可以让外界访问当前值
  • 可以发出 onBlur 等事件
  • 可以让外界更新自己的值。
  • 可以插入子组件,如前后缀等等。

编写 Component Spec

Spec 本质上是一个 JSON它的作用是描述组件的参数、行为等信息。我们上述的所有能力都将会体现在 Spec 中。

首先我们来看一下这个 Input Component Spec 示例:

{
  version: "arco/v1",
  metadata: {
    name: "input",
    displayName: "Input",
    exampleProperties: {
      placeholder: "Input here",
      disabled: fasle,
    },
  },
  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 能够接受的参数名称和类型。这里定义了两个参数,placeholderdisabled ,类型分别是 String 和 Boolean。

你可能对这种声明类型的方法感到陌生。前文已经说过Spec 本质是一个 JSON但 JSON 不像 Typescript 可以声明类型,所以当我们要在 Spec 中声明类型时,我们使用 JSONSchema。JSONSchema 本身也是 JSON但是可以用来声明一个 JSON 数据结构的类型。

但手写 JSONSchema 比较困难,所以我们推荐使用 TypeBox 库来辅助生成 JSONSchema。示例中的写法就是调用了 TypeBox。

properties: Type.Object({
  placeholder: Type.String(),
  disabled: Type.Boolean(),
})

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 示例解析

现在我们再来看一下一开始的示例,对照着解释一遍。

const InputSpec = {
  version: 'arco/v1',
  metadata: {
    name: 'input',
    displayName: 'Input',
    exampleProperties: {
      placeholder: 'Input here',
      disabled: fasle,
    },
  },
  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,也就是 placeholderdisabled。我们可以从参数中直接获取。然后,我们把这个参数传递给一个 input 的 JSX 元素,并返回。

const InputImpl = props => {
  const { disabled, placeholder } = props;

  return <input disabled={disabled} placeholder={placeholder} />;
};

就这么简单!其实这已经是一个完整的 Component Implementation但我们还有很多功能没有实现。

暴露 Component 的状态

我们的 Input 将会暴露自己的状态,这就需要用到一个 Sunmao 内置的函数 mergeState。这个方法会被自动注入到 Component Implementation 中,可以像读取 properties 一样读取,调用方式如下。

const InputComponent = props => {
  const { mergeState } = props;
  const [value, setValue] = useState('');

  // 当 value 改变时,调用 mergeState 方法。
  // 每次调用 mergeState 时,最新的 value 值将会合并到 Sunmao 状态树中,以供其他 Component 访问。
  useEffect(() => {
    mergeState({
      value,
    });
  }, [mergeState, value]);

  return <input value={value} onChange={newVal => setValue(newVal)} />;
};

暴露 Component 的方法

我们的 Input 还会暴露自己的方法 updateValue。这也需要用到一个内置的方法 subscribeMethods

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 <input value={value} />;
};

发布 Event

我们的 Input 还会发布 onBlur 事件,来通知其他 Component 自己失焦了。这就需要用到另一个内置参数 callbackMap

const InputComponent = props => {
  const { callbackMap } = props;

  // callbackMap 中已经带有了对应事件的回调函数,在对应的时机直接调用即可。
  const onBlur = () => {
    if (callbackMap.onBlur) {
      callbackMap.onBlur();
    }
  };

  return <input onBlur={onBlur} />;
};

预留 Slot 和 StyleSlot 的位置

Slot 和 StyleSlot 都是可以插入自定义内容的插槽,它们的位置需要事先预留。它们也同样需要对应的参数,分别是:slotsElementscustomStyle。两个都是 js 对象,通过 slot 和 styleSlot 的名称就可以访问到对应的内容。

slotsElements 的内容一个函数,这个函数接受 slotProps,返回 JSX 元素。slotProps 是可选的,根据 spec 中的定义来传递。

customStyle是一个 styleSlot 和 CSS 字符串的 map。因为是 CSS 字符串,所以需要经过一定处理才能使用。我们推荐使用 emotion 作为 CSS-in-JS 的处理方案。

const InputImpl = props => {
  const { slotsElements, customStyle } = props;

  return (
    <div>
      {slotsElements.prefix()}
      <input className={css(customStyle.content)} />
      {slotsElements.suffix()}
    </div>
  );
};

其实customStylecallbackMap其实都是来自于 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 元素了。

const InputComponent = props => {
  const { getElement } = props;
  const ref = useRef(null);

  useEffect(() => {
    const ele = ref.current?.dom;
    if (getElement && ele) {
      getElement(ele);
    }
  }, [getElement, ref]);

  return <input ref={ref} />;
};

// 或者

const InputComponent = props => {
  const { elementRef } = props;

  return <input ref={elementRef} />;
};

完整的 Component Implementation

最后我们把所有功能结合起来,实现一开始的 Input Component Spec 的所有逻辑。

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 (
    <div>
      {slotsElements.prefix()}
      <input
        ref={elementRef}
        className={css(customStyle.content)}
        disabled={disabled}
        placeholder={placeholder}
        value={value}
        onChange={onChange}
        onBlur={onBlur}
      />
      {slotsElements.suffix()}
    </div>
  );
};

封装 Spec 和 Implementation

写完 Component 的 Spec 和 Implementation 以后,离成功只差最后一步,就是把二者封装成 Sunmao runtime 能接受的格式。这一步很简单,只需要调用 implementRuntimeComponent 函数即可。

import { implementRuntimeComponent } from '@sunmao-ui/runtime';

const InputComponent = implementRuntimeComponent(InputSpec)(InputImpl);

最后,这个组件添加到 lib 中即可,并在 Sunmao 启动时传给 initSunmaoUI就大功告成了。

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 的 versionname共同构成了 Component 的唯一标志符。
description string?
displayName string? 在 Editor 中的 Component 列表中展示的名字。
exampleProperties Record<string, any> Component 在 Editor 中被创建时的初始 properties
annotations Record<string, any>? 可以自定义声明一些字段。

Component Spec 其余字段

 定义 propertiesstate 时,我们使用 JSONSchema。JSONSchema 本身也是 JSON但是可以用来声明一个 JSON 数据结构的类型。有了类型的帮助Sunmao 就可以对参数和表达式进行校验和和输入提示。

参数名 类型 备注
properties JSONSchema Component 接受的参数。
state JSONSchema Component 对外暴露的 State。
methods Record<KMethodName, JSONSchema> Component 对外暴露的 Method。key 是 Method 的名字value 是 Method 的参数。
events string[] Component 会发出的 Event。数组元素是 Event 的名字。
slots Record<string, {slotProps: JSONSchema}> 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<string, (slotProps: any) => ReactElement[]> 子 Component 列表,详见下文 API
mergeState (partialState: object) => void 详见下文 API
subscribeMethod (methodsMap: Record<string, (params: object) => void>) => void 详见下文 API
elementRef React.Ref 详见下文 API
getElement (ele: HTMLElement) => void 详见下文 API
services object Sunmao 的各种服务实例,详见下文 services
customStyle Record<string, string> 来自于 Trait 的自定义样式,详见下文 Trait
callbackMap Record<string, Function> 来自于 Trait 的回调函数,详见下文 Trait

Services

Services 是 Sunmao 的各种服务的实例,包括状态管理、事件监听、组件注册等等。这些 Service 都是全局唯一的实例。

参数名 类型 备注
registry Registry Registry 上注册了 Sunmao 所有的 Component、Trait、Module您可以在其中它们所对应的 Spec 和渲染组件。
stateManager StateManager StateManager 管理着 Sunmao 的全局状态 Store而且还具 eval 表达式的功能。
globalHandlerMap GlobalHandlerMap GlobalHandlerMap 管理着所有 Component 的 Method 实例。
apiService ApiService ApiService 是全局事件总线。
eleMap Map<string, HTMLElement> 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 元素实现功能。