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
上一篇: PHP如何使用array_unshift()在数组开头插入元素
下一篇: C++ 字符串输入