前言
随着数字化转型的不断发展,低代码开发平台已成为企业快速建立自己的应用程序的首选方案。然而,实现这样一个平台需要具备高效、灵活和可定制化的能力。这正是基于描述依赖渲染(Description dependency rendering)
所实现的。通过使用该技术,我们可以实现动态渲染组件,从而大大提高开发效率和灵活性。此外,组件的递归调用、扁平化的事件处理还能实现复杂的界面效果,让用户体验更加友好。本文将介绍如何使用Vue.js实现描述依赖渲染,让您在几分钟内轻松掌握这一技术,并享受到它所带来的便利。作为前端开发人员,了解这种前沿技术将有助于您在低代码领域中保持竞争优势。因此,通过阅读本文并掌握这一重要技术,是做出低代码开发平台的第一步,也是信息化创新的重要一环。
效果展示
动态根据JSON构建组件:
开发环境准备
1、首先我们需要建立一个使用js的vue3项目,如果还没有vue3项目,则用以下命令手动创建一个,如果没有这个命令,那么请先准备好node.js环境、npm环境和vue-cli环境,这里不再赘述。
vue create .
2、本示例会用到angular-expressions和axios,所以需要先npm install angular-expressions
、npm install axios
安装一下。这里axios负责进行网络通讯,angular-expressions负责实现脚本解析引擎,它因为运行在独立的沙盒环境里,具有比eval和new Function更好的安全性,可以一定程度上防止XSS攻击。
引入控件系统思想
控件系统思想是一种将应用程序拆分成多个可复用的组件的编程模型。它可以提高开发效率,降低代码复杂度,使得应用程序更易于维护和扩展。在控件系统中,每个控件都有自己的方法、事件和属性,并且可以被其他组件调用和操作。
这里可以参考传统老牌的微软COM模型,它具有以下核心特点:
组件化:将应用程序拆分成多个可复用的组件;
接口化:每个组件都有自己的接口,允许其他组件调用和操作;
注册表:组件需要在注册表中注册,以便其他应用程序可以找到并使用它们。
与微软COM类似,VUE的组件也具有类似的特点。VUE组件是一个可复用的代码块,具有自己的方法、事件和属性,并且可以被其他组件调用和操作。
区别在于,VUE组件是基于Web技术的,可以轻松地嵌入到HTML页面中,并且可以与其他Web技术(如CSS和JavaScript)无缝集成。而微软COM则是基于Windows操作系统的,只能在Windows平台上运行。所以我们需要在Web上复刻出来控件系统只需要将接口化变为事件驱动,注册表变为全局自动注册即可。
在控件系统中,方法、事件和属性是控件的三个核心组成部分。方法用于执行控件的操作;事件用于通知其他控件发生了某些事情;属性则用于存储和获取控件的状态信息。
如上图,通过这个例子,可以一目了然一个控件的主要功能。
方法、事件和属性的作用和重要性在控件系统中非常重要。
通过方法,我们可以执行控件的操作,例如:设置控件的值、获取控件的状态等;
通过事件,我们可以通知其他控件发生了某些事情,例如:当用户点击按钮时触发Click事件;
通过属性,我们可以存储和获取控件的状态信息,例如:文本框的值等。
控件系统思想可以提高开发效率,降低代码复杂度,使得应用程序更易于维护和扩展。方法、事件和属性则是控件的三个核心组成部分,在控件系统中具有重要的作用和意义。接下来我们基于控件系统思想开始进行描述,从而实现描述依赖渲染DDR的一个简单模式。
定义描述格式
文件位于public/test.json,实际项目中,这个JSON是由后台拼装后提供的
{
"is":"uiPage",
"props":{
"id":"uiPage1",
"style":"background-color:#f3f3f3;padding:20px;box-sizing:border-box;"
},
"controls":[{
"is":"uiTextBox",
"props":{
"id":"uiTextBox1",
"text":"在这里输入姓名"
}
},{
"is":"uiButton",
"props":{
"id":"uiButton1",
"text":"确认"
},
"events":{
"Click":"this.uiLabel1.text=this.uiTextBox1.text"
}
},{
"is":"uiLabel",
"props":{
"id":"uiLabel1",
"text":"这是一个Label组件",
"style":"color:blue"
}
}]
}
这个JSON是一个描述界面的示例,也被称之为界面描述或界面模型,它描述了一个包含三个组件的页面。其中,uiPage是一个页面容器组件,它的props属性指定了页面的id和样式。controls属性中包含了三个子组件:uiTextBox1、uiButton1和uiLabel1。
uiTextBox是一个文本框组件,它的props属性指定了文本框的id和默认文本。
uiButton是一个按钮组件,它的props属性指定了按钮的id和显示文本,同时还定义了一个events属性,用于配置该按钮的点击事件。在该示例中,当按钮被点击时,执行this.uiLabel1.text = this.uiTextBox1.text
语句,将文本框中的内容显示在uiLabel1组件中。
uiLabel是一个标签组件,它的props属性指定了标签的id、默认文本和样式。
这段JSON描述了一个简单的界面,下面我们将其转换为Vue.js组件进行渲染。通过使用描述依赖渲染(DDR)技术,我们可以轻松地实现低代码开发平台,快速构建出符合需求的应用程序。
自动化引入组件
接下来我们改造main.js,为了能快速开发组件,我们让webpack自动扫描路径并全局注册组件
let app = createApp(App);
const requireComponent = require.context(
// 其组件目录的相对路径
'./components',
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/\.vue$/
)
// 自动引入组件开始
const requireComponents = require.context("/src/components", true, /\w+\.vue$/);
requireComponents.keys().forEach((item) => {
const componentConfig = requireComponents(item);
const componentName = item
.split("/")
.pop()
.replace(/\.\w+$/, "");
app.component(componentName, componentConfig.default || componentConfig);
});
这段代码将自动引入组件SFC文件,它通过require.context函数来实现自动化引入。context函数在第一个参数中,我们指定了组件所在目录的相对路径;第二个参数表示是否查询其子目录;第三个参数则是匹配基础组件文件名的正则表达式。
在第二段代码中,我们使用了require.context函数来获取所有组件的上下文信息,并通过forEach函数遍历每个组件。在遍历过程中,我们首先获取组件的配置信息componentConfig,然后提取组件的名称componentName,并通过Vue.js的全局方法app.component将其注册为全局组件。需要注意的是,由于require函数返回的是一个对象,因此我们需要使用default属性来获取组件的默认导出。
通过这段代码,我们可以实现组件的自动化引入,避免手动引入组件时可能出现的疏漏和错误,提高开发效率。
定义元组件
元组件相当于真正组件的一个逻辑层包装器,模拟了真实组件环境中的嵌套效果。
文件位于src/components/metaComponent.vue
<template>
<component :is="data.is" v-bind="data.props" :context="context" :initData="data">
<metaComponent v-if="data.controls" v-for="(item,i) in data.controls" :data="item" :context="context">
</metaComponent>
</component>
</template>
<script>
export default {
props: ['data', 'context']
}
</script>
<style>
</style>
首先,该组件使用了Vue.js的动态组件功能,通过传递一个data对象的is属性来指定渲染的组件类型,具体组件则是上文中我们通过自动化引入src/components下的对应SFC。另外,组件的props属性也通过v-bind指令动态绑定了data.props中的所有属性,实现了属性透传。此外,还传递了context和initData属性,其中context是上下文对象,用于在组件内部访问其他资源,例如API、状态管理器等;initData则是组件初始化数据。组件还包含了一个metaComponent子组件,用于渲染data.controls中定义的所有子元素,这样就实现了递归自动渲染,可以渲染无线层级。
我们再来看script,props属性接收了两个参数:data和context。data是一个对象,包含了组件所需的各种属性和控制元素,context则是组件上下文对象,全局唯一的,我们可以认为对于同一个页面工厂来说,只有一个context。
实现UI工厂
UI工厂是一切渲染的源头,我们通过UI工厂来管理上下文,并将后台传过来的JSON转变成我们能看到的界面
文件位于src/UIFactory.vue:
<template>
<div>
<metaComponent :data="page" :context="context"></metaComponent>
</div>
</template>
<script>
import axios from 'axios'
import expressions from 'angular-expressions'
export default {
components: {},
data() {
return {
page: {},
context: {
fireEvent: function(control, event) {
if (control.initData.events && control.initData.events[event]) {
let code=control.initData.events[event]
expressions.compile(code)(this)
}
},
initControl(control) {
let {
id,
initData,
context
} = control;
let proxyObj = {}
for (let key in initData.props) {
Object.defineProperty(proxyObj, key, {
get() {
return initData.props[key]
},
set(value) {
initData.props[key] = value;
}
})
}
for (let methodName in control) {
if (typeof control[methodName] == "function") {
proxyObj[methodName] = control[methodName]
}
}
proxyObj.controls = initData.controls
context[id] = proxyObj;
}
}
}
},
mounted() {
axios.get('/test.json').then((data) => {
let rData = data.data
this.page = rData
})
}
}
</script>
该组件使用了metaComponent自定义组件来动态渲染页面。
在script中,该组件引入了axios和angular-expressions模块,并定义了一个包含两个方法的context对象。其中,fireEvent方法用于触发控件事件;initControl方法用于初始化控件。
在mounted生命周期函数中,该组件使用axios.get方法从服务器获取页面数据,并将页面数据赋值给page属性。通过这种方式,我们可以动态地生成页面。
在methods部分,该组件定义了initControl方法,该方法用于初始化控件。当我们创建一个控件时,该控件会回调initControl方法,为控件创建一个代理对象并注册到上下文对象中。只有通过代理对象的方式,我们才能在脚本引擎的沙盒里实现双向绑定,像this.uiLabel1.text = this.uiTextBox1.text
这样的语法就可以轻松实现,达到对属性的无感访问,非常类似于VB.net和C#的属性读写方式。
该组件还使用了angular-expressions模块来编译和执行控件事件代码。通过这种方式,我们可以在运行时动态地处理控件事件,而且因为运行在沙盒里,安全性要比eval和new Function高出不少。不过需要注意的是尽管它运行在沙盒里,但是如果我们给某一个图片展示控件传递了http://www.example.com/hacker.img?cookie=xxx
这样的图片地址,仍然会有泄漏当前用户cookie的可能性,因为img标签加载src并不在沙盒的管控范围里,同样的,这样的地址传递给一个用了axios的方法也可能有安全问题,需要各位架构师读者们仔细进行代码审查和安全加固。
实现uiPage控件
接下来我们就实现各个控件了,每一种控件实际上都是基于VUE的组件,先实现src/components/uiPage.vue:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: ['id', 'context', 'initData'],
mounted() {
this.context.initControl(this)
}
}
</script>
<style>
</style>
这段代码实现了一个简单的页面容器功能。在模板部分,该组件使用了Vue.js的插槽(slot)功能,将子组件插入到页面容器中,可以很方便的实现嵌套效果。
在脚本部分,该组件定义了三个属性:id、context和initData。其中,id属性指定了页面容器的唯一标识符;context属性用于访问上下文对象,以便在组件内部访问其他资源;initData属性则是初始化数据,用于在页面容器创建时初始化组件状态。
在mounted生命周期函数中,该组件调用了context.initControl方法,该方法用于在上下文对象中注册当前组件,以便在需要时可以对其进行操作。通过这种方式,我们可以在整个应用程序中轻松地访问和管理组件,后面不做赘述。
实现uiTextBox控件
文本框:src/components/iTextBox.vue:
<template>
<input type="text" :value="text" @input="onInput($event)" />
</template>
<script>
export default {
props: ['id', 'context', 'initData', 'text'],
mounted() {
this.context.initControl(this)
},
methods:{
onInput(e){
this.initData.props.text=e.target.value
this.context.fireEvent(this,'Input')
}
}
}
</script>
<style>
</style>
在模板部分,该组件使用了input元素来实现文本框的显示,并使用了Vue.js的双向绑定语法:value来绑定文本框的值。
在脚本部分,该组件定义了四个属性:id、context、initData和text。其中,id属性指定了文本框的唯一标识符;context属性用于访问上下文对象,以便在组件内部访问其他资源;initData属性则是初始化数据,用于在文本框创建时初始化组件状态;text属性则是绑定到文本框的值。
该组件还定义了一个名为onInput的方法,用于处理文本框输入事件。当用户在文本框中输入内容时,该方法会更新initData.props.text的值,并调用context.fireEvent方法触发Input事件。通过这种方式,我们可以在文本框值发生变化时及时驱动自定义事件。
实现uiButton控件
按钮:src/components/uiButton.vue:
<template>
<button @click="click" type="button">{{text}}</button>
</template>
<script>
export default {
props: ['id', 'text', 'context', 'initData'],
methods: {
click() {
this.context.fireEvent(this,'Click')
}
},
mounted() {
this.context.initControl(this)
}
}
</script>
<style>
</style>
在模板部分,该组件使用了button元素来实现按钮的显示,并使用了Vue.js的双向绑定语法:text来绑定按钮的显示文本。
在脚本部分,该组件定义了四个属性:id、text、context和initData。其中,id属性指定了按钮的唯一标识符;text属性用于绑定按钮的显示文本;context属性用于访问上下文对象,以便在组件内部访问其他资源;initData属性则是初始化数据,用于在按钮创建时初始化组件状态。
在methods部分,该组件定义了一个名为click的方法,用于处理按钮的点击事件。当用户点击按钮时,该方法会调用context.fireEvent方法触发Click事件。通过这种方式,我们可以在按钮被点击时及时通知其他组件。
实现uiLabel控件
文本标签:src/components/uiLabel.vue:
<template>
<div>{{text}}</div>
</template>
<script>
export default {
props: ['id', 'text', 'context', 'initData'],
mounted() {
this.context.initControl(this)
}
}
</script>
<style>
</style>
在模板部分,该组件使用了div元素来实现文本标签的显示,并使用了Vue.js的双向绑定语法:text来绑定文本标签的显示文本。
在脚本部分,该组件定义了四个属性:id、text、context和initData。其中,id属性指定了文本标签的唯一标识符;text属性用于绑定文本标签的显示文本;context属性用于访问上下文对象,以便在组件内部访问其他资源;initData属性则是初始化数据,用于在文本标签创建时初始化组件状态。
测试
最后我们把UI工厂显示到界面上,读取我们设置好的json文件,即可看到组装好的界面,经测试,功能一切正常:
总结
低代码开发平台基于模型驱动,而模型驱动后的控件则是解决复杂度的关键,控件系统思想是一种将应用程序拆分成多个可复用的组件的编程模型,它可以提高开发效率、降低代码复杂度,使得应用程序更易于维护和扩展。在控件系统中,每个组件都有自己的方法、事件和属性,并且可以被其他组件调用和操作。为了能够将模型驱动与控件系统相结合,所以使用了描述依赖渲染(DDR)思想,作为胶水方法论,将模型转化成了控件。
这一套组合拳打下来不仅仅适用于Web应用程序,还适用于其他类型的应用程序,例如:桌面应用程序、移动应用程序等。它可以帮助开发人员提高开发效率、降低代码复杂度,使得应用程序更易于维护和扩展。
因此,我们应该认真学习低代码、描述依赖渲染、控件系统的思想,掌握概念和实现方式,以便在开发应用程序时更加高效、优雅地进行编程。同时,我们也应该关注利用低代码的信息化发展和创新,探索出更加灵活、高效的编程模型,推动信创领域的不断进步。