在我们公司自研的低代码平台中,有个需求是在内置组件无法满足业务需求的情况下,需要快速进行组件的设计开发,目前有三种方案:
本文将从项目的创建到设计器的开发,组件的加载,渲染等步骤向你一步步揭开在线设计组件的面纱。
此demo所使用的技术栈为:
相信我,你看得懂的!!
ngnewonline-component-design项目创建好了安装ace-builds库
yarnaddace-builds初始化ace-builds库,在angular.json中将html,css,javascript的样式导入到assets中:
"assets":["src/favicon.ico","src/assets",{"glob":"worker-html.js","input":"./node_modules/ace-builds/src-noconflict/","output":"/"},{"glob":"worker-css.js","input":"./node_modules/ace-builds/src-noconflict/","output":"/"},{"glob":"worker-json.js","input":"./node_modules/ace-builds/src-noconflict/","output":"/"},{"glob":"worker-javascript.js","input":"./node_modules/ace-builds/src-noconflict/","output":"/"}],写个工具函数用于加载ace-builds:
getAce().subscribe((ace)=>{...});2.将页面分为在线组件设计器和渲染器添加两个路由模块:
设计器
import{NgModule}from'@angular/core';import{RouterModule,Routes}from'@angular/router';import{OnlineDesignComponent}from"./design.component";constroutes:Routes=[{path:'design',component:OnlineDesignComponent}];@NgModule({imports:[RouterModule.forRoot(routes)],exports:[RouterModule]})exportclassOnlineDesignRoutingModule{}设计器页面布局
如上,将页面分为四部分:
渲染器
import{NgModule}from'@angular/core';import{RouterModule,Routes}from'@angular/router';import{OnlineRenderComponent}from"./render.component";constroutes:Routes=[{path:'render',component:OnlineRenderComponent}];@NgModule({imports:[RouterModule.forRoot(routes)],exports:[RouterModule]})exportclassOnlineRenderRoutingModule{}渲染器页面布局
组件操作有:组件设置,组件编辑,组件预览
页面布局完了,下一步进行编辑器组件封装
编辑器组件分为html、css、javascript、json组件,其实这四个组件大同小异,基本都差不多:
html:
到这里我们可以拿到组件设计时的数据:
JavaScript的解析我们希望能够注入一些(数据、服务、接口、组件、工具函数)供组件消费。我们希望通过self.ctx可以拿到我们注入的的上下文信息:
上面的截图我们可以注入了包括且不限于:
文末会演示使用内置组件进行二次编辑的例子。
我们拿到用户编写的js字符串:
通过newFunction的方式注入context和执行用户编写的js。
privatecreateWidgetControllerDescriptor(widget:Widget,name:string){letwidgetTypeFunctionBody=`returnfunction_${name.replace(/-/g,'_')}(ctx){\n`+'varself=this;\n'+'self.ctx=ctx;\n\n';widgetTypeFunctionBody+=widget.javascriptTemplate;widgetTypeFunctionBody+='\n};\n';//console.log('widgetTypeFunctionBody>>:',widgetTypeFunctionBody);constwidgetTypeFunction=newFunction(widgetTypeFunctionBody);constwidgetType=widgetTypeFunction.apply(this);constresult={widgetTypeFunction:widgetType};returnresult;}上面的方法中我们先通过创建一个自定义函数体(绑定ctx)+用户js,通过newFunction执行函数体widgetTypeFunctionBody创建函数并返回,然后将此函数放到widget的widgetTypeFunction上。
下一步解析html,这一步就是创建组件的过程,我这边使用Angular技术栈创建一个动态组件,其他技术栈的同学可以参考此方法实现。
创建动态组件我把他封装了一个服务dynamic-component-factory.service.ts:
viewContainerRef.createComponent(this.widgetInfo.componentFactory,0,injector);可以创建一个组件。
拿到工厂组件widgetInfo然后实例化上下文newWidgetContext(this.widgetInfo)
init(){this.customComponentService.getWidgetInfo(this.widget).subscribe({next:(widgetInfo)=>{console.log('widgetInfo>>:',widgetInfo);this.widgetInfo=widgetInfo;this.widgetContext=newWidgetContext(this.widgetInfo);this.loadFromWidgetInfo();},error:err=>{console.log(err)}})}解析之前保存到widget上的widgetTypeFunction方法,并执行;
privateloadFromWidgetInfo(){this.widgetContext.widgetNamespace=`widget-type-${this.widget.id}`;constelem=this.elementRef.nativeElement;elem.classList.add('custom-widget');elem.classList.add(this.widgetContext.widgetNamespace);this.widgetType=this.widgetInfo.widgetTypeFunction;if(!this.widgetType){this.widgetTypeInstance={};}else{try{//这一步是核心this.widgetTypeInstance=newthis.widgetType(this.widgetContext);}catch(e){this.widgetTypeInstance={};}}//console.log('this.widgetTypeInstance>>:',this.widgetTypeInstance);if(!this.widgetTypeInstance.onInit){this.widgetTypeInstance.onInit=()=>{};}if(!this.widgetTypeInstance.onDestroy){this.widgetTypeInstance.onDestroy=()=>{};}//这个创建组件方法this.configureDynamicWidgetComponent();//执行里面的生命周期//可以加入更多的生命周期函数this.widgetTypeInstance.onInit();}创建组件
拿到用户编写的css直接执行这个函数:
privateparserCss(){constnamespace=`${this.widgetInfo.widgetName}-${Math.random()}`;constcustomCss=this.widgetInfo.cssTemplate;this.cssParser.cssPreviewNamespace=namespace;this.cssParser.createStyleElement(namespace,customCss,'nonamespace');}我们来看下实际运行效果:
编辑settings,让组件表现不同的行为。这一步其实就是组件实例化的过程。
完成以上JavaScript、settings、html、css的解析,我们的组件在线编写和渲染也差不多完成了,这个只是示例,实际生产环境需要考虑更多情况和边界条件,各位可以参考此实现方式,给做低代码的同学提供一个设计思路,如有更多实现方式,欢迎讨论。
本文所有的代码均已开源放到gitHub上了,欢迎各位大佬食用: