阿里lowcode低码引擎源码(二)设计器渲染

之前有写一批关于低代码引擎代码大体框架的文章,看这篇之前可以先看看那篇,读不懂的地方可以评论我即使更新。

阿里lowcode低码引擎源码(一)页面ui结构和插件系统

本期主要讲解设计器是如何注册资产包和渲染器renderer是如何渲染shema,这次代码过于庞大不适合像上期那样展示大量代码,本期主要讲解概念和实现方式

在进入渲染流程之前先了解一些对象在本轮渲染的作用

Editor对象

在渲染流程上转换、储存、提供资源低代码组件协议资源,注意是组件协议资源不是组件库本身,举个最简单的资产包

{
    "components": [
    {
      "exportName": "AlilcLowcodeMaterialsMeta",
      "npm": {
        "package": "@alilc/lowcode-materials"
      },
      "url": "https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.7/build/lowcode/meta.js",
      "urls": {
        "default": "https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.7/build/lowcode/meta.js",
        "design": "https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.7/build/lowcode/meta.design.js"
      }
    },
    {
      "exportName": "AlifdLayoutMeta",
      "npm": {
        "package": "@alifd/layout",
        "version": "2.0.7"
      },
      "url": "https://alifd.alicdn.com/npm/@alifd/layout@2.0.7/build/lowcode/meta.js",
      "urls": {
        "default": "https://alifd.alicdn.com/npm/@alifd/layout@2.0.7/build/lowcode/meta.js"
      }
    },
    {
      "exportName": "AlifdFusionUiMeta",
      "npm": {
        "package": "@alifd/fusion-ui"
      },
      "url": "https://alifd.alicdn.com/npm/@alifd/fusion-ui@2.0.0/build/lowcode/meta.js",
      "urls": {
        "default": "https://alifd.alicdn.com/npm/@alifd/fusion-ui@2.0.0/build/lowcode/meta.js",
        "design": "https://alifd.alicdn.com/npm/@alifd/fusion-ui@2.0.0/build/lowcode/meta.design.js"
      }
    }
  ],

 。。。。
}

Editor下setAssets方法将components属性url加载后得到组件协议描述,然后再对asset进行保存

class Editor {
 。。。
 async setAssets(assets: IPublicTypeAssetsJson) {
    const { components } = assets;
    if (components && components.length) {
      const componentDescriptions: IPublicTypeComponentDescription[] = [];
      const remoteComponentDescriptions: IPublicTypeRemoteComponentDescription[] = [];
      components.forEach((component: any) => {
        if (!component) {
          return;
        }
        if (component.exportName && component.url) {
          remoteComponentDescriptions.push(component);
        } else {
          componentDescriptions.push(component);
        }
      });
      assets.components = componentDescriptions;
      assets.componentList = assets.componentList || [];

      // 如果有远程组件描述协议,则自动加载并补充到资产包中
      if (remoteComponentDescriptions && remoteComponentDescriptions.length) {
        await Promise.all(
          remoteComponentDescriptions.map(async (component: IPublicTypeRemoteComponentDescription) => {
            const { exportName, url, npm } = component;
            if (!url || !exportName) {
              return;
            }
            if (!AssetsCache[exportName] || !npm?.version || AssetsCache[exportName].npm?.version !== npm?.version) {
              await (new AssetLoader()).load(url);
            }
            AssetsCache[exportName] = component;
            function setAssetsComponent(component: any, extraNpmInfo: any = {}) {
              const components = component.components;
              assets.componentList = assets.componentList?.concat(component.componentList || []);
              if (Array.isArray(components)) {
                // 将远程url获取到的组件库加载到assets.components
                components.forEach(d => {
                  assets.components = assets.components.concat({
                    npm: {
                      ...npm,
                      ...extraNpmInfo,
                    },
                    ...d,
                  });
                });
                return;
              }
            }
            if ((window as any)[exportName]) {
              if (Array.isArray((window as any)[exportName])) {
                // setArrayAssets((window as any)[exportName] as any);
              } else {
                setAssetsComponent((window as any)[exportName] as any);
              }
            }
            return (window as any)[exportName];
          }),
        );
      }
    }
    // const innerAssets = assetsTransform(assets);
    this.context.set('assets', assets);
    this.notifyGot('assets');
  }
。。。
}

export function load(url: string, scriptType?: string) {
  const node = document.createElement('script');
  
  。。。

  node.src = url;

  // `async=false` is required to make sure all js resources execute sequentially.
  node.async = false;

  scriptType && (node.type = scriptType);

  document.head.appendChild(node);

  。。。。
}

Designer对象

利用协议包创建ComponentMeta,每一个组件对应一个 ComponentMeta 的实例其属性和方法就是描述协议中的所有字段,所有 ComponentMeta 都由设计器器的 designer 模块进行创建和管理

Area对象

Area对象这个在上期有调用记录,但本期还是粗略讲下

主要是设计器管理区域渲染的一个对象,例如leftArea,mainArea,rightArea,手下大概管理有这些东西

    WidgetContainer区域实际装载渲染内容的容器 (也就是最终视图渲染会调用其item)

    WidgetView小部件渲染组件壳子 不过其内容都由widget类型控制(PanelDock,Widget)通过      传入widget类型渲染其类型body

   Widget类型

   1.PanelDock->Panel 实现小部件渲染是渲染情况之一例如左侧区域图标,点击图片弹出内容这       种

  2.Widget实现小部件渲染是渲染情况之一例如中间设计器区域直接展示这种

BuiltinSimulatorHost 对象

这个SimulatorHost主要用于是设计器与renderer模块之间的通信,因为renderer是在iframe里面离开设计器单独渲染的不涉及与设计器相关交互所以增加一层host,host 可以访问到设计器中所有模块,并提供相关方法供renderer 层调用

如图所示,左侧就是我们的iframe里面的画布或者官方点叫renderer渲染器,右侧就是我们这个Host对象了,Host除了与我们这个renderer通讯外还可以和外界的设计器沟通达到设计器与renderer相关交互

Adapter对象(可以先不用管)

1.适配层提供的是各个框架之间的差异项。比如 React 和 Rax createElement 方法是不同的。所以需要在适配层对 API 进行抹平,我们当前只讨论react组件的渲染
情况,所以你基本看作适配器里面的方法就是原生React方法
2.除此之外适配器提供基本的页面渲染方法可以定制页面渲染的生命周期,定制导航,定制路由等,说人话就是写了一个基本的页面容器组件,就像一般组件他也是通过schema描述渲染,不过页面容器组件一般是在schema的最外层。(当然除了页面容易还有组件容器,区域容器不过本期暂时不涉及)

Document对象(DocumentModel)

DocumentModel

首先文档模型之前可以先说下渲染模型之间的所属关系
整体来看,一个 Project 包含若干个 DocumentModel 实例,每个 DocumentModel 包含一组
Node 构成一颗树(类似 DOM 树),每个 Node 通过 Props 实例管理所有 Prop。如下图

具体来说文档模型提供文档管理的能力,每一个页面即一个文档流,对应一个文档模型。文档模型包含了一组 Node 组成的一颗树,类似于 DOM。我们可以通过文档模型来操作 Node 树,来达到管理文档模型的能力。每一个文档模型对应多个 Node,但是根 Node 只有一个,即 rootNode 和 nodes。

了解完这些基本概率后下面就可以开始正式渲染流程了

首先设计器mainArea的渲染也就是中间画布区域(非renderer)的渲染

defaultPanelRegistry->DesignerPlugin->DesignerView->ProjectView->BuiltinSimulatorHostView -> Canvas -> Content 文件 渲染renderer器所在单独的iframe(这是源码的调用过程看不懂没关系知道就行就是通过defaultPanelRegistry插件最后渲染了content文件)

class Content extends Component<{ host: BuiltinSimulatorHost }> {
    state = {
        disabledEvents: false,
    };

    private dispose?: () => void;

    componentDidMount() {
        // const editor = this.props.host.designer.editor;
    }

    componentWillUnmount() {
        this.dispose?.();
    }

    render() {
        const sim = this.props.host;

        return (
            <div className="lc-simulator-content">
                <iframe
                    className="lc-simulator-iframe"
                    ref={(frame) => sim.mountContentFrame(frame)}
                />
            </div>
        );
    }
}

这个iframe就是用来装载render渲染器,从ref={(frame) => sim.mountContentFrame(frame)方法开始算进入真正的渲染器加载部分。

这个渲染的部分主要递归Document对象解决

Document对象是怎么生成的

Document 创建了一个根节点 rootNode, 然后把schema作为参数传入,用schema的基础信息封装成根节点,用shema的children封装成children对象,children对象里遍历传入的children属性生成一个Node对象又根据children的children生成children对象 整个过程就是不断递归生成一个能完整描述pageshema 的Node对象树 ,其中每个节点的props属性封装成props对象再进一步装成一个个的prop用来描述节点的prop属性

export class DocumentModel {
    constructor(project: any, schema?: IPublicTypeRootSchema) {
        makeObservable(this, {});
        this.project = project;
        this.designer = this.project?.designer;
        this.rootNode = this.createNode(
            schema || {
                componentName: 'Page',
                id: 'root',
                fileName: '',
            },
        );
    }
    createNode(data: any): any {
        let schema = data;
        let node = new Node(this, schema);
        this._nodesMap.set(node.id, node);
        this.nodes.add(node);
        return node as any;
    }
}
export default class Node {
     constructor(readonly document: any, nodeSchema: any) {
        const { componentName, id, children, props, ...extras } = nodeSchema;
        this.id = id;
        this.componentName = componentName;
        this.props = new Props(this, props, extras);
        this._children = new NodeChildren(this, this.initialChildren(children));
        // this._children.internalInitParent();
    }
}

export class NodeChildren {
       constructor(
        readonly owner: Node,
        data: any[],
    ) {
        makeObservable(this);
        this.children = (Array.isArray(data) ? data : [data]).filter(child => !!child).map((child) => {
            return this.owner.document?.createNode(child);
        });
    }
}

渲染最终视图

把自定义容器组件合并进入组件数组 ,从documentModel中rootNode的schema开始渲染最外层容器组件是在代码中自定义好的容器组件,然后将schema作为其属性后续通过不断 __createVirtualDom和__getSchemaChildrenVirtualDom 递归下去 生成最终页面 ,这块原项目代码过于复杂我简化了下贴出来

class Renderer extends Component<{
  rendererContainer: SimulatorRendererContainer;
  documentInstance: DocumentInstance;
}> {
 const { container, document } = documentInstance  
 <LowCodeRenderer schema={documentInstance.schema} components={container.components} />
}

// 渲染schema主函数

class Renderer extends Component{        
        getComp() {
           const { schema, components } = this.props;
           const { componentName } = schema;
           const allComponents = { ...RENDERER_COMPS, ...components };
           let Comp = allComponents[componentName] || RENDERER_COMPS[`${componentName}Renderer`];
           Comp = RENDERER_COMPS[`${componentName}Renderer`];
           return Comp;
        }         
        render() {              
           let Comp = this.getComp();
            // 首次Comp为pageRenderer 
           _createElement(Comp, _extends({              
                 。。。                                              
                __components: allComponents,
                __schema: schema,
                __designMode: designMode
              })          
        } 
}

pageSchema最外层容器

最外层容器在项目中的代码

// 此处为代码中自定义的组件外层容器
const RENDERER_COMPS: any = adapter.getRenderers();
adapter.setRenderers({
  PageRenderer: pageRendererFactory()
});
pageRenderFactory(){
   render() {
      // const { __schema, __components } = this.props;
      return this.__renderContent(this.__renderContextProvider({ pageContext: this }));
    }
     __renderContextProvider = (customProps?: object, children?: any) => {
      return createElement(AppContext.Provider, {
        value: {
          ...(customProps || {})
        },
        children: this.__createDom(),
      });
    };
    __createDom = () => {
      const { __schema, __ctx, __components = {} } = this.props;

      const _children = getSchemaChildren(__schema);
      let Comp = __components[__schema.componentName];
      const parentNodeInfo = ({
        schema: __schema,
        Comp
      });
      return this.__createVirtualDom(_children, {}, parentNodeInfo);
    };
     __createVirtualDom(){
          let child = this.__getSchemaChildrenVirtualDom(schema, scope, Comp, false);
      const renderComp = (innerProps: any) => engine.createElement(Comp, innerProps, child);
      return renderComp({
        __inner__: {
          hidden: schema.hidden,
        },
      });
    }
   __renderContent(children: any) {
      const { __schema } = this.props;
      // const parsedProps = this.__parseData(__schema.props);
      // const className = classnames(`lce-${this.__namespace}`, getFileCssName(__schema.fileName), parsedProps.className, this.props.className);
      // const style = { ...(parsedProps.style || {}), ...(typeof this.props.style === 'object' ? this.props.style : {}) };
      const id = this.props.id
      return createElement('div', {
        // ref: this.__getRef,
        // className,
        id,
        // style,
      }, children);
    }
}

这其中还有很多细节,后续会持续更新

github连接

https://github.com/chenjixue/diy-lowcode

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值