CardRenderModule设计备忘

CardRenderModule是LinkCat中用于将数据渲染为卡片形式HTML的前后端代码的总称,下文简略成为卡片渲染模块。这里贴个草图

卡片渲染模块简图

PS:可以想象我平时的OneNote上笔记的风格是多么“奇伟壮丽”,BrainStorm下来只有Storm了XD。

首先定义一下渲染模块,渲染模块是LinkCat中在在注解模块后将已注解资源进行处理的模块。它会把LinkCat注解模块输出的带有注解信息的资源渲染成human-readable的信息,比如卡片式的显示信息(HTML),流程节点图式的显示(这个是之前StackFrame文章想要做的)。这样将渲染模块独立出来的好处有很多,分离焦点增强稳定性便于开发等。而这里的渲染模块特指的是在后端上运行的渲染相关的代码(虽然我也不知道需要渲染啥,反正差不多是SSR或者React18内设想的ServerComponent)。

CardRenderModule代码逻辑暂时只实现了前端的一小部分。这里后端没啥讲的,因此下面主要还是对前端的描述。

这里定义一下卡片渲染,卡片渲染模块是将一个资源,从代码中一个Object渲染成用户界面上的一个限定大小的显示资源数据的富文本卡片类型方格。emm类似于下面这个。

当然,这是最终的设想,这里的每一个方格中都有复杂的可交互组件,考虑到一个页面上可能有多个Card,可交互的组件太多太复杂会有PerformanceIssue,我们可以这里直接用Puppeteer截图,而不是放一个可交互的东西在这里。

上面这个就是一个Card。这就是我最终想达到的一种效果,当然没那么丑。

因为我希望用户能够自己设计Card来使用,通过能够搭建一个CardMarket为想要分享自己Card的用户提供平台,因此我第一个是需要Card能够Serilizable,第二个是拥有一个可以方便设计Card的地方。而这个地方就是BlockCat。下面我们来看看BlockCat。

emm本来只是单纯想写LinkCat的卡片显示的渲染模块,结果写着写着就开始写低代码的组件设计应用BlockCat了。

BlockCat

BlockCat是在LinkCat开发过程中因为有自定义设计Card的需求,所以衍生出来的LinkCat插件生态UI部分比较重要的一环。BlockCat简单来说是一个低代码平台。可以通过简单的点击和拖拽不用直接编写代码就可以对UI进行调整。而且有所及即所得的特点。这是BlockCat的总体目标。

BlockCat当前只是CardRender部分的UI设计器,后面考虑做成一个多种Render模块支持(Multi-RenderModule Supported)低代码平台。

现在我们来详细讲一下卡片渲染的BlockCat的架构和能力。

BlockCat CardRender架构

这里我们将一个Card中的任何一个标签元素定义为一个Component,而一个Card我们称为一个Block块。而一个Card里面是一个Component树。

因此我们可以说Card=Block~Component树。

需要注意的是这里的Component不是React定义上的Component。而是对于BlockCat上的最小可操纵单元的称呼。

可行性

这里我们首先探讨一下可行性。

我们要实现的功能是

  • 导入导出Card的能力
  • 所见即所得的UI修改能力

这两个能力再展开来就非常复杂了。

1.Card Template(Slottable)

比如一个Card中有一种叫Slot的组件,可以其保存为Template,而后导入另一个Card中通过Slot注入不同的内容。

2.Card Content

Card需要知道在哪些位置需要填充后端发来的哪些数据,并且这些位置是可以对数据进行简单的修改后展现

3.Card Dynamic Content

Card可以通过WebSocket或者其他方式获取后端持续发来的数据,类似与 Hemdall 的一些卡片式导航。然后持续不断的更新卡片上的数据。

可以看到,无论哪个能力写下去都是一个大坑。但由于最近在找工作时间有限无法Dive In Design,所以只有先直接rush A了。

这里的所见即所得我们可以通过Chakra-UI实现,通过将CSS直接注入Chakra-UI的组件中,可以方便的通过JSON配置文件修改组件的样式。然后Chakra-UI 与 Framer-Motion联动,直接完美的实现了动画需求。

Slottable还是比较好实现的,就是往组件树某个节点上插入一个子树。但是用户使用体验方面需要详细设计。

CardDynamicContent我还没有想好怎么实现。打算把后端参考AnnotateService模块和React reconceiler 拉通重构后再考虑,因为这有一个Schedule问题。这种数据的优先级有高有低。

BlockCat设计

BlockCat草图

上面是BlockCat的设计草图,整个应用参考Node-Red的界面设计。主要分为上侧的menu bar,左侧ComponentList,右侧ComponentPanel,下侧的Editor(暂时这样考虑,后面可能会参考VSCode的Drawer设计),中间是WorkSpace。

1.MenuBar

MenuBar菜单栏,一般桌面应用都有的一个东西,用来做文件(Card配置文件)级别的操作和一些全局设置,暂时没有多考虑,不多解释。

2.ComponentList

组件列表,这里包含所有内建组件和用户从CardStore上下载的组件,同时有分类功能和Favorite功能。

点击一个Component就会向焦点组件(外部)下侧,插入一个组件。

暂时不考虑拖拽操作,我还没想好怎么实现这里的拖拽。

3.ComponentPanel

从上到下分为ComponentHierarchyTree组件层级树,CardMainConfigPanel卡片主要配置面板,ComponentCSSPropertyPanel组件CSS属性面板,ComponentTextManagementPanel组件文字管理面板

第一部分 ComponentHierarchyTree

是Card ComponentHierarchyTree组件层级树,这里是用户对Card中组件最为直观了解的地方,可以通过拖拽的方式改变拖拽组件的层级。

组件层级树有一些问题,首先是要限制能够DropIn的组件,比如Image组件就不应该有子组件。

第二部分 CardMainConfigPanel

CardMainConfigPanel是卡片主要配置面板,这里是对卡片级别的参数,记录卡片名,卡片作者,卡片描述等进行设置。

第三部分 ComponentCSSPropertyPanel

ComponentCSSPropertyPanel组件CSS属性面板,这里显示焦点组件的CSS属性,同时可以对属性进行实时修改即刻刷新。这里为了能够在用户键入时自动刷新,需要监听用户输入事件,将每一次type in都dispatch对应的modComponent事件来刷新参数。

第四部分 ComponentTextManagementPanel

ComponentTextManagementPanel是组件文字管理面板,这个面板只在可以容纳文字的标签上出现,例如Box(在这里我们沿用Chakra-UI对于div封装的名称Box)。而不能容纳文字的标签例如Image。

但是我认为这并不是一个好的职责划分,我后面应该会使用ComponentContentManagementPanel替代,为每一个组件的内容提供管理,这样更加符合Image、自定义组件的内容管理设计。

4.Editor

这里是一个Editor,我是打算这里做成一个在LinkCat上运行的一个沙盒,只提供基础的plugins def API访问。或者做成Mock API的配置窗口。具体等把基础能力实现再说。

5.WorkSpace

这里是组件的渲染窗口,用户所有的更改都会即刻显示在这里。

BlockCat当前实现

现在的BlockCat只实现了一小部分功能,能够提供组件的所见即所得,但是还没有实现导出和导入,而且CSS属性的设置十分的麻烦,需要记参数名或缩写,后面打算做一个Mapper来解决这个问题。只能说在写出来导出和保存能力后能凑合用,至少让CardRender拥有一个简单的设计器。

首先要说明的是当前的配色样式都是开发过程中我为了加快速度随便给的,后面会仔细调整到比较舒服的样式。

下面中间是一个粉色的卡片,这就是我们Card的边界,我们可以在卡片内部自由添加组件。

初始界面

左侧是组件列表,这里将内置组件编组为Default,点击就会在当前空白的WorkSpace中添加一个Card根节点然后append选中组件到该根节点上。

右侧是一个Tab栏,第一个Tab是PropertyTab,这里因为还选中Card(当前选中Card为空),所以Card相关的参数没有渲染,而下面这个方向键是我用来测试组件间的位置关系做的一个工具栏。

可以看到中间有个绿色的圆点,这里实际是一个工厂环组件,详细设计见:

这里的工厂环组件还没有完成,把当前完成的部分放到这里,请忽略这配色和测试用的数据。

现在我们点击Box按钮:

现在WorkSpace中就比较丰富了,我给Box的默认CSS显示在了右侧,h是高度的缩写,w是宽度,bg是背景颜色(backGround)的缩写。中间的绿色的部分就是我们添加的Box,而这个Box周围有8个工厂环按钮,蓝色按钮是向组件内部添加组件,红色按钮时向焦点组件的外侧,也就是当前的根节点(根节点不是这个Box而是Box的父节点,这个节点的id是0,名称为Box1)上添加一个组件。

然后我们看右侧的CSS属性面板,这里是默认的三个属性,下面有一个可以添加属性的按钮,非常的不人性化,是我我会打这个作者XD。暂时的实现是这样的。后面有时间再该为有Autocomplete的输入方式。

这里很清楚,是组件文本管理器,这里默认的Text类型是none,而一共有四种类型

  • dynamic 动态的内容,是我上面架设的持续从后端接收数据的情况
  • programmable 可编程内容,暂时只是个构想,等想好再讲解
  • static 直接从后端接收的注解内容或静态文字
  • none 无文字内容

这个部分还没有实现好,本来我想如果设置了需要异步获取的数据会直接在组件上显示骨架屏,但这个操作和我的动态渲染逻辑有些CSS冲突,暂时搁置,等实现了fetch的代码逻辑再考虑。

下面是一个添加组件的演示,这里请忽略后半段捉急的拖拽操作。

然后是组件组变更演示,focuse框还有一些bug,没有更新focused对象的边界变更:

到这里就基本是当前实现的所有bug哈哈。

BlockCat代码细节

这里不多解释UI方面的东西,都是老一套。

这里最为重要的是对整个应用的状态管理,这里的状态管理我使用了Redux。这里我把所有reducer逻辑都放到了一个Slice里,后面会根据功能和域分成几个。

上面是一些封装useSelector的Hooks。

我认为整个BlockCat最应该记录的是在Component的创建修改方面。

Component Operations

组件操作被我用一个ComponentActionController封装了起来。

上面是代码中一个卡片的实际存储方式,可以看到其中有一些比较奇怪的属性,暂时我们先不解释,只看comps属性,这里是一个PositionableBlockProps数组,这里面存储了整个Plain化的组件树。

我们需要对于BlockCat组件的最基础的了解,这里每一个Card中的组件树中的节点是PositionableBlockProps。

其中最为重要的是tag属性,存储了这个组件的标签名(可以是自定义标签名)。nickName是对一个组件添加昵称,比如某个组件及其子树是负责某一样功能,则可以添加一个nickName在这个组件上。其他的就不用赘述了。

我们先来解释一下这个名称的构成。首先是Positionable,可定位的,表明组件树直接是有位置关系的,而通过下面的Positionable接口我们可以看到,其中有个directions属性存储了一个叫InnerOuterDirection为key,number(实际就是这个节点的id)为值的键值对集合。因此我们可以确定组件节点可以使用上面介绍的方向键在整个组件树内通过directions属性遍历。

而PlainTreeLike就不用多说了,看名字,首先这是个PlainTree就说明这里面的parent、children指针不是直接使用引用,而是使用id,这样符合redux对于state best design practice中的描述(但应该最好写成Record<id,PositionableBlockProps>),不用进行deep compare。

不同颜色代表所属于不同组件,最外侧和最内侧使用的蓝色,中层使用的绿色

这里有一个关于direction的图示,首先我们定义,无论是Inner还是Outer前缀的方向,其方向(Up Down Left Right)都是由他的出发点决定,比如从组件左边框(Left)出发,指向其内部的叫InnerLeft,而指向外部的称为OuterLeft,可以通过上图比较清晰的看出来。而如果不存在子组件(子节点,children为空),则将指针置-1。我们默认整个Card的根节点的id是0,用户创建的组件id从1开始递增。

上面介绍的在Card 组件树中实际存储的组件节点的结构,我们就应该介绍组件的元数据了,我们把组件的元数据称为ComponentMeta。

这里可以看到ComponentMeta有一个node属性,里面是一个React的FunctionComponent类型的值,我们渲染组件实际就是进行React.createElemnt(meta.node,.......)操作。

ComponentMeta里面需要着重介绍的是controller属性,也就是ComponentActionController。

这个接口其中的API都十分语义化,不多解释。重要的是其实现类的内部逻辑。

这里因为篇幅原因,只看一个tryInsert函数,它也是最具有代表性和最为复杂的一个操作。

这里我们再详细解释一下这个在PositionableBlockProps内的directions属性的存在方式,BlockCat架设一个父组件下只有两种子组件组织方式,即垂直方式和水平方式,我们这样架设可以降低复杂度同时提高Card成品的稳定系。所以任何一个组件,都只有一种子组件组织方式,比如垂直组织方式的子组件按顺序就是从上到下,因此父节点中的children属性就是以这种从上到下的方式组织起来。但这样的组织方式有一个问题,父组件的Inner${Direction}方向的指针只有一个,但有多个子组件可以指向,这时候我们只指向children[0]元素,就是垂直方向的最顶端元素或者水平方向的最左端元素。这样就再一次降低了复杂度。

首先我们抽象一下这个操作,你想插入一个组件到一个特定的位置,这个操作如果从插入位置的附近组件(也就是处于同一个父组件下的兄弟组件)为基准考虑,那么我们需要这样,首先需要以这个组件为基础通过它的directions属性,获得所有Effected组件,也就是directions属性中某些属性因本次操作需要修改的组件拿到。有了上文关于子组件组织形式的描述,我们可以知道子组件不是垂直组织就是水平组织,因此可以通过父组件的属性得出子组件的组织形式。

这里我们引入一下VStack和HStack,BlockCat中主要的结构型组件,他们承担着为其子组件塑性的工作,VStack是VerticalStack的缩写,其子组件是垂直组织形式,HStack是HorizontalStack的缩写,其子组件是水平组织方式,这里我们定义其他的组件默认组织形式是Vertical形式的。因此只有父组件是HStack时,子组件children数组内的组织形式才是从左至右,而其余都是从上到下。

根据以上两段的描述,我们可以很轻易的得出,一次Insert最少更改一个组件,对多更改两个组件。

  • 一个组件,插入一个没有子组件的组件内部,其父组件的四个InnerDirection属性都需要修改
  • 两个组件,一个为父组件一个为子组件
    • 若为最左边或最上面插入,则需要改父组件对应的Inner和插入方向两侧的Inner指针。例如插入到垂直Box的第一个,则需要修改父组件的InnerUp后,修改InnerLeft InnerRight指向inserted节点。而后修改原第一个子节点。
    • 若为最右或最底部插入,则需要改父组件对应的Inner节点和最后一个子节点
  • 两个组件,两个都是子组件,修改对应组织类型方向的双向链即可
向内部第一个位置添加组件

现在我们将这个流程理清楚了,下面是对代码进行设计。

这里我们首先需要拿到受影响的组件,而后判断其类型,根据其类型在上面的列表内去找对应的处理办法。但有一个问题是,我在Insert的过程中实际上始终需要操作一个节点,这个节点就是操作位置的父节点,因此我在代码实现里实际是以操作位置的父节点为target目标节点(这样还有个好处,我们可以提前检测该节点是否可以存在子组件例如Image),而插入节点位置的节点称为basePoint(插入操作实际是基于FocusedComponent来操作,肯定有一个basePoint)。然后把除basePoint外的另一个节点称为effected。现在我们就需要三个组件节点:basePoint、target、effected,通过对三个节点的比较和操作,我们就可以完成InsertAction。我们可以将这三个节点和一些需要的信息封装为一个Payload对象,更加方便的进行操作。

这里封装Payload是有意义的,因为我把整个操作的过程封装为一个middleware类型的属性,把这个payload传进入consume,通过返回值判断操作的合法性和结果。下面是tryInsert方法的代码实现,这里前面的一大部分实际上只是将数据以正确的方式封装了起来,并不是进行实际操作,方法后半段handler.dispatch(ctx, payload);才是真正开始执行middleware的位置。

下面是tryInsert的代码实现:

tryInsert(comp: PositionableBlockProps, direction: InnerOutDirection, block: CardRenderConfig, target: PositionableBlockProps): { result: CompActionResType, comp: number } {
    const meta = this.meta;//获取ComponentMeta用检查插入节点的合法性
    //TODO change to handler adapter pattern
    const shortDirection = InnerOuterMapDirection[direction];//将direction中inner和outer前缀通过table-driven的方式去除
    let effected;//effected节点
    let payload: Payload;//创造负载对象
    if (direction.startsWith("Outer")) {//进行操作主体转换,将target转换为target.parent
      effected = block.comps.find(comp => comp.id === target.directions[direction]);
      payload = {
        target: block.comps.find(comp => comp.id === target.parent),
        action: {
          basePoint: target,
          direction: shortDirection
        },
        effected,
        block,
        comp
      }
    }
    else {
      if (target.children.length === 0) {//没有子节点,直接insert in
        payload = {
          target,
          action: {
            basePoint: target,
            direction: "Center"
          },
          effected: target,
          block,
          comp
        }
      }
      else {//有子节点,构建正确的payload对象
        let basepoint: PositionableBlockProps;
        //Insert in first last
        if (shortDirection === "Up" || shortDirection === "Left") {
          basepoint = block.comps.find(comp => comp.id === target.children[0]);
          effected = target;
        } else {
          const lastId = target.children[target.children.length - 1];
          basepoint = block.comps.find(comp => comp.id === lastId);
          effected = target;
        }
        payload = {
          target,
          action: {
            basePoint: basepoint,
            direction: shortDirection
          },
          effected,
          block,
          comp
        }
      }
    }
    const ctx: Context = {//上下文对象,这里就是对应的组件meta数据
      meta: meta,
      factory: ComponentFactory//所有组件的工厂函数,代码在下面
    }
    payload.comp.id = payload.comp.id === -1?++payload.block.maxChild:payload.comp.id;//code smell,后面要理清一下这里给id的逻辑
    const action = this.actions;
    //TODO make this a priority chain to do this check automatically.
    //按照优先级获取这个Insert的操作handler
    //优先级为作用域越小,优先级越高
    const handler: CompMiddlewares = action[`Insert${payload.action.direction}`] ?? action[shortDirection === "Up" || shortDirection === "Down" ? "InsertVertical" : "InsertHorizontal"] ?? action["GeneralInsertHandler"] ?? DefaultInsertMiddlewares;
    const res = handler.dispatch(ctx, payload);//开始dispatch event
    if (res === undefined) {//结果判定
      console.log("WIP");//这里应该用Logger,但是还没有实现LoggerService,所以打印个WIP
      return;
    }
    return {//emmm 没时间写逻辑判定,直接给个成功
      result: "Success",
      comp: payload.comp.id
    };
  }

export class DefaultComponentFactory implements ComponentFactory {

  constructor(private metas: { [tagName: string]: ComponentMeta }) {
  }
  gen(tagName: string): PositionableBlockProps {
    const meta = this.metas[tagName];
    return BlockLinkTool.convertToBlock({ tag: meta.name,textType:"none", css: meta.defaultProps,text:"" });
  }
}

可以看到上面代码中其实大部分是在封装Payload对象和Context对象,而核心的操作代码应该在handler.dispatch中。

这里我们引入Middleware,用过express的读者应该非常熟悉这个api,说白了middleware就是将全局变量封装到Context,Session相关的变量封装为payload,然后将这两个参数放到一堆串在一起的中间件函数里顺序执行,这里面如果那个中间件选择中断执行,整个流程会立刻返回结果。相当于每个中间件都对payload有几乎完整的控制权,LinkCat内Service部分也是中间件的设计。

中间件模式抽象类

这里随便给了控制中间件的类型MiddlewareEntry名称,使用use将中间件按顺序注册进去,而dispatch则是执行中间件链。

ComponentActionController实际使用的中间件类型

这里的Gen方法是一个语法糖,可以通过Gen().use().use()链式创建和注册中间件。

这里介绍了代码内的中间件设计,下面就该看实际代码实现了。首先我们来看一下在组件没有设置对应Action时,DefaultCompActionController内置的DefaultActions,这里面的每一个属性都是一个MiddlewareEntry。

可以看到通过中间件模式的使用,让我们有代码复用的可能,GenerateTag、WrappWith是两个高阶函数,他们通过参数调整生成中间件,而DefaultComponentDeleter和InsertBettwenBasePoint2Effected是两个可复用的组件操作中间件。而最后的Stop中间件是用来终止成功执行的中间件,即返回执行成功和对应的结果(通常为操作对象比如被插入的组件对象、被删除的组件对象)。

GenerateTag 中间件

很简单的一个中间件,单纯的生成一个组件,组件id,directions,parent为-1。

const GenerateTag: (tagName: string, cb?: (ctx: Context, payload: Payload, comp: PositionableBlockProps) => void) => CompGenMiddleware = (tagName: string, cb) => (ctx, payload, next) => {
  next();
  return { comp: ctx.factory.gen(tagName) };
}

WrappWith 中间件

想要知道为什么多了一个p吗,因为我打错了。

这个是用来包装目标组件的中间件,其实就是把目标组件的outerDirections给Wrapper(包装者组件),这里的第一个参数就是传入包装者的标签名,第二个参数是为了拓展性写的一个用户自定义控制流的callback。

这里使用了一个BlockLinkTool,这是一个用于连接组件的工具类,里面的对于direction的操作全是硬编码(这操作写了我一下午),因为害怕`Inner{direction}`这样的操作会降低效率,所以99%的操作都是硬编码进去的。不多不多,也就两百多行,自闭。

这里显示通过tagName把包装者组件gen出来,然后将id给他,然后用BlockLinkTool的wrapper和insertBetten函数链接起来。需要说明的是InsertBettwen函数内部会自动计算effected的类型来链接不同的direction(硬编码.jpg)。

const WrappWith: (tagName: string, cb?: (ctx: Context, payload: Payload, wrapper: PositionableBlockProps) => void) => CompMiddleware = (tagName: string, cb) => (ctx, payload, next) => {
  const wrapper = ctx.factory.gen(tagName);
  BlockLinkTool.setId(payload.block, wrapper);
  if (cb)
    cb(ctx, payload, wrapper);
  else {
    BlockLinkTool.wrapper(payload.target, payload.action.basePoint, wrapper);
    BlockLinkTool.insertBettwen(payload.target, payload.action.basePoint, payload.action.direction, wrapper, payload.comp);
  }
  next();
  return { comp: payload.comp };
}

DefaultComponentDeleter

这个中间件用于通用的组件删除操作,没啥可说的,都是把指针正确的链接。(又是一个硬编码.jpg)

const DefaultComponentDeleter: CompMiddleware = (ctx, payload, next) => {
  const parent = payload.target;
  const deleted = payload.action.basePoint;
  const Former = payload.effected;
  const Later = payload.comp;
  const direction = payload.action.direction;
  //Erase Direction information
  if (parent.children.length === 1) {

    BlockLinkTool.fillInner(parent, -1);
  }
  else if (parent.children[0] === deleted.id) {
    if (direction === "Up") {
      parent.directions.InnerLeft=deleted.directions.OuterDown;
      parent.directions.InnerRight=deleted.directions.OuterDown;
      parent.directions.InnerUp = deleted.directions.OuterDown;
      Later.directions.OuterUp = parent.id;
    }
    else {
      parent.directions.InnerLeft = Later.id;
      Later.directions.OuterLeft = parent.id;
      parent.directions.InnerUp=Later.id;
      parent.directions.InnerDown=Later.id;
    }
  }
  else if (parent.children[parent.children.length - 1] === deleted.id) {
    if (direction === "Up") {
      parent.directions.InnerDown = Former.id;
      Former.directions.OuterDown = parent.id;
    }
    else {
      parent.directions.InnerRight = Former.id;
      Former.directions.OuterRight = parent.id;
    }
  }
  else {
    if (direction === "Up") {
      Former.directions.OuterDown = Later.id;
      Later.directions.OuterUp = Former.id;
    }
    else {
      Former.directions.OuterRight = Later.id;
      Later.directions.OuterLeft = Former.id;
    }
  }
  //Remove from children
  parent.children.splice(parent.children.indexOf(deleted.id), 1);
  next();
  return { comp: deleted };
}

InsertBettwenBasePoint2Effected 中间件

得益于BlockLinkTool的封装,本来应该最大最复杂的Insert操作被精简到只需要一个五行的函数就可以完成,因此我把这个函数称为Macro,展开后就是巨大的逻辑代码。

这里很简单,由于是Insert操作,因此首先把被插入的组件的Outer方向的指针全部填充为父组件(后面只需要改需要变动的方向)。而后再调用BlockLinkTool.InsertBettwen方法,然后Boom,就完成了。

const InsertBettwenBsePoint2Effected: CompMiddleware = (ctx, payload, next) => {
  next();
  return { comp: InsertBettwenMacro(ctx, payload) };
};

export const DefaultInsertMiddlewares = CompMiddlewares.Gen().use(InsertBettwenBsePoint2Effected);

const InsertBettwenMacro = (ctx: Context, payload: Payload) => {
  const action = payload.action;
  BlockLinkTool.fillOuter(payload.comp, payload.target.id);
  BlockLinkTool.insertBettwen(payload.target, action.basePoint, action.direction, payload.effected, payload.comp);
  if(payload.block.comps.find(comp=>comp.id === payload.comp.id) === undefined)payload.block.comps.push(payload.comp);
  return payload.comp;
}

到这里默认的中间件就介绍完了,这里我们思考一下这个问题,如果我在VStack中的一个子元素上选择在OuterLeft方向插入一个元素,该怎么办呢?

这里首先由于一个组件的子组件的组织形式只能有垂直或竖直的一种,而我们不可能直接改父元素,因此我们需要在焦点组件外面Wrap(包装)一层HStack,然后在HStack内进行OuterLeft操作。这样就保证整个组件树的结构的稳定性和合理性。这就是上面WrappWith中间件的用途之一。

下面是一个组件的定义,这里的ComponentDefBuilder是一个工具类,单纯的让我们可以链式构建整个组件,没有啥可讲的。这里可以看到我们设置了组件的定义,组件的type(structure、feature、end类型,end类型是无法容纳子节点的),这里的.action()方法中我们实际传入了一个DefaulCompActionController实例,通过该类的静态方法set构建新的实例。其中InsertHorizontal和InsertVertical就是我们上面考虑的使用WrappWith的情况。而后传入了默认的css值。最后将ComponentMeta生成出来。

ComponentDefBuilder.def("VStack", VStack).type("structure").action(DefaultCompActionController.set({
      InsertHorizontal: CompMiddlewares.Gen().use(InsertBettwenBsePoint2Effected).use(Stop),
      InsertVertical: CompMiddlewares.Gen().use(WrappWith("HStack")).use(Stop),
    })).defaultCss({
      h: "100px",
      w: "100%"
    }).generate()

//DefaultCompActionController.set 方法
static set(actions?: Partial<ComponentActions>) {
    const real: ComponentActions = {
      ...DefaultCompActionController.defaultActions,
      ...actions,
    };
    const controller = new DefaultCompActionController();
    controller.actions = real;
    return controller;
  }

到这里整个关于ComponentActions方面我认为有记录价值的点都包含在内了。

–未完待续–


v1.0 wep 2022/03/05 完成BlockCat v^0.xxx版本架构和文章编写

v1.1 fp 修改一些愚蠢的拼写错误,比如“疯转”

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注