Vue + element从零打造一个H5页面可视化编辑器——pl-drag-template
pl-drag-template
Github地址:https://github.com/livelyPeng/pl-drag-template
前言
想必你一定使用过易企秀或百度H5等微场景生成工具制作过炫酷的h5页面,除了感叹其神奇之处有没有想过其实现方式呢?本文从零开始实现一个H5编辑器项目完整设计思路和主要实现步骤,并开源前后端代码。有需要的小伙伴可以按照该教程从零实现自己的H5编辑器。(实现起来并不复杂,该教程只是提供思路,并非最佳实践)
一个h5可视化编辑器种子, 高仿凡科建站模板。
大概图形:
拖动左边组件到画板区域释放即可,或者点击左边区域的组件。
注意: 最好使用谷歌打开,点击保存按钮就是一串json数据,你可以吧这个数据拿到其他手机平台进行渲染啦。有问题就加群 里面代码注释齐全,谁都看懂的哦
在这个模板的基础上,你就可以实现类似凡科的模板(当然你还可以实现其他的类似模板)。如下图就是我们产品的模样
项目目录
src {
apiUrl: 请路径存放
assets: 项目资产存在(图片等)
components: 公用组件存放
module: 模块位置 {
画板模块的配置如下: {
components: 当前模块的私有组件 {
attributeConfig: 右边属性配置组件
... 其他的都是画板页面的组件
}
pluginLibrary: 画板的插件/模块/组件(非常重要)
routers: 当前模块的路由表
style: 当前画板的样式
utils: 公用js存放库
vuex: 当前模块的状态存储
viewPage: 当前模块的页面
index.js: 导出当前模块
}
}
vuex: 整个项目的状态存储汇集地方
themes: 整个项目的公用样式表集中地方
utils: 整个项目的工具文件夹
}
技术栈
前端:vue: 模块化开发少不了angular,react,vue三选一,这里选择了vue。vuex: 状态管理less: css预编译器。element-ui:不造轮子,有现成的优秀的vue组件库当然要用起来。没有的自己再封装一些就可以了。loadsh:工具类
工程搭建
基于vue-cli2环境搭建
- 如何规划好我们项目的目录结构?首先我们需要有一个目录作为前端项目,一个目录作为后端项目。所以我们要对vue-cli 生成的项目结构做一下改造:
··· · |-- client // 原 src 目录,改成 client 用作前端项目目录 |-- server // 新增 server 用于服务端项目目录 |-- engine-template // 新增 engine-template 用于页面模板库目录 |-- docs // 新增 docs 预留编写项目文档目录 · ···
这样的话 我们需要再把我们webpack配置文件稍作一下调整
module.exports = { resolve: { extensions: ['.ts', '.js', '.vue', '.json'], alias: { // 'vue$': 'vue/dist/vue.esm.js', '@': utils.resolve('src') } }, externals: { 'vue': 'Vue', "echarts": "echarts", 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'element-ui': 'ELEMENT', 'moment': 'moment' }, module: { rules: [ ...(config.dev.useEslint ? [createLintingRule()] : []), { test: /\.vue$/, loader: 'vue-loader', options: { transformAssetUrls: { video: ['src', 'poster'], source: 'src', img: 'src', image: 'xlink:href' } } }, { test: /\.js$/, loader: 'babel-loader', exclude: file => /node_modules/.test(file) && !/\.vue\.js/.test(file) && !/element-ui(\\|\/)(src|packages)/.test(file) && !/pl-table/.test(file) }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash].[ext]') } }, { test: /\.less$/, use: [{ loader: process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'vue-style-loader' }, { loader: 'css-loader', options: { sourceMap: cssSourceMap } }, { loader: 'less-loader', options: { sourceMap: cssSourceMap } }, { loader: 'sass-resources-loader', options: { resources: [ path.resolve(__dirname, '../src/themes/publicStyle/common.less') ] } }] }, { test: /\.css$/, use: [{ loader: process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'vue-style-loader', }, { loader: 'css-loader', options: { sourceMap: cssSourceMap } }] }] }, plugins: [ new VueLoaderPlugin(), // 复制静态资源到目录中,如果有更多需要复制的资源,请在这里添加 new CopyWebpackPlugin([{ from: utils.resolve('static'), to: config.build.assetsSubDirectory, ignore: ['.*'] }]) ] }
这样我们搭建起来一个简易的项目目录结构。
前端编辑器实现
编辑器的实现思路是:编辑器生成页面JSON数据,服务端负责存取JSON数据,渲染时从服务端取数据JSON交给前端模板处理。
数据结构(非常重要)
/*
* 注意注意注意: pluginLibrary里面组件的name值必须写,然后必须写下面的elName组件名
* 1. elName: 'pl-text', // 非常重要请正确写上对应的vue组件的组件名,name值 如export default {name: 'PlButton'} 那么elName就是pl-button
* 2. 除了容器的对象plContainer属性,(注意:看容器的属性请看下面的容器基本结构)其他配置表属性的介绍如下
* title: 组件提示文字(左边组件按钮区域用到了)
* icon: 组件图标(左边组件按钮区域用到了,使用的是 Iconfont-阿里巴巴矢量图标库)
* 以下全是组件本身的属性,不是左边组件按钮区域列表的属性
* elName: 组件名
* pointList: 控制组件拖动的方向(拖动的小圆点) pointList: ['lt' 左上, 'rt' 右上, 'lb' 左下, 'rb' 右下, 'l' 左, 'r' 右, 't' 上, 'b' 下],
* // ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b' ]
* value: '' // 输入框的值,主要用在这个画板元素上的输入框类型组件上
* contenteditable: 组件输入状态是否可以被拖动
* placeholder: 输入框类型的组件,空文本提示文字
* commonStyle:初始化的样式,就是css不多介绍
* options:{ // 组件配置项
* classList: [], 当前组件的类集合
lineHeightChange: true // 表示行高需要随着拖动的高度变化(只有可以拖动的元素有效)
* }
* module: boolean 为true代表当前组件不是个画板元素,而是作为一个模块的身份。(但是它依然存放在容器中) 什么是非画板元素,就是不能再自由容器中拖动和自由组合,非画板元素是模块组件
* containerOptions: {} 如果我配置了module为true,代表当前是个模块,模块身份可以去配置容器对象的属性
* propsValue: {} // 里面包含了组件所有的data对象属性,它不需要再基本结构中配置,他会在生成组件的时候会放到该配置中来
*/
import {pageWh, defaultStyle, moduleContainer} from './config'
// 容器的基本结构
export const plContainer = {
elName: 'pl-container',
title: '自由容器',
icon: 'iconfont iconrongqi',
pointList: ['b'], // 模块拖动的方向有哪些
// 容器最外层盒子的样式
containerStyle: { // 容器大盒子的样式
marginBottom: 10
},
allowed: true, // 代表我当前容器是个画板,拖动画板元素可以放到容器上面
showTitle: true, // 是否显示头部
// 容器头部的样式
titleStyle: {
height: 50,
lineHeight: 50
},
titleBarName: '标题栏',
// 容器画板的默认样式
commonStyle: {
width: pageWh.width,
height: 250,
position: 'relative',
minHeight: 50, // 容器里面的画板最小高度值
backgroundColor: '#fff'
},
childNode: [] // 容器子节点的集装箱
}
// 基础组件
const BasicComponents = [
{
title: '基础组件',
components: [
plContainer,
{
elName: 'pl-text',
title: '文本',
icon: 'iconfont iconwenbenyu',
pointList: [], // 控制组件拖动的方向
contenteditable: false,
placeholder: '点击输入内容',
commonStyle: {
...defaultStyle,
padding: 8,
fontSize: 15,
lineHeight: 17,
height: 'auto',
textAlign: 'left',
minWidth: 35,
width: 160
}
},
{
elName: 'pl-button',
title: '按钮',
icon: 'iconfont iconanniu',
pointList: ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b'], // 控制组件拖动的方向
contenteditable: false,
options: {
classList: [],
lineHeightChange: true // 表示行高需要随着拖动的高度变化
},
commonStyle: {
...defaultStyle,
fontSize: 15,
lineHeight: 36,
height: 36,
textAlign: 'center',
minWidth: 35,
minHeight: 36,
width: 80
}
},
{
elName: 'cube-nav',
title: '魔方导航',
icon: 'iconfont iconfenlei',
module: true,
containerOptions: {
...moduleContainer,
titleBarName: '魔方导航模块'
},
options: {
classList: []
}
},
{
elName: 'carousel',
title: '多图文轮播',
icon: 'iconfont iconlunbotu',
module: true,
containerOptions: {
...moduleContainer,
titleBarName: '多图文轮播'
},
options: {
classList: []
}
}
]
}
]
const components = [...BasicComponents]
// 遍历判断找出画板元素的组件
// 在拖拽元素到画板的时候,会判断当前拖动的组件是否在这里面存在,存在才可以添加组件到画板容器
// 必须是画板组件
export const drawingComponent = components.map(item => item.components.map(con => {
if (!con.module && con.elName !== 'pl-container') return con.elName
}))[0].filter(item => item)
export default components
页面整体结构

核心代码
编辑器核心代码,基于 Vue 动态组件特性实现:

// 获取需要绘画的节点数据(整个可视化编辑器的最重要的东西)
export const getNodeElement = (nodeData, type) => {
// 如果不存在该组件就直接返回
if (!nodeData || !componentsName.includes(camelCase(nodeData.elName).toLowerCase())) {
Message.error({message: '没有该模块!', type: 'warning', duration: 2000})
return null
}
// 需要添加的节点元素对象
let nodeElement
// 获取当前组件的data数据(非常重要,它将是你原始组件的初始化数据,你右边的属性控制就是去更改的它)
let props = getComponentProps(nodeData.elName)
// 获取需要添加的节点元素的数据结构
nodeElement = deepClone(getElementConfig({...nodeData, needProps: props}))
// 注意注意注意: 如果我进来的不是容器,那么就需要包装一层容器,在返回节点
// type如果存在,代表我是往容器里面加节点不需要被容器包裹,就不需要执行if语句了
if (nodeElement.elName !== 'pl-container' && type !== '我是往容器里面加节点不需要被容器包裹') {
// 获取pl-container容器组件的data数据
let props = getComponentProps('pl-container')
// 获取容器的基本结构
let containerNodeData = getElementConfig({...plContainer, needProps: props})
// 什么是非画板元素,就是不能再自由容器中拖动和自由组合,非画板元素是模块组件
// 下面if语句是做非画板元素的关键,意思就是非画板元素,它也属于自由容器中,但是它不能拖动
// 如果当前组件是一个模块, 就需要执行下面的语句
if (nodeElement.module) {
// 如果是模块,那么就去看是否改变了容器的样式,没有改变默认给个改变容器的基本值
let cops = judgeObject(nodeElement.containerOptions) ? nodeElement.containerOptions : moduleContainer
// 合并容器的属性(很好理解就是去覆盖掉原来容器的属性,因为原来容器的属性是为了画板而生的,但是模块本身也是被容器包裹的,所以需要去覆盖容器的配置)
let newContainer = {...containerNodeData, ...cops}
// 删除当前需要添加的节点,里面的配置容器对象
delete nodeElement.containerOptions
// 然后再把需要添加的节点放入容器中
newContainer.childNode.push(nodeElement)
return deepClone(newContainer)
}
// 把需要添加的元素放入到容器节点中
containerNodeData.childNode.push(nodeElement)
// 导出容器
return deepClone(containerNodeData)
}
// 返回当前组件
return nodeElement
}
组件库
编写组件,考虑的是组件库,所以我们竟可能让我们的组件支持全局引入和按需引入,如果全局引入,那么所有的组件需要要注册到Vue component 上,并导出:
/**
* 组件库入口
* */
// 基础组件
import plEditDiv from './editDiv' // 必须放第一个位置引入 因为下面的组件有用到它
import plText from './text'
import plButton from './Button'
import plContainer from './container'
import cubeNav from './cubeNav'
import carousel from './carousel'
// 所有组件列表
const components = [
plEditDiv,
plText,
plButton,
plContainer,
cubeNav,
carousel
]
let plRegisterComponentsObject = {}
let componentsName = []
components.forEach(item => {
plRegisterComponentsObject[item.name] = item
// 导出当前组件的组件名
if (item.name && typeof item.name === 'string') {
componentsName.push(item.name.toLowerCase())
}
})
// 定义 install 方法,接收 Vue 作为参数
const install = function (Vue) {
// 判断是否安装,安装过就不继续往下执行
if (install.installed) return
install.installed = true
// 遍历注册所有组件
components.map(component => Vue.component(component.name, component))
}
export {
componentsName,
plEditDiv,
cubeNav,
plButton,
carousel,
plText,
plContainer,
plRegisterComponentsObject
}
export default {
install
}
启动运行
npm run dev
Vue + element从零打造一个H5页面可视化编辑器——pl-drag-template的更多相关文章
- 编写第一个H5页面
<!DOCTYPE html><html ><head> <meta charset="UTF-8"> <title>第 ...
- 【前端vue进阶实战】:从零打造一个流程图、拓扑图项目【Nuxt.js + Element + Vuex】 (一)
本系列教程是用Vue.js + Nuxt.js + Element + Vuex + 开源js绘图库,打造一个属于自己的在线绘图软件,最终效果:topology.le5le.com .如果你觉得好,欢 ...
- 【前端新手也能做大项目】:跟我一起,从零打造一个属于自己的在线Visio项目实战【ReactJS + UmiJS + DvaJS】(二)
本系列教程是教大家如何根据开源js绘图库,打造一个属于自己的在线绘图软件.当然,也可以看着是这个绘图库的开发教程.如果你觉得好,欢迎点个赞,让我们更有动力去做好! 本系列教程重点介绍如何开发自己的绘图 ...
- 从零打造一个Web地图引擎
说到地图,大家一定很熟悉,平时应该都使用过百度地图.高德地图.腾讯地图等,如果涉及到地图相关的开发需求,也有很多选择,比如前面的几个地图都会提供一套js API,此外也有一些开源地图框架可以使用,比如 ...
- Vue+Koa+MongoDB从零打造一个任务管理系统
大概是在18年的时候,当时还没有疫情.当时工作中同时负责多个项目,有 PC 端运营管理后台的,有移动端 M 站的,有微信小程序的,每天 git 分支切到头昏眼花,每个需求提测需要发送邮件,而且周五要写 ...
- Netty+MUI从零打造一个仿微信的高性能聊天项目,兼容iPhone/iPad/安卓
要说到微信,我相信是个人都应该知道,几乎人人都会安装这款社交APP吧,它已经成为了我们生活中不可缺少的一份子. 我记得我上大学那会刚接触Java,做的第一个小项目就是基于J2SE的聊天室,使用Java ...
- vue+element 给表格添加数据,页面不实时刷新的问题
由于页面加载时,使用了keep-alive,keep-alive具有数据缓存作用,当在添加页面添加成功时,返回主页面没有立即更新.数据有缓存. 解决办法如下: 将获取数据列表的方法放到activate ...
- 记录一个h5页面生成canvas画布做签名的js插件--signature_pad
demo地址:https://jsfiddle.net/02dLn15g/5/ GitHub地址:https://github.com/szimek/signature_pad 配置项: dotSiz ...
- 安卓app中嵌入一个H5页面,当手机系统设置字体变大时,如何使H5页面的字体不会随用户自己调整的系统字体变化而变化?
webview.getSettings().setTextZoom(100);WebView加上这个设置后,WebView里的字体就不会随系统字体大小设置发生变化了. https://segmentf ...
随机推荐
- Luogu_2434_[SDOI2005]区间
题目描述 现给定n个闭区间[ai, bi],1<=i<=n.这些区间的并可以表示为一些不相交的闭区间的并.你的任务就是在这些表示方式中找出包含最少区间的方案.你的输出应该按照区间的升序排列 ...
- github 下载部分代码
作者:知乎用户链接:https://www.zhihu.com/question/25369412/answer/96174755来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...
- jq ajaxPrefilter 防止重复提交ajax
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Promethues配置
# my global config global: scrape_interval: 10s # Set the scrape interval to every 15 seconds. Defau ...
- (为容器分配独立IP方法二)通过虚拟IP实现docker宿主机增加对外IP接口
虚拟IP.何为虚拟IP,就是一个未分配给真实主机的IP,也就是说对外提供数据库服务器的主机除了有一个真实IP外还有一个虚IP,使用这两个IP中的任意一个都可以连接到这台主机,所有项目中数据库链接一项配 ...
- 【Android TimeCat】 解决cannot resolve symbol R
莫名其妙出现了,鬼知道怎么来的. 解决方法总结 1. 推荐 解决90%的情况: Build->Clean ProjectBuild->Rebuild Project 2. 不常见 Andr ...
- C++中cin的输入分隔符问题及相关
1.C/C++中的类型转换函数(区分类中的类型转换构造函数): 头文件:C中stdlib.h C++中cstdlib atof(将字符串转换成浮点型数) atoi(将字符串转换成整型数) atol(将 ...
- 基于activity的强大java工作流引擎,可视化开发工作流
我们先来看看工作流引擎和Activity? 工作流引擎 所谓工作流引擎是指workflow作为应用系统的一部分,并为之提供对各应用系统有决定作用的根据角色.分工和条件的不同决定信息传递路由.内容等级等 ...
- Django报Warning错误 RuntimeWarning: DateTimeField Goods.create_at received a naive datetime (2019-07-31 23:05:58) while time zone support is active
报错和UTC(世界标准时间)有关,在settings.py 文件中设置 USE_TZ = False 警告错误不再报
- Python中max()内置函数使用(list)
在学习完列表和元组的基础知识后,做到一个题: 求出列表中频次出现最多的元素. 学习到了python内置函数max的用法 其参数key的用法 匿名函数lamda的用法 python内置函数max() m ...