记录一次开发可视化表单拖拽生成工具开发完的收获新得

最近因为公司有需求,正好把老早就感兴趣的可视化表单生成工具给研究了一番,现在记录下心得体会。

当时第一次看到这种拖拽生成表单或者页面的工具,就觉得很神奇,不知原理为何,后来查阅了一些资料和参考了一些开源项目,通过拆轮子搞懂了原理,解决了 自己的疑惑,在这里不将很详细的实现步骤,就记录下概要,自己在这个过程中的收获。具体项目代码git地址https://github.com/Miaodashu/form_build.git

一、 核心拖拽功能怎么实现的

1.  使用原生的`HTML draggable` 属性,开启标签的`draggable="true"`,然后监听各种拖拽事件。
2. 使用vuedraggable(Vue.Draggable是一款基于Sortable.js实现的vue拖拽插件)

因为项目技术选型时使用vue+element实现的,使用原生的话需要写很多复杂的事件交互(偷懒而已),有现成的轮子就直接使用了。 具体可以看这个vuedraggable中文文档上面有具体介绍和很多小列子。

二、 整体框架怎么设计

整体呢可以分为三个大模块,

  • 左边模块为各种组件列表
  • 中间为拖拽或者点击左边组件生成的可拖动展示页面
  • 右边为组件的一些个性化配置(也可以添加全局配置功能)

左面模块:可以说是组件列表模块。 一般都会根据一定规则进行一下简单归类,比如说,一部分是基础组件,一部分是选择组件,一部分是容器组件
中间模块:作为展示模块,与左右模块联动
右边模块:对选中的组件进行一些参数配置
效果图如下
在这里插入图片描述
整个工具的核心其实依托于自定义的JSON数据,针对这些JSON数据做一些解析渲染的操作罢了,

 // 数据示例
  [{
    // 组件的自定义配置
    __config__: {
      label: "单行文本", // 标题
      labelWidth: null, // 标签宽度
      showLabel: true, // 展示标题

      tag: "el-input", // 组件name
      tagIcon: "input", // 左边面板的展示icon
      defaultValue: undefined,
      required: true,
      layout: "colFormItem", // 组件的布局容器属性
      span: 24, // 表单栅格
      // 正则校验规则
      rules: "",
      rulesMsg: ""
    },
    // 其余的为可直接写在组件标签上的属性
    placeholder: "请输入",
    style: { width: "100%" },
    clearable: true,
    maxlength: 15,
    "show-word-limit": false,
    readonly: false,
    disabled: false
  },
  {
    // 组件的自定义配置
    __config__: {
      label: "多行文本", // 标题
      labelWidth: null, // 标签宽度
      showLabel: true, // 展示标题
      tag: "el-input", // 组件name
      tagIcon: "textarea", // 左边面板的展示icon
      defaultValue: undefined,
      required: true,
      layout: "colFormItem", // 组件的布局容器属性
      span: 24, // 表单栅格
      // 正则校验规则
      rules: "",
      rulesMsg: ""
    },
    type: "textarea",
    // 其余的为可直接写在组件标签上的属性
    placeholder: "请输入",
    autosize: { minRows: 2, maxRows: 4 },
    style: { width: "100%" },
    clearable: true,
    maxlength: null,
    "show-word-limit": false,
    readonly: false,
    disabled: false
  },]

那么怎么将左边选中的组件 给渲染到中间组件呢,这里当时我找了两种方案,
一种是利用vue提供的动态组件 <component v-bind:is="currentTabComponent"></component>,然后根据自定义json数据的标签标识来渲染出组件
另一个则是使用render函数来进行渲染。

为了更全面贴合element-ui,使之能够灵活使用ui库的各种api,以及自定义组件的props,还有部分性能考虑,最终我选择使用了render函数来进行组件渲染。

render: function (createElement) {
  return createElement('标签名字', {参数配置}, 子节点)
}

createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。它的参数描述以及数据对象概念大家区官网了解下数据对象,
然后将自定义的一些api与数据对象相结合,有的就覆盖,没有就放到attr里面。

然后写渲染功能的注意将逻辑细分一下,不要写的太耦合了。

三、 导出vue模板文件

这个功能主要是根据自定义的一些规则将vue整体页面拼接出来,可以单独的拼js,html和style,然后进行整合,这里具体逻辑就不写了,具体各位去git上拉代码来看,这里主要安利一波本地格式化文本格式的插件js-beautify,因为当我拼接完成后,文本格式化始终存在样式格式化错乱问题,所以找到这个插件可以在本地格式化好,非常省心,还有其他的用法具体看这个插件的git文档

import jsBeautify from "js-beautify";

//使用
   let htmlCode = jsBeautify.html(formBuild(options));
   let jsCode = jsBeautify.js(buildJs(options));
   let cssCode = jsBeautify.css(vueStyle());

这此开发主要认识了和运用一下api

1. Vue.extend()

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象,可以理解为这个函数会返回一个新得vue构造函数,你实例化后就可以使用$mount('要挂载的dom节点类名')挂载到你要挂载的地dom上。下面聚个小栗子,使用场景是假如你有个消息弹框组件,项目中好多地方使用它(忽略注册为全局组件),你每次都需要重复的import...from ...引入,然后components注册,就会显得很麻烦。然后可以使用 Vue.minx + Vue.extend

import pop from './components/pop' // 这里为消息组件
Vue.mixin({
	methods:{
		show(mes, el){
			const constructor = Vue.extend(pop);
			const vm = new constructor();
			vm.$data.mes = mes;
			vm.$mount(el);
		}
	}
})

使用

<div class="mask"></div>
<button @click="show('hello, world','.mask')"></button>

2. webpack的require.context ()

它是webpack的一个依赖管理, 官方文档,不明白这个api会返回什么,可以根据下面的列子自己输出点东西

require.context函数接受三个参数

  1. directory {String} -读取文件的路径
    
  2. useSubdirectories {Boolean} -是否遍历文件的子目录
    
  3. 	regExp {RegExp} -匹配文件的正则
    
require.context('./test', false, /\.test\.js$/);
//(创建出)一个 context,其中文件来自 test 目录,request 以 `.test.js` 结尾。
require.context('../', true, /\.stories\.js$/);
// (创建出)一个 context,其中所有文件都来自父文件夹及其所有子级文件夹,request 以 `.stories.js` 结尾。

它主要能做什么用呢? 在前端工程中,如果遇到从一个文件夹引入很多模块的情况,可以使用这个api,它会遍历文件夹中的指定文件,然后自动导入,使得不需要每次显式的调用import导入模块,实现前端模块自动化导入

比如 将路由/vuex/自定义组件, 当你把上述文件划分成很多模块,你每次都要import引入很多次。
比如 我的vuex的module按照业务划分了很多module, 如下
在这里插入图片描述
以前肯定是 一个个的impoet导入模块,然后再注册模块。使用了require.context()一切就变简单多了

const modulesFiles = require.context('./module', true, /\.js$/)
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = modulesFiles(modulePath)
  modules[moduleName] = value.default
  return modules
}, {})

export default new Vuex.Store({
	actions: {},
    modules
}

上面只是一种应用示例,如果你要导入多个模块,就可以使用。还有如果有很多高频使用的组件,你需要全局注册,就不要用老方案一个个导入了, 你可以将高频组件全部放入一个文件夹中,然后在添加一个叫index.js的文件,在这个文件里使用require.context 动态将需要的高频组件统统打包进来,然后在main.js文件中引入index.js的文件。然后使用Vue.use(),进行插件注册


//  index.js文件
import Vue from 'vue'
function changeStr (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}
const requireComponent = require.context('./', false, /\.vue$/)
// 查找同级目录下以vue结尾的组件
const install = () => {
  requireComponent.keys().forEach(fileName => {
    let config = requireComponent(fileName)
    console.log(config) // ./child1.vue 然后用正则拿到child1
    let componentName = changeStr(
      fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
    )
    Vue.component(componentName, config.default || config)
  })
}
export default {
  install // 对外暴露install方法
}

3. eval()函数

定义和用法 eval() :
函数可计算某个字符串,并执行其中的的 JavaScript 代码。
说明:
该方法只接受原始字符串作为参数,如果 string 参数不是原始字符串,那么该方法将不作任何改变地返回。因此请不要为 eval() 函数传递 String 对象来作为参数。
如果试图覆盖 eval 属性或把 eval() 方法赋予另一个属性,并通过该属性调用它,则 ECMAScript 实现允许抛出一个 EvalError 异常。
抛出:
如果参数中没有合法的表达式和语句,则抛出 SyntaxError 异常。
如果非法调用 eval(),则抛出 EvalError 异常。
如果传递给 eval() 的 Javascript 代码生成了一个异常,eval() 将把该异常传递给调用者

用到这个方法是因为当我想做预览功能时, 我根据自己定义的json数据 生成了字符串格式的vue文件,下面是js部分的示例

{
    data(){
        return {
            formData: {
                field101: undefined,
field102: undefined,
field103: undefined,
            },
            rules: {
                field101: [{"type":"string","required":true,"message":"多行文本不能为空","trigger":"blur"}],
field102: [{"type":"string","required":true,"message":"单行文本不能为空","trigger":"blur"}],
field103: [{"type":"string","required":true,"message":"计数器不能为空","trigger":"blur"}],
            },
            
        }
    },
    created(){
        
    },
    methods:{
        submitForm() {
                this.$refs['elForm'].validate((valid) => {
                  if (valid) {
                    console.log('submit!');
                  }
                });
              },resetForm() {
                this.$refs['elForm'].resetFields();
              }
    }
}

因为这部分是字符串,就需要这里使用eval()将这个转化为可执行的js代码
转化前:可以看到在控制台输出的就是字符串格式
在这里插入图片描述
转化后
在这里插入图片描述
转换为可执行的js代码后,那怎么将 html部分的代码+js的代码转为可预览的页面呢,下来上代码

/*
* @params html template内的html代码
* @params js   javascript内的js代码 
* @params el   要挂载到的dom节点
*/
function init(html, js, el) {
    let jsCode = eval(`(${js})`);
    jsCode.template = `<div>${html}</div>`
    var Profile = Vue.extend({
        template: `<div><child/></div>`,
        components: {
            child: jsCode
        }
      })
    new Profile().$mount(el)
}

export default init;

4. 自定义指令directive

官方文档https://cn.vuejs.org/v2/guide/custom-directive.html

我们通常给一个元素添加 v-if / v-show 来做权限管理,但如果判断条件繁琐且多个地方需要判断,这种方式的代码不仅不优雅而且冗余。
针对这种情况,我们可以通过全局自定义指令来处理:我们先在新建个 directive.js 文件,用于存放与权限相关的全局函数;


// directive.js
export function checkArray (key) {
  let arr = ['1', '2', '3', '4', 'demo']
  let index = arr.indexOf(key)
  if (index > -1) {
    return true // 有权限
  } else {
    return false // 无权限
  }
}

// main.js
import { checkArray } from "./common/directive";
Vue.directive("permission", {
  inserted (el, binding) {
    let permission = binding.value; // 获取到 v-permission的值
    if (permission) {
      let hasPermission = checkArray(permission);
      if (!hasPermission) { // 没有权限 移除Dom元素
        el.parentNode && el.parentNode.removeChild(el);
      }
    }
  }
});

最后我们在页面中就可以通过自定义指令 v-permission 来判断:

<div class="btns">
    <button v-permission="'1'">权限按钮1</button>  // 会显示
    <button v-permission="'10'">权限按钮2</button>  // 无显示
    <button v-permission="'demo'">权限按钮3</button> // 会显示
  </div>
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值