Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于Vue的拖拽组件自动生成活动页面的实现 #22

Open
rico-c opened this issue Mar 24, 2019 · 5 comments
Open

基于Vue的拖拽组件自动生成活动页面的实现 #22

rico-c opened this issue Mar 24, 2019 · 5 comments
Labels

Comments

@rico-c
Copy link
Owner

rico-c commented Mar 24, 2019

活动平台截屏

介绍

厌倦了每次来一个活动写一次活动页,想要以自动化方式自动生成活动页?

趁着公司需求实现了一个活动页的自动构建平台,因为还在编写代码中,这里就讲一下核心的思路:在Vue框架下,通过引入组件库的形式,自动化识别可使用的组件,并以拖拽加输入属性的形式形成一个完整的VNode结构,通过NodeJs模板引擎的处理,自动化生成活动页,同时开启webpack-dev-server,提供活动页搭建的实时预览。

如何操作

画了个简单的草图,描述下大概的场景,我们可以在左侧的组件区拖动对应的组件到拖拽区,这时候检测到拖拽完成,就向Node端发送Ajax请求,Node端通过模板引擎重新更新代码,webpack-dev-server进行热更新,然后前端的实时演示区的页面得到更新。

1

流程图

2

各部分功能实现

UI组件库

UI组件库是活动页搭建的基础资源,这里的组件库的实现可以参考类似Mint-UI等基于Vue的UI库,目的是向Node端导出组件,向前端平台输出组件信息。为了前端更好的获取UI库信息(例如,该组件的各项Prop值都是用来做什么的,默认值是什么),我们需要对组件内部的props选项进行更丰富的配置,同时,为了前端在拿到组件信息的时候知道每个英文名字的组件名对应的中文功能是什么,还需要在组价内新增对组件的描述字段。

下面提供一个简单的示例:

<template>
  <div class="btn" @click="jump">{{btnname}}</div>
</template>
<script>
export default {
  name: "Button",
  intro: "按钮",  //这里写入组件的介绍用于前端展示
  props: {
    btnname: { type: String, default: "请输入按钮名称", desc: "按钮名称" },
    jumpUrl: {
      type: String,  //这里可以决定前端在输入props值时的输入样式,例如,输入框/单选按钮等
      default: "https://www.juejin.com",  //prop的默认值
      desc: "点击跳转链接"  //该字段用于前端对该prop的描述,辅助用户更好理解该如何输入prop值来控制组件
    }
  },
  methods: {
    jump() {
      location.href = this.jumpUrl;
    }
  }
};
</script>

按上述配置好后,一个组件的基础信息就完善好了,接下来使用import导入组件,注册install方法,以使组件库能够被Vue.use()注册使用(具体UI组件库的实现可以参考其他组件库的实现,这里不再赘述)。

前端拖拽编辑平台

作为前端平台,有几个功能需要完成:

  1. 引入组件库并识别组件名和组件信息
  2. 通过拖拽组件和prop配置形成页面结构信息
  3. 将页面信息与Node端通信
  4. 实现一个实时预览区域

下面分开来讲一下如何实现:

1. 引入组件库并识别组件名和组件信息

在前端main.js中导入组件库,并遍历组件,并剔除install,余下为所有我们写好的组件。

然后分别读取每个组件的name、intro(中文介绍),同时提取每个prop对应的描述、类型,用于前端描述每个组件的详情。

import UI from 'UI/index.js';

Vue.prototype.UIData = Object.values(UI).filter(item => {
  // 剔除install方法
  return Object.prototype.toString.call(item) === "[object Object]"
}).map(item => {
  let newPropObj = {};
  let propDesc = [];
  let propType = [];
  for (let p in item.props) {
    newPropObj[p] = item.props[p].default;
    propDesc.push(item.props[p].desc);
    propType.push(item.props[p].type.name);
  }
  return {
    name: item.name,
    intro: item.intro,
    config: { props: newPropObj, propDesc: propDesc, propType: propType, style: {} },
    coms: []
  }
});

现在,我们就可以通过访问this.UIData获取UI库中每个组件的信息及每个组件对应的prop的信息。

2.通过拖拽组件和prop配置形成页面结构信息

首先来实现拖拽形成组件,这里我们需要了解一下Vue的渲染函数官方文档点这里,在Node端我这里是使用了createElement函数,接受三个参数,分别是html标签,config参数和子元素的配置参数。

下面是该函数在Vue的官方文档介绍:

createElement(
  // {String | Object | Function}
  // 一个 HTML 标签字符串,组件选项对象,或者
  // 解析上述任何一种的一个 async 异步函数。必需参数。
  'div',

  // {Object}
  // 一个包含模板相关属性的数据对象
  // 你可以在 template 中使用这些特性。可选参数。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选参数。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

在前端进行拖拽操作后,我的目标是形成一个可被createElement函数使用的VNode参数,同时还应该允许将一个组件拖到另一个组件中形成层叠结构,也就是将一个组件的数据放在另一个组件数据createElement的第三个参数中。

为了实现拖拽功能,这里我们就需要使用一个第三方库,我这里使用的是SortableJS,这个库有一个Vue的封装版本叫Vue.Draggable,这个库的在线演示效果可以访问这里

OK,进到演示页面翻到最下面可以看到有一个"Nested Sortables Example",也就是层叠拖拽,我这里就是使用了层叠的模式,来实现VNode数据的拼装。

组件区代码,用于向拖拽区拖出组件,这里有一点需要注意:

1)因为组件区的组件需要多次向拖拽区拖拽,默认模式下拖拽一次后,组件区的数据将被转移到拖拽区,这里需要设置为clone模式,以支持多次拖拽。

2)直接拖出去会被认为是组件的浅拷贝,当有多个相同的组件出现在拖拽区后会出现数据冲突的问题,这里需要将每一个拖出去的组件进行深拷贝处理,以保证每一个拖拽区的组件都有独立的数据。

<draggable v-model="UIData" :options="option" :clone="deepClone" :sort="false">
  <el-menu-item
     v-for="(element,index) in xzUIData"
     :key="element.id"
  >{{element.intro}} - {{element.name}}</el-menu-item>
</draggable>

option: {
    group: { name: "optionname", pull: "clone", put: false }
}

deepClone(el) {
   return JSON.parse(JSON.stringify(el));
}

这时,我们从组件区拖拽向拖拽区后,就可以形成层叠的VNode结构,此时createElement函数的第一个和第三个参数就搞定了,然后要解决的第二个参数,也就是对应每个组件的配置。

我这里是通过写了一个组件,通过递归调用组件的方式达到修改每一层prop的目的。

<template>
  <div >
    <draggable
      :element="'ul'"
      :list="coms"
      :options="{group:{ name:'optionname'},onEnd:handleDragEnd()}"
    >
      <li :key="index" v-for="(el,index) in coms" class="drag_li">
        <span>{{el.name}}{{el.intro}}</span>
        <div @click="set(index)" class="settingProp">设置参数</div>
        <xzdrag v-if="el.coms" :coms="el.coms" @set="upSet()"></xzdrag>
      </li>
    </draggable>
  </div>
</template>

<script>
import draggable from "vuedraggable";
export default {
  name: "xzdrag",
  components: {
    draggable
  }
}
</script>    

这里v-for遍历的coms数据就是Vue.Draggable经过拖拽后生成的JSON数据,现在来遍历这个数据,同时,每当一个组件的coms数组(coms字段为我这里定义的createElement函数要用到的第三个参数,也就是子元素的数组)不为空时递归调用该组件,当点击对应组件的设置参数按钮后,就可以直接根据当前的层级数直接修改VNode数据中对应每个组件的prop参数。

现在,经过拖拽形成层叠的VNode数据,同时在递归的组件中设置prop,已经得到了一个完整的可用于createElement函数渲染页面的数据,现在这个数据已经可以准备传给Node服务用来拼装页面了。

在完成了构建页面的基本数据后,渲染一个活动页面当然还需要其他的数据,例如:活动名称、页面标签的标题、该页面是否允许分享、该页面是否允许浏览器打开、是否在加载时检验用户登录等全局功能,这些数据我们可以单独维护一个区域让用户输入相关参数,然后在模板文件中我们编写好代码,例如:

// handlebars模板语法
const isCheckLogin = {{isCheckLogin}};
//模板App.vue
mounted(){}
  if(isCheckLogin){
    // 实现代码
  }
}

在模板中写好相关代码并制定一个变量作为开关,当前端用户选择使用该功能时,isCheckLogin的代码就将被执行。

OK,所以现在,我们的前端要传给后端的数据已经全部准备好了,按照功能分类可以分成三块:

1)活动页基本信息

2)活动页全局功能的配置

3)活动页页面组件结构的VNode数据

3.将页面信息与Node端通信

VueDraggable库有拖拽完成后的事件钩子,每当拖拽完成后触发,同时,给VNode数据中的prop配置字段添加watch,当检测到prop数据有变时页需要手动触发拖拽完成的事件钩子,在拖拽事件钩子中,实现将当前的VNode数据和活动页的其他数据统一发送Ajax给Node服务端,由Node端做页面的拼装。

这里需要注意一件事,因为用户在使用时需要大量的进行参数修改和拖拽操作,所以在拖拽事件钩子上需要加一个防抖函数进行处理,我这里是加了500ms的防抖,防止发送过多的Ajax请求给Node端。

4.实现一个实时预览区域

前端通过使用iframe,并在iframe中使用对应活动的实时预览url即可,这里需要使用Node回传的端口号进行url确认,下节会详细讲到。

我们肯定也希望在前端能够像控制浏览器一样的控制iframe,但事实上由于iframe与前端网页跨域的原因,很多操作都会被限制,这里讲几个我是如何实现几个关键操作的。

1)iframe手动刷新

在前端页面执行this.$refs.iframe.src=this.$refs.iframe.src即可刷新。

正常情况下,由于webpack-dev-server在开启热更新后,可以自动刷新iframe,所以大部分情况下是无需使用该项的。

2)清除活动页的localStorage等缓存

使用postMessage发送跨域通知,在活动页中注册window.addEventListener("message", receiveMessage, false) 监听,可以实现活动页的缓存清理。

3)二维码

用户使用时希望能在手机上实时看到该页面,这里我使用了qrcode第三方库,根据url生成二维码,可以提供用户扫描实时在真机上预览。

NodeJS实现页面拼装功能

Node端的文件结构:

  1. Node路由处理及中间件
  2. 使用模板语法编写好的模板活动页文件
  3. UI组件库代码
  4. 活动页目录(用于存放生成的活动页文件)

分开介绍一下:

1. Node路由处理及中间件:

用于处理前端Ajax请求并完成页面构建流程

2. 使用模板语法编写好的模板活动页文件:

本质上一个使用Vue-cli生成的Vue项目,然后将App.vue使用handleBars模板语法改写,使之能够接收前端传来的参数,根据参数形成不同的页面。

<script>
//App.vue
import Vue from "vue";
const mountedHook = JSON.parse(JSON.stringify({{{json mountedHook}}}));
const actInfo = JSON.parse(JSON.stringify({{{json info}}}));
const VNode = JSON.parse(JSON.stringify({{{json VNode}}}));
export default {
  name: "App",
  data(){
    return {}
  },
  created(){
    // 设置页面title
    window.document.title = actInfo.actPageName?actInfo.actPageName:'default';
  },
  mounted() {
    window.addEventListener("message", receiveMessage, false) ;
    function receiveMessage(event) {
      switch (event.data){
        case 'clearStorage':
          localStorage.clear()
          break;
      }
    }
    // 检查登录并跳转登录 
    if(mountedHook && mountedHook.checkLogin){
      this.$checkLogin();
    }
  },
  render: function(createElement) {
    var create = function(a) {
      if(a.length){
        return a.map(item => {
          return createElement(item.name, item.config, create(item.coms));
        });
      }else{}
    };

    return createElement(
      VNode.name,
      VNode.config,
      VNode.coms.length?VNode.coms.map(item => {
        return createElement(item.name, item.config, create(item.coms));
      }):[]
    );
  }
};
</script>
3. UI组件库代码:

用于模板活动引用,在模板中引入组件库,如果前端传过来的VNode数据中有使用该组件,则该组件会被渲染出来。

4. 活动页目录(用于存放生成的活动页文件)

每当新建一个活动页时,当使用前端传来的数据通过模板语法渲染后的活动代码的文件夹,会被复制到活动页目录中,作为活动页的文件夹。

工作流程:

首先我们要开两个核心的路由监听,其中一个是新建页面,一个是更新页面。

新建页面接口:

Node监听到新建页面请求后,首先将Handlebars模板语法使用一个初始化的空数据渲染后的页面的文件夹复制到活动页目录下,同时在该目录下开启webpack-dev-server,监听返回值,当监听到当前webpack的server的端口号后,发送response给前端,将当前活动页的devserver的端口号通知前端,同时前端将该端口号与域名拼接,注入到前端页面的iframe的src中,这时候,前端对活动页面的实时预览就完成了。

if (fs.existsSync(AppFilePath)) {
      const content = fs.readFileSync(AppFilePath).toString();
      const result = handlebars.compile(content)(componentsData);
      fs.writeFileSync(AppFilePath, result);
      var child = shell.exec(`webpack-dev-server --inline --config ${newActPath}/build/webpack.dev.conf.js`, { async: true });
  
      var initBack = true;
      child.stdout.on('data', function (data) {
        if (data.indexOf('http://0.0.0.0') !== -1) {
          console.log('devServer启动');
          initBack && res.send(data);
          initBack = false;
        }
      });
    }
页面更新接口:

当前端有拖拽的操作后,前端就会开始向后端发送带有VNode数据的Ajax请求,这时再次用Handlebars模板语法将App.vue的模板数据进行更新,这时webpack-dev-server会对比差异后进行热更新,前端通过iframe即可看到实时更新后的页面。

活动上线

当页面构建完成后,可以使用数据将VNode等数据存储起来用于二次编辑,也可以直接执行webpack的build操作生成打包后文件,这时dist中的文件即可用于上线使用。

几个需要注意的问题

1.活动数量增多后node_modules占用空间过大

因为所有的活动都是基于同一个UI组件库,所以复用度非常高,同时每一个活动配一个node_modules文件夹确实太费空间,这里我们可以在创建活动的时候,过滤掉对node_modules文件夹的复制,同时创建一个向模板文件夹node_modules的软连接ln -s ${templatePath}/node_modules ${newActPath}/node_modules,这时node_modules包的占用体积就可以得到大幅度的节省,从一个新活动占用100+Mb到一个活动5Mb以下

2.webpack-dev-server进程的管理

当用户退出前端页面后,webpack-dev-server并不会随之自动关闭,如果不进行处理,将会占用大量内存,所以这里需要进行一下优化。

首先找到能够检测到的退出编辑节点,在前端页面的后退、跳转到其他页等离开活动编辑页的操作时发送Ajax通知Node当前页使用的webpack-dev-server端口号,然后Node根据端口号杀掉占用指定端口号的任务。但是有的用户直接退出浏览器或者其他情况,则可以在每开一个webpack-dev-server后确定当前的端口号后写一个定时器 ,在一定时间后,将该进程杀掉。

代码参考:kill -9 $(lsof -t -i:${port}

3.Linux暗坑:同时开一定量的devServer后热更新会失效

在Mac上调试时并没有遇到这种问题,但是当代搬到服务器上后,在Linux同时开一定量的devServer后,会遇到热更新失效的问题,这时需要手动修改Linux的inotify的最大监控值

echo 100000 | sudo tee /proc/sys/fs/inotify/max_user_watches

inotify是一个内核用于通知用户空间程序文件系统变化的机制,它的max_user_watches值在Linux上默认为8192,因为webpack的热更新需要使用到这里的相关配置,这里我手动改到了100000,还可以更高。

4.安全校验

因为Node端接口拥有直接操作活动页的权限,所以需要接入校验机制,我这里是和用户的登录状态绑定在一起,当用户登陆后传递Http Header头中带有token信息,拿到token后再去登录的后端接口进行校验,并使用Redis进行缓存减少请求量。

总结

到这里基本讲完了活动页自动构建的核心设计,最终活动页自动构建平台如果搭建完善的话,我个人的目标是前端工程师只需要不断的完善组件库,从而可以将搭建活动页面完全的放心交给产品经理来完成,当然如果要搭建完整的系统还会需要登录、活动审核等功能,因为整体实现还是不会很难这里就不再细说了,如果还有相关的问题的话,可以添加本人的微信ricardo_cy 讨论。
也欢迎大佬指出我的不足~

@rico-c rico-c added the 笔记 label Mar 24, 2019
@rico-c rico-c changed the title 活动页自动构建平台的实现 基于Vue的拖拽组件自动生成活动页面的实现 Mar 25, 2019
@Tiramisupxl
Copy link

你好,请问组件的配置(右边侧栏)这一块是怎么实现的呢

@Z-Leila
Copy link

Z-Leila commented Jul 30, 2020

大佬,求实例源码学习(~ ̄▽ ̄)~

@heyushuo
Copy link

heyushuo commented Sep 9, 2020

为什么要node端还要做处理渲染页面呢? 后台管理系统直接把当前组件的名字传给后台,h5展示的时候,后台把组件名字,和组件的数据返回给前端,前端再用动态组件去渲染

@GracefulTing
Copy link

预览区使用iframe,与拖拽区如何交互的。vue.draggable支持跨iframe拖拽嘛?

@againF
Copy link

againF commented Aug 30, 2021

这个怎么支持多语言

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants