前言
在上一篇文章中介绍了样式绑定的实现方式,可查看博客----,事件的绑定在低代码开发工具中是必不可少的,一个良好的设计方案可以提高事件绑定的灵活性,提高组件之间的交互通能力,在这篇文章中讲解一下事件绑定的实现原理。
项目地址要是觉着有意思不妨给颗星,目前项目开源欢迎大家参与项目的开发
实现
1.实现简介
首先将编辑器中编写的object 文本通过eval()的方式转换为js可使用的object,再将object传入到对应的函数(EcVue())中进行处理,将处理过后的函数绑定到对应的页面中,便可在各组件中使用。
单个页面的数据格式如下:
{type:"page",
pageName:'',
label:'',
children:[],
status:{active:false},
data:{},
ecVueInfo:'',// 编辑器的object文本
EcVue:null, // 用于存放页面的object
css:"",
id:""}
2.EcVue
将如下格式的文本传入**createEcVue()**中便可返回EcVue函数对象,将EcVue函数对象放入到对应的页面中,便可在组件上通过对应的方法名、属性名称来进行调用,其中mounted、data、methods与vue中的是一样的作用,可以通过this的方式进行互通调用
export default{
mounted(){
},
data(){
return
}},
methods:{
}}
// 将页面object文本生成函数并返回
export function createEcVue(ecVueInfo) {
ecVueInfo = ecVueInfo.replace("export default", '')
let ecVue = null
try {
if (ecVueInfo && ecVueInfo !== '') {
let info = `()=>{return ${ecVueInfo}}`
let a = eval(info)
ecVue = new ECVue(a())
}
} catch (e) {
console.log(e)
}
return ecVue
}
EcVue 的实现,可以通过这种方式在methods中可以通过this.属性名的方式调用data()中定义的变量
import {registryMethods} from "./registered/registeredMethods";
var noop = function () {}
var sharedPropertyDefinition = {
get() {
},
set() {
}
};
export function ECVue(options) {
if (!this instanceof ECVue) {
console.log('ECVue是一个构造函数,应使用“new”关键字调用');
}
this._init(options)
}
ECVue.prototype._init = function (options) {
var sc = this;
sc.$options = options
//代码进行了删减
initState(sc);
}
function initState(sc) {
var opts = sc.$options; //这里是我们在创建实例的时候传的参数
//如果传了methods 则去调用
if (opts.methods) {
initMethods(sc, opts.methods);
}
if (opts.data) {
initData(sc);
}
if(registryMethods){
initRegistryMethods(sc,registryMethods)
}
// 对应vue 中mounted函数的调用
sc['mounted'] = typeof opts.mounted === 'function' ? opts.mounted : noop
// 用于存放vue中的ref 可通过this.$refs['ref']来进行调用
sc['$refs'] = {}
}
function initRegistryMethods(sc,registryMethods){
for (var key in registryMethods) {
sc[key] = typeof registryMethods[key] !== 'function' ? noop : bind(registryMethods[key], sc);
}
}
function initMethods(sc, methods) {
//循环methods对象
for (var key in methods) {
//给实例增加methods中的方法 这样其实我们就已经可以用vm访问 到methods中的方法了
sc[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], sc);
}
}
function initData(sc) {
var data = sc.$options.data;
data = sc._data = typeof data === 'function'
? getData(data, sc)
: data || {};
var keys = Object.keys(data);
var methods = sc.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
//判断key值有没有跟methods中的key重名
{
if (methods && hasOwn(methods, key)) {
console.log(key + "与methods重名");
}
}
proxy(sc, `_data`, key)
}
}
function proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function bind(method, sc) {
return method
}
function hasOwn(method, key) {
return method[key] ? true : false
}
function getData(data, sc) {
return sc.$options.data()
}
当有些函数需要重复使用时便可通过添加自定义的函数,在此处也可以绑定api等对应的方法,在methods中通过this.自定义方法名的形式进行调用。
import {ecRouter} from "../core";
export const registryMethods = {
"router":ecRouter,// 路由跳转方法
}
3.组件的封装
在这里用input组件来演示讲解EcVue函数的使用
input组件的注册信息如下:
**component:**用于记录组件的配置信息如样式,事件,属性等相关内容
**setter:**用于配置组件的设置相关的内容
1.attributes 用于配置组件的属性
bind: ‘value’ 标识的属性为需要进行v-model属性的绑定,当对应属性所需属性值为Function、object时可以设定bind:'value’通过数据绑定的方式来实现其效果。
bind:‘ref’ 标识的属性为需要进行ref的绑定,考虑到ref值的固定性,其设定为对应的ref名不可更改,若在添加组件时有冲突会进行名称增加数字后缀的操作以进行区分
2.events 用于配置组件中的事件
export const ScInput = {
component: {
component: "ScInput",
label: '输入框',
events: {},
attributes: {},
styles: {},
},
setter:{
component: "ScInput",
setter: {
attributes: [
{
attributeName: "inputValue",
label: "value绑定",
detail:"绑定值",
bind: 'value',
type: "input",
value: "inputValue",
defaultValue: "inputValue",
},
{
attributeName: "inputRef",//组件配置中属性字段名
label: "绑定inputRef",
bind:'ref',
type: "input",
value: "inputRef",
defaultValue: "inputRef",
},
{
attributeName: "type",
label: "类型",
type: "select",
value: "text",
defaultValue: "text",
detail:"类型",
typeArray: [{
value: 'text',
label: 'text'
}, {
value: 'textarea',
label: 'textarea'
},]
},
],
styles: {},
events: [
{
event: "blur", // 事件名称
enable: false,// 是否启用
detail:"当选择器的输入框失去焦点时触发",
method: ''// 绑定方法名
},
{
event: "focus", // 事件名称
enable: false,// 是否启用
detail:"当选择器的输入框获得焦点时触发",
method: ''// 绑定方法名
},
{
event: "change", // 事件名称
enable: false,// 是否启用
detail:"仅当 modelValue 改变时,当输入框失去焦点或用户按Enter时触发",
method: ''// 绑定方法名
},
{
event: "input", // 事件名称
enable: false,// 是否启用
detail:"在 Input 值改变时触发",
method: ''// 绑定方法名
},
{
event: "clear", // 事件名称
enable: false,// 是否启用
detail:"在点击由 clearable 属性生成的清空按钮时触发",
method: ''// 绑定方法名
}
],
}
},
}
封装组件
v-model的绑定 考虑到要保留其响应式的效果采用computed的方式来进行处理,通过调用getPageData()与setPageData()的方法来与EcVue中的data()中设定的变量进行值的交换。
事件绑定 在组件调用的事件中,使用**this.EcVue[‘方法名’]**的形式调用在EcVue中注册的事件,在调用对应的事件之前通过执行execMethod()方法来确认对应事件是否可以执行。
this.EcVue[this.propValue.events['focus'].method](方法参数...)
ref绑定 在组件的mounted生命周期时将组件的ref="inputRef"通过bindRefs()方法传入到EcVue中的$refs中进行使用。
<template>
<el-input
v-bind = "propValue.attributes"
v-model="inputValue"
ref="inputRef"
@change="changeMethod"
@blur = "blurMethod"
@focus="focusMethod"
@input="inputMethod"
@clear="clearMethod"
/>
</template>
<script>
import { getPageData, setPageData,execMethod,bindRefs} from "@/utils/core";
export default {
name: 'ScInput',
props: {
propValue: {
type: Object, String,
default: function () {
}
},
EcVue:{
type:Function,
default:()=>{}
}
},
computed:{
inputValue: {
get(){
// 绑定事件监听
return getPageData(this.propValue.attributes['inputValue'],this.EcVue)
},
set(value){
setPageData(this.propValue.attributes['inputValue'],value,this.EcVue)
}
}
},
mounted() {
bindRefs(this.propValue.attributes,this.$refs['inputRef'],'inputRef',this.EcVue)
},
methods: {
changeMethod(val){
if(execMethod(this.propValue.events['change'])){
this.EcVue[this.propValue.events['change'].method](val)
}
},
blurMethod(val){
if(execMethod(this.propValue.events['blur'])){
this.EcVue[this.propValue.events['blur'].method](val)
}
},
focusMethod(val){
if(execMethod(this.propValue.events['focus'])){
this.EcVue[this.propValue.events['focus'].method](val)
}
},
inputMethod(val){
if(execMethod(this.propValue.events['input'])){
this.EcVue[this.propValue.events['input'].method](val)
}
},
clearMethod(){
if(execMethod(this.propValue.events['clear'])){
this.EcVue[this.propValue.events['clear'].method]()
}
}
}
}
</script>
<style scoped>
</style>
getPageData方法
若想给组件属性绑定form中的name属性,可以通过form.name的形式进行绑定,getPageData()先通过analysisData()获得一个属性的数组[‘form’,‘name’]在进行循环调用以获取name中的数值,setPageData()同理。通过这种方式可以应用到表单组件的form-item中进行v-model的绑定
// 调用示范 form.name
form:{
name:'Tom'
}
// 调用示范 formArrauy.0.name
formArrauy:[
{name:'Tom'}
]
// 返回当前页面绑定的数据
export function getPageData(attribute, EcVue) {
const params = analysisData(attribute)
let setData = EcVue
let result = ''
if (params.length > 0) {
let length = params.length
result = (setData !== null && setData[params[0]]) ? setData[params[0]] : ''
if (setData) {
for (let i = 1; i < length; i++) {
result = result[params[i]]
}
}
return result
}
}
setPageData方法
// 设置当前页面绑定的数据
export function setPageData(attribute, value, EcVue) {
const params = analysisData(attribute)
let setData = EcVue
if (params.length > 0) {
let length = params.length
for (let i = 0; i < length - 1; i++) {
setData = setData[params[i]]
}
setData[params[length - 1]] = value
}
}
analysisDataf方法
// 解析数据绑定的数值
function analysisData(param) {
if (!param) {
return []
}
return param.split(".")
}
bindRefs方法
export function bindRefs(attr, refs, name, EcVue) {
if (!EcVue.$refs) {
EcVue['$refs'] = null
}
if (!refs) {
refs = null
}
if (attr[name]) {
let i = 0
let value = attr[name].replace(/[0-9]+/g, "")
while (EcVue.$refs[attr[name]]) {
i++
attr[name] = value + i
}
EcVue.$refs[attr[name]] = refs
}
}