关于前端:浅谈前端可视化编辑器的实现

51次阅读

共计 6384 个字符,预计需要花费 16 分钟才能阅读完成。

背景

前端可视化编辑器在当初大公司中都有各样的实现形式,不同的业务会赋予不一样的性能,能够参考上面这个文章:https://github.com/taowen/awe…,外面介绍了国内很多厂商低代码平台的实现形式。
行业里有许多利用 AI 去切割页面、生产页面的案例,但在短时间要实现产出,须要耗费大量的人力和精力,不倡议在个别团队去做这样的事件。

目标

设计这样一款可视化编辑器,须要达成以下目标

  • 可视化编辑,能够在画布拖拽元素
  • 丰盛的款式配置
  • 将罕用业务抽离成组件,并能够配置组件的参数

设计思路

将这个我的项目分为三个局部:

  • 编辑器
    提供用户治理我的项目、拖拽元素、配置参数、预览公布等能力
  • 组件库
    抽离业务逻辑,将其封装成组件库治理保护
  • 服务端
    存储管理我的项目,最重要的是将编辑器生产进去的数据进行解析生成页面

技术栈

  1. 因为团队里对 vue 比拟相熟,前端的编辑器和组件库都应用 vue 作为开发框架
  2. 服务端应用 egg.js,思考到 egg 较为成熟,不须要本人再从 0 搭起。当然,也能够抉择其余框架。

技术能够依据集体或者团队偏好本人决定。

设计思路

一、JSON Schema

整个我的项目除了设计成三个外围工程之外,还有一段 JSON Schema,这段 JSON 在整个我的项目中起到至关重要的局部,它是形成页面的基石。JSON Schema 是由编辑器生产,并且每个生产出的 JSON Scheme 的格局必须保持一致,这样,服务端渲染的时候才能够对我的项目的 JSOn 进行解析,并且最终渲染出页面。
将 JSON 划分为三个层级

1. 我的项目级

我的项目级的 JSON 次要是介绍我的项目的根本信息

{
  id: '', // 我的项目惟一标记
  name: '', // 项目名称
  label: [], // 我的项目标签
  description: '', // 我的项目形容
  author: '', // 作者
  pages: [] // 页面}

2. 页面级

页面级的 JSON 能够了解为咱们的一个 html 页面,外面包含页面脚本,页面款式、元素等

{
  id: '', // 页面惟一标记
  type: '', // 页面类型
  route: '', // 页面路由
  title: '', // 页面题目
  style: { // 页面根元素的款式
    width: '',
    height: '',
    ......
  },
  elements: [], // 组件
  plugins: [], // 页面插件服务}

3. 组件级

组件是组成页面的外围局部,能够了解为页面都是由一个个具备特定性能的积木沉积而成。

{
  id: '', // 组件惟一标记
  elementName: '', // 组件名
  style: { // 组件通用款式
    position: '',
    width: '',
    height: '',
    border: ''
  },
  props: {}, // 组件属性参数
  children: [], // 子组件
  ......
}

其中值得一提的是,props 能够了解 vue 里的 props,通过给组件传入 props,组件能够依据参数给于绝对应的体现,置信学习过 vue 的组件相干常识的人肯定了解。

最初一个残缺的我的项目的 JSON 长这样

{
  id: '_clv_fPQu',
  name: 'FAB 页',
  label: ['fab', '游戏'],
  description: 'Feature、Advantage 和 Benefit,依照这样的程序来介绍,就是压服性演讲的构造,它达到的成果就是让客户置信你的是最好的',
  author: 'lujintao',
  pages: [
    {
      id: '_Tmy_CdpY',
      type: 'pc',
      route: 'fab',
      title: '游戏页',
      style: {
        width: '750px',
        height: '1334px',
        position: 'relative',
        margin: '0 auto',
        padding: '',
        overflow: '',
        backgroundColor: '',
        backgroundImage: '',
        backgroundSize: '',
        backgroundRepeat: '',
        backgroundPosition: ''
      },
      elements: [
        {
          id: '_aIv_SesL',
          elementName: 'aicc-image',
          style: {
            position: 'absolute',
            width: '100px',
            height: '100px',
            border: '1px solid #eeeeee'
          },
          props: {src: '***/test1.png'},
          children: [],}
      ],
      plugins: [
        {
          name: 'mobile',
          state: true
        },
        {
          name: 'weixinShare',
          state: true,
          data: {
            title: '微信分享的题目',
            description: '微信分享的文案',img: '***/wxshare.png'
          }
        }
      ],
    }
  ]
}

二、组件库

作为页面的积木,组件是非常重要的局部。组件除了须要有像文本组件、图片组件、视频组件等一些根底的性能,还须要一些贴近业务的组件。比方在官网制作中常常能够看到的轮播图组件,亦或者导航栏组件等等,这些组件都须要联合业务进行提炼出通用能力,能力不便用户疾速搭建一个残缺页面的。
搭建组件库须要开发两份代码,一份是组件自身,一份是用来配置组件 props 的配置面板。

1. 组件

组件的作用有两个,一个是提供给编辑器,在画板上可能展现组件,另外一个作用则是将 组件打包 后提供给服务端作为脚本插入。
比方写一个视频组件,代码很简略,定义好 props 就能够了

<template>
    <video :src="src" :autoplay="autoplay" />
</template>
<script>
export default {
  name: 'aicc-video',
  props: {
    src: {
      type: String,
      default: ''
    },
    autoplay: {
      type: String,
      default: ''
    }
  }
}
</script>
  • 在编辑器里的引入形式
import AICCVideo from '@/components/AICCVideo'
  • 在服务端的引入形式,在浏览器环境下,组件是曾经被打包好作为 script 脚本插入。
<script src="https:**/**/aiccvideo.min.js">

2. 组件配置面板

跟组件须要利用到服务端不同,组件配置面板所要做的事件就是生产出自定义数据(JSON Scheme),因而只须要提供给编辑器注册。
还是拿下面的视频组件举例

<template>
  <div class="props-video">
    <input v-model="propsValue.src" />
    <input v-model="propsValue.autoplay" />
  </div>
</template>
<script>
export default {
  name: 'props-video',
  props: {
    value: { // 配置面板在编辑器里挂载的时候是通过 v -model="props" 传入的
      type: Object,
      default: () => {}
    }
  },
  data() {propsValue: {}
  },
  watch: {value(val) {this.propsValue = val},
    // 配置面板更新的时候,子传父
    propsValue(val) {this.$emit('input', val) }
  }
}
</script>

组件配置面板在编辑器的引入形式和组件的引入统一,只须要 require 进来注册挂载到 component 组件上就好了,同时 v-model 绑定以后组件的 props 数据。

三、编辑器

编辑器作为用户应用的惟一窗口,在几个板块里显得重中之重。咱们曾经晓得,整个我的项目的基石是第一点所提到的 JSON Schema。编辑器也不例外,编辑器实质上做的事件就是批改 / 增加 / 删除这段 JSON 数据的各种参数。

1. 如何保护数据

在编辑器中,只须要保护一份 JSON 数据,所以思考应用 vuex 去治理我的项目数据。依据 vuex 官网形容,非常合乎编辑器这种中大型的我的项目

Vuex 是一个专为 Vue.js 利用程序开发的 状态管理模式。它采纳集中式存储管理利用的所有组件的状态,并以相应的规定保障状态以一种可预测的形式发生变化

实现过程

  1. 写一个 name 为 editor 的 store

    • state.js: 存储要保护的数据
    const state = {projectData: {}, // 工程项目数据
      activePageUUID: '', // 以后正在编辑的页面
      activeElementUUID: '', // 以后被选中的组件 UUID
    }
    • action.js
    // 初始化我的项目
    export function initProject({commit, state}, data) {
     let projectData = data
     if (!data) projectData = initProject() // 空我的项目时生成默认 JSON Data
     dispatch('setActivePageUUID', projectData.pages[0].uuid)
    }
    
    // 设置以后被选中的页面的 UUID
    export function setActivePageUUID({commit}, uuid) {state.activePageUUID = uuid}
    
    // 设置以后被选中的元素的 UUID
    export function setActiveElementUUID({commit}, uuid) {state.activeElementUUID = uuid}
    • getter.js
    // 以后选中页面的 JSON 数据
    export function activePage(state) {const idx = state.projectData.pages.findIndex(p => { return state.activePageUUID === p.uuid})
      return state.projectData.pages[idx]
    }
    // 以后选中组件的 JSON 数据
    export function activeElement(state) {const pIdx = state.projectData.pages.findIndex(p => { return state.activePageUUID === p.uuid})
      const activePage = state.projectData.pages[pIdx]
      const eIdx = activePage.elements.findIndex(e => { return state.activeElementUUID === e.uuid})
    }
  2. 编辑器里各页面绑定数据,通过 mapGetters 获取以后页面,以后元素,以及我的项目数据
<template>
  <div>
    <component :is="activeElement.elementName" v-model="activeElement.props" />
  </div>
</template>
 <script>
import {mapGetters, mapState} from 'vuex'
export default {
  computed: {
    ...mapGetters({'activeElement': 'editor/activeElement'}),
    ...mapState({
      pages: state => state.editor.projectData.pages,
      activePageUUID: state => state.editor.activePageUUID,
      activeElementUUID: state => state.editor.activeElementUUID,
    })
  }
}
</script>

2. 如何实现画布拖拽

对于可视化编辑器来说,用户心愿能够可能拖拽画布中的元素进行更改地位。实现思路非常简略

  • 选中元素,监听 mousedown 事件
  • 获取以后按下元素的 offsetTop 和 offsetLeft
  • 获取以后按下鼠标的坐标地位(e.clientX, e.cliengY)
  • 监听 mousemove 事件,获取鼠标挪动的长度,计算出间隔
  • 元素的新坐标 = 间隔 + 按下时元素的 offset 信息

代码如下

function mousedown(e) {
  let newTop = null
  let newLeft = null

  // 记录按下时以后元素地位
  const cTop = e.currentTarget.offsetTop
  const cLeft = e.currentTarget.offsetLeft
  // 记录按下时以后鼠标地位
  const mouseX = e.clientX
  const mouseY = e.clientY
  
  const move = mEvent => {
    // 只是单纯挪动地位,不须要传递事件给后辈
    mEvent.stopPropagation()
    mEvent.preventDefault()
    const cX = mEvent.clientX
    const cY = mEvent.clientY
    // 挪动的地位
    const distanceX = cX - mouseX
    const distanceY = cY - mouseY
    // 新坐标
    newTop = distanceX + cTop
    newLeft = distanceY + cLeft
  }
  const up = () => {document.removeEventListener('mousemove', move, true)
    document.removeEventListener('mouseup', up, true)
  }
  document.addEventListener('mousemove', move, true)
  document.addEventListener('mouseup', up, true)
}

这里其实有优化的空间,在元素进行拖拽的时候,如果实时去扭转 top,left 时,会引起重排。解决办法也很简略,在 mousemove 的过程中咱们应用 transform 去实时显示以后组件的地位,等 mouseup 开释鼠标的时候,咱们再把实在的坐标地位赋值给组件的 Style 里。

四、服务端渲染

咱们在后面编辑器里生产进去的我的项目 JSON 数据,最终须要让服务端这边进行 DSL 解析。因为咱们驳回的技术栈是 Vue,想要生成一个结构化的页面也非常容易,应用 vue 的 render 函数。
具体能够参考官网文档:渲染函数 & jSX
拿第一局部 JSON Schema 的示例,渲染步骤大抵如下:

  • 拿到页面的 page 信息生成页面的 title/seo 等页面配置信息,依据路由生成对应的 ${name}.html 文件
  • 挂载打包好的组件库 <script src="*/*/aiccvideo.cdn.js">
  • 因为应用的 render 函数,不须要编译器,只须要挂载 vue.runtime.js 脚本即可
  • 应用模板引擎进行字符串替换,替换的信息有页面信息、render 函数里的组件数组等
  • 遍历 elements,用 vue 的 render 函数 createElement('组件名', { props: { ... 生产进去的 element JSON 数据}})
  • 解析 style 的 JSON 数据,生成 选择器 {${key}: ${value} } 的样式表
  • 写入文件fs.writeFileSync(文件门路,htmlString)

结语

文章到这就差不多完结了,因为只是简略阐明一下整个我的项目怎么搭建,在具体细节里没有太详尽形容。要写下来,每一块都能够作为一个单元去写。比方

  • 组件库如何打包
  • 我的项目的治理保护,怎么应用 lerna 治理我的项目
  • 如何实现拖拽元素实时扭转元素大小
  • 如何保障提交数据的格局符合要求
  • 如何实现插件化服务,如微信分享等

……
要上线一个残缺可能生产的产品,细节的中央还有很多技术点能够探讨,在这里仅仅只是谈及整个我的项目技术的组成部分和根本实现原理。
欢送有更好的想法~

正文完
 0