71 Star 182 Fork 94

UBML / farris-vue

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
how_to_develop_component.md 21.07 KB
一键复制 编辑 原始数据 按行查看 历史
cassiel 提交于 2024-03-08 10:31 . !670merge latest data

如何开发 Farris Vue 低代码元组件

下面是参与贡献 Farris UI 的共享指南,请在反馈issuePull Request之前,花费几分钟阅读以下内容。 你也可以根据自己的实践经验,自由的通过Pull Request修改完善这个指南。

准备工作

  1. Fork 属于你的代码仓库。

如下图所示,在 Farris Vue 项目首页,点击右上脚的「Fork」按钮,创建属于自己的项目副本。 示例页面

  1. 在Fork创建的个人项目中,创建新组件特性分支。

如下图所示,首先进入个人命名空间下的 Farris Vue 项目,然后点击左侧的「main」分支下拉框,在弹出的分支面板中点击「新建分支」创建新组件特性分支。 需要注意的是,特性分支以feature/{新特性}的方式命名。 示例页面

  1. Clone 特性分支代码至本地。 如下图所示,首先点击「克隆/下载」按钮,系统弹出Clone项目操作页面。 示例页面 如下图所示,可以选择自己熟悉的方式,Clone下载项目源代码。 示例页面 例如
git clone https://gitee.com/farris-design/farris-vue.git
  1. 同步 Farris Vue 主项目代码 目前 Farris Vue 项目正处于活跃发展中,每天都会合并新特性PR,提交代码前,可以点击下方的刷新按钮,将个人命名空间下的 Farris Vue 项目与主项目同步一致。 示例页面

  2. 安装依赖包 在本地运行项目前,请先执行以下命令,检查环境中是否已经安装 yarn。

yarn -v

如果未得到yarn版本信息,请参考安装 yarn.

然后在 farris-vue 目录下使用yarn安装依赖包。

cd farris-vue
yarn
  1. 预览组件 首先,进入 farris vue 组件子项目目录。
cd packages/ui-vue

然后,同步npx执行vite命令编译预览项目。 例如:

 npx vite dev --open=#designer-canvas/drag-over

需要说明的是--open参数,其后的值为预览demo路由,#designer-canvas/drag-over是可视化低代码设计器画布demo的路由。 更多示例demo路由,请参见:packages/ui-vue/src/app.vue中的routes变量。

新建元组件

1. 新建目录结构

farris vue 组件代码保持在packages/ui-vue/components目录下,开发者可以在此目录为新组件创建存储目录。 以下是组件目录结构。

input-group
├── test                                    // 单元测试代码目录
|  └── input-group.spec.tsx
├── src                                     // 源代码目录
|  ├── components                           // 子组件元代码目录
|  |  ├── appended-button.component.tsx     // 附加按钮组件
|  |  └── text-edit.component.tsx           // 文本编辑器组件
|  ├── composition                          // 组件的可复用逻辑
|  |  ├── types.ts                          // 组合式Api返利值接口类型
|  |  ├── use-append-button.ts              // 实现组件特性「附加按钮」的组合式Api
|  |  ├── use-clear.ts                      // 实现组件特性「清空文本」的组合式Api
|  |  ├── use-password.ts                   // 实现组件特性「显示密码」的组合式Api
|  |  └── use-text-box.ts                   // 实现组件特性「文本框」的组合式Api
|  ├── designer                             // 设计器组件
|  |  └── input-group.design.component.ts   // 用于可视化设计器画布的设计时组件
|  ├── schema                               // 元组件 schema 描述目录
|  |  ├── input-group.schema.ts             // 以Json-Schema模式描述的统一UI组件JSON结构
|  |  └── schema-mapper.ts                  // schema 与 props 映射关系
|  ├── input-group.component.tsx            // 组件代码
|  └── input-group.props.ts                 // 定义组件Api
└── index.ts                                // 组件入口文件

2. 实现组件代码

如果你成功领取了项目Issue,请通过Gitee推荐的「fork + pull request」的方式贡献代码。 为了保证项目代码质量,我们指定了详细的编码风格指南。 为了你的PR可以顺利通过代码审查,请在编码前认真阅读以下编码指南

3. 描述组件低代码schema结构

首先,在组件源代码目录下,建立名称为schema的目录,在其下以{组件名}.schema.ts命名组件schema描述文件。 我们采用JSON Schema规范描述组件schema结构。 JSON Schema规范参见:JSON Schema 规范(中文版)

需要注意的是,除了遵循JSON Schema规范描述组件schema之外,还需要遵循以下约定。

  • https://farris-design.gitee.io/{组件名}.schema.json作为命名空间,标识组件schema。
  • 其中{组件名}采用「kebab-case」命名方法,例如:button-editsplitter-panel等。
  • 采用「kebab-case」命名规范的组件名,命名组件schema的title。
  • 注意:schema中的title务必使用「kebab-case」名份规范的组件名,此处约定用于后续动态解析构造组件props等结构。 例如:
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://farris-design.gitee.io/button-edit.schema.json",
    "title": "button-edit",

properties节点下描述组件schema结构,要求所有组件必须具有idtype两个属性。 其中,必须指定type属性等默认值,即其default属性的值为「kebab-case」命名规范的组件名。 例如:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://farris-design.gitee.io/button-edit.schema.json",
    "title": "button-edit",
    "description": "A Farris Input Component",
    "type": "object",
    "properties": {
        "id": {
            "description": "The unique identifier for a Buttton Edit component",
            "type": "string"
        },
        "type": {
            "description": "The type string of Button Edit component",
            "type": "string",
            "default": "button-edit"
        }
    },
    "required": [
        "id",
        "type"
    ]
}

同理,采用以上规则描述完整组件的属性结构,在default属性中描述每个属性的默认值。 完成的button-edit组件schema结构,参见:Button Edit 组件 Schema 结构

4. 建立Schema结构与props映射关系

默认情况下,开发者可以按照与组件props结构一致的方式描述组件的Schema结构。 因为Schema结构拥有抽象描述组件,并广泛的应用与可视化设计器、属性编辑器、解释渲染引擎、代码生成引擎等公共组件,其结构具有普遍性,与组件props结构会出现结构性差异。 开发者需要实现schema-mapper.ts进行结构映射。 例如: 在组件schema结构中采用apperance属性描述组件的自定义class和自定义style,在组件的props中使用customClass描述自定义class。

schema结构片段

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://farris-design.gitee.io/button-edit.schema.json",
    "title": "button-edit",
    "description": "A Farris Input Component",
    "type": "object",
    "properties": {
        "appearance": {
            "description": "",
            "type": "object",
            "properties": {
                "class": {
                    "type": "string"
                },
                "style": {
                    "type": "string"
                }
            },
            "default": {}
        }
    }
}

props结构片段

export const buttonEditProps = {
    /**
     * 组件自定义样式
     */
    customClass: { type: String, default: '' }
};

此时需要在schema目录下,新建文件名为schema-mapper.ts的映射文件。 其内容为:

import { MapperFunction, resolveAppearance } from '../../../dynamic-resolver';

export const schemaMapper = new Map<string, string | MapperFunction>([
    ['appearance', resolveAppearance]
]);

对于apperance属性,项目内提供了默认映射方法,可以引用dynamic-resolver下的resolveAppearance方法。

对于仅属性名不一样的值映射,可以直接在Map对象中声明schema属性名和props属性名。 例如,在section组件的schema中使用expanded描述展开状况,在props中使用expandStatus描述展开状态。

对于复杂的映射关系,可以使用匿名函数完成映射,例如:在section组件的schema中使用title描述标题,在props中使用mainTitle描述标题。

完整示例如下:

import { resolveAppearance, MapperFunction } from '../../../dynamic-resolver';

export const schemaMapper = new Map<string, string | MapperFunction>([
    ['appearance', resolveAppearance],
    ['expanded', 'expandStatus'],
    ['title', (key: string, value: any) => ({ 'mainTitle': value })]
]);

5. 实现可视化设计器使用的设计时组件

设计时组件与组件本身不同,其仅应用于低代码可视化设计器,可以在其中实现「添加子元素」、「配置属性」等可视化设计器独有的交互。 在本项目中,需要独立提供设计时组件。设计时组件可以是精简交互的完整组件。

首先,在组件源代码目录,建立目录名为designer的目录,存储设计时组件源代码。 然后,以{组件名}.design.component.tsx命名,创建设计时组件文件。其中,组件名遵循「kebab-case」命名规范。

需要注意的是,除实现设计时组件展现和交互逻辑外,需要额外引入支持可视化设计器拖拽布局的结构。 以基本布局容器组件content-container为例:

  1. 首先,需要定义名称为elementRefRef类型对象,并将器绑定至可拖拽布局的容器。
  2. 然后,需要在允许拖拽布局的容纳DOM节点上添加drag-containerclass。
  3. 接下来,需要在在允许拖拽布局的容纳DOM节点上添加dragref属性,其值为{组件标识}-container,在项目中可以通过designItemContext.schema.id或者组件标识。 此部分示例代码片段如下:
export default defineComponent({
    name: 'FContentContainerDesign',
    props: contentContainerProps,
    emits: [],
    setup(props: ContentContainerPropsType, context: SetupContext) {
        const elementRef = ref();
        const designItemContext = inject<DesignerItemContext>('design-item-context') as DesignerItemContext;

        return () => {
            return (
                <div ref={elementRef} class="drag-container" dragref={`${designItemContext.schema.id}-container`}>
                    {context.slots.default && context.slots.default()}
                </div>
            );
        };
    }
});
  1. 接下来,需要定义可视化拖拽校验规则,在校验规则中,验证那些类型的元素允许拖入当前容器中。 这部分的内容具有很强的自定义色彩,可以参考下面的示例代码,实现UseDesignerRules接口。 此部分内容为缺省内容,对可以容纳任何组件的容器,可以不提供。 以基本布局容器组件content-container为例,其「可视化拖拽校验规则」参见:use-designer-rules.ts

  2. 创建供可视化设计器使用设计时组件实例。 项目提供了创建可视化设计时组件实例的公共方法,可以使用designer-canvas下的use-designer-component创建设计时组件实例。 改方法需要接受三个参数:组件Html元素、可视化拓展元素上下文、可视化拖拽校验规则,其中: 组件Html元素 - 由开发者自行声明。 可视化拓展元素上下文 - 由开发者调用inject方法从上下文环境中,使用依赖注入获取。 可视化拖拽校验规则 - 为缺省参数。

开发者创建完设计时组件实例后,需要在onMounted事件将其记录至组件元素,需要使用expose方法将其公开。

此部分示例代码片段如下:

        const elementRef = ref();
        const designItemContext = inject<DesignerItemContext>('design-item-context') as DesignerItemContext;
        const designerRulesComposition = useDesignerRules(designItemContext.schema, designItemContext.parent?.schema);
        const componentInstance = useDesignerComponent(elementRef, designItemContext, designerRulesComposition);

        onMounted(() => {
            elementRef.value.componentInstance = componentInstance;
        });

        context.expose(componentInstance.value);
  1. content-container为例的完整设计时组件代码
import { SetupContext, defineComponent, inject, onMounted, ref } from 'vue';
import { ContentContainerPropsType, contentContainerProps } from '../content-container.props';
import { useDesignerRules } from './use-designer-rules';
import { DesignerItemContext } from '../../../designer-canvas/src/types';
import { useDesignerComponent } from '../../../designer-canvas/src/composition/use-designer-component';

export default defineComponent({
    name: 'FContentContainerDesign',
    props: contentContainerProps,
    emits: [],
    setup(props: ContentContainerPropsType, context: SetupContext) {
        const elementRef = ref();
        const designItemContext = inject<DesignerItemContext>('design-item-context') as DesignerItemContext;
        const designerRulesComposition = useDesignerRules(designItemContext.schema, designItemContext.parent?.schema);
        const componentInstance = useDesignerComponent(elementRef, designItemContext, designerRulesComposition);

        onMounted(() => {
            elementRef.value.componentInstance = componentInstance;
        });

        context.expose(componentInstance.value);

        return () => {
            return (
                <div ref={elementRef} class="drag-container" dragref={`${designItemContext.schema.id}-container`}>
                    {context.slots.default && context.slots.default()}
                </div>
            );
        };
    }
});

6. 创建动态解析props的解析器

在低代码场景下,低代码引擎需要接受组件的Schema结构,动态解释执行代码,将shema结构转换为组件。 此时需要组件提供可以由schema结构解释创建props的服务。 开发者需要在组件的props文件中,创建propsResolver对象。 开发者可以使用dynamic-resolver中提供的createPropsResolver创建props解析器。 createPropsResolver方法接受三个参数:组件props对象、组件schema结构、组件schema结构与props映射对象,其中: 组件props对象 - 由开发者声明组件时自行声明。 组件schema结构 - 由开发者在步骤「3」中创建。 组件schema结构与props映射对象 - 为缺省参数,由开发者在步骤「4」中根据需要创建。

content-container为例完整输出propsResolver的示例代码

import { ExtractPropTypes } from 'vue';
import { createPropsResolver } from '../../dynamic-resolver';
import { schemaMapper } from './schema/schema-mapper';
import contentContainerSchema from './schema/content-container.schema.json';

export const contentContainerProps = {
    customClass: { type: String, default: '' }
} as Record<string, any>;

export type ContentContainerPropsType = ExtractPropTypes<typeof contentContainerProps>;

export const propsResolver = createPropsResolver<ContentContainerPropsType>(contentContainerProps, contentContainerSchema, schemaMapper);

7. 注册组件

完成以上步骤后,开发者需要在index.ts文件中注册输出组件。

  • 开发者在install方法中,使用app.component方法全局注册组件。
  • 开发者在register方法中,为低代码解析引擎提供组件及其解析Props服务。
  • 开发者在registerDesinger方法中,为低代码可视化设计器提供设计时组件及其解析Props服务。其中解析Props服务可以在低代码解析引擎和可视化设计器中复用。

content-container为例完整注册示例代码

import type { App } from 'vue';
import ContentContainer from './src/content-container.component';
import ContentContainerDesign from './src/designer/content-container.design.component';
import { propsResolver } from './src/content-container.props';

export * from './src/content-container.props';
export { ContentContainer, ContentContainerDesign };

export default {
    install(app: App): void {
        app.component(ContentContainer.name, ContentContainer);
    },
    register(componentMap: Record<string, any>, propsResolverMap: Record<string, any>): void {
        componentMap['content-container'] = ContentContainer;
        propsResolverMap['content-container'] = propsResolver;
    },
    registerDesigner(componentMap: Record<string, any>, propsResolverMap: Record<string, any>): void {
        componentMap['content-container'] = ContentContainerDesign;
        propsResolverMap['content-container'] = propsResolver;
    }
};

开发者声明注册插件后,需要在项目源代码根目录的index.ts文件中使用。

import { App } from 'vue';
import ContentContainer from './content-container';

export default {
    install(app: App): void {
        app.use(ContentContainer);
    }
};

完整示例源代码参见组件库index.ts

开发者声明注册插件后,需要在可视化设计器中使用组件。 编辑designer-canvas下的maps.ts文件,增加以下内容:

import FContentContainer from '../../../content-container';

const componentMap: Record<string, any> = {};
const componentPropsConverter: Record<string, any> = {};

FContentContainer.registerDesigner(componentMap, componentPropsConverter);

export { componentMap, componentPropsConverter };

完整示例源代码参见设计器组件库

开发者完成设计时组件后,需要在工具箱组件注册元组件。 编辑designer-canvas/src/components/toolbox.json文件,添加工具箱选项。 需要注意的是,选项的type属性为注册组件时使用的key。 例如:

[
    {
        "type": "container",
        "name": "容器类控件",
        "items": [
            {
                "id": "Tab",
                "type": "Tab",
                "name": "标签页区域",
                "category": "container"
            },
            {
                "id": "HtmlTemplate",
                "type": "HtmlTemplate",
                "name": "模版容器",
                "category": "container"
            },
            {
                "id": "ContentContainer",
                "type": "content-container",
                "name": "容器",
                "category": "container"
            }
        ]
    }
]

完整工具箱注册代码请参见Designer Canvas Toolbox

8. 完成

恭喜各位开发者,通过以上步骤,你已经完成了开发组件的全部步骤。 下面可以制作示例页面了。

制作演示示例

所有 Farris Vue 组件均在项目跟目录下的demo目录中提供示例程序。 对于低代码可视化拓展元组件,可以在packages/ui-vue/demos/designer-canvas目录下添加示例代码。

  1. 参考drag-over.json制作低代码页面母版。示例Designer Canvas Demo - 页面Schema结构
  2. 复制drag-over.vue将其中引用的drag-over.json改为步骤1中新建的页面Schema结构文件。
  3. 在根目录packages/ui-vue/src/app.vue中,引入步骤2创建的组件,并注册路由。参见:app.vue
  4. 执行以下命令预览效果。
npx vite dev --open=#{步骤3中注册的路由路径}

提交新特性PR

我们欢迎你通过提交PR参与项目贡献,在你计划提交PR前,请先阅读以下注意事项:

  • 在你提交PR之前请确保已经开启了一个Issue并认领了它,我们只接收与认领Issue关联的PR。如果你打算实现一个比较大的特性,在开启新的Issue前最好先与项目管理者进行充分讨论。

  • 在没有十足把握时,尽量提交小规格的PR。不要在一个PR中修复多于一个bug或实现多于一个新特性,以便于更容易被接受。提交两个小规模的PR,会比提交一个大规模修改的PR要好。

  • 当你提交新特性,或者修改已有特性时,请包含相应的测试代码,以便于确认组件新的交互特性。

  • 在提交PR前端请先执行Rebase以便于保持干净的历史提交记录。

  • 我们提供了PR模板,请在提交PR时安装模板要求提供「修改的内容」、「管理的PR」、「测试用例」、「界面预览」等相关内容。

TypeScript
1
https://gitee.com/ubml/farris-vue.git
git@gitee.com:ubml/farris-vue.git
ubml
farris-vue
farris-vue
main

搜索帮助