背景
之前参与开发一个小程序商城,涉及非常多的落地页面需要前端开发维护,又由于小程序包大小限制,不能完成用小程序代码一一实现,所以最后决定把落地页的需求拆成一个单独的产品-落地页管理系统,通过可视化的界面编辑维护小程序和H5的落地页面,落地页面通过获取后台数据动态生成页面内容。
对于运营同事来说,在没有前端同事配合的情况下,也可通过可视化的操作来搭建所需的落地页面。解决页面生成的效率问题,同时释放开发人员。
落地页管理系统的需求
- 页面的可维护(新建、编辑、删除、状态管理等)。
- 页面的常见功能或属性编辑(标题,背景、有效期、微信自定义分享、返回顶部功能等)。
- 组成页面的元素组件化,可自由拖拽组装,编辑修改组件属性。
- 页面结构数据化,客户端根据数据动态渲染落地页面。
功能需求的技术实现
技术栈
后端技术栈 node+Koa+mongodb,提供落地页的更删改查API服务、素材的上传更新及有效期定时检测等。
前端技术栈 Vue+Vuex+Vue-route+EmenetUI+Axios,开发实现组成页面所需的基础组件及业务营销组件。
后端只是简单存储记录,本篇主要分享可视化页面编辑和渲染的前端技术实现。
项目目录说明
src
|-- App.vue
|-- main.js // 项目入口
|-- components // 公共组件目录
| |-- BreadCrumb // 面包屑导航组件
| | |-- breadcrumb.scss
| | |-- index.vue
| |-- Carousel // 轮播图组件
| | |-- carousel.scss
| | |-- index.vue
| |-- CarouselSet // 轮播图属性编辑组件
| | |-- index.vue
| |-- ContextMenu // 右键菜单组件
| | |-- contextmenu.scss
| | |-- index.vue
| |-- Event // 事件组件
| | |-- event.scss
| | |-- index.vue
| |-- EventSet // 事件属性编辑组件
| | |-- index.vue
| |-- Goods // 商品组件
| | |-- goods.scss
| | |-- index.vue
| |-- GoodsSet // 商品属性编辑组件
| | |-- index.vue
| |-- HeadInfo // 头部显示组件
| | |-- headinfo.scss
| | |-- index.vue
| |-- Picture // 图片组件
| | |-- index.vue
| | |-- picture.scss
| |-- PictureSet // 图片属性编辑组件
| | |-- index.vue
| |-- Video // 视频组件
| | |-- index.vue
| | |-- video.scss
| |-- VideoSet // 视频属性编辑组件
| |-- index.vue
|-- pages // 页面目录
| |-- LandPage // 落地页渲染页面
| | |-- index.vue
| | |-- landpage.scss
| |-- Login // 登陆页面
| | |-- index.vue
| | |-- login.scss
| |-- PageCreate // 落地页创建页面
| | |-- index.vue
| | |-- pagecreate.scss
| | |-- components // 创建页面分成左侧组件列表、中间窗口渲染组件、右侧属性编辑组件
| | |-- pageCenter // 中间窗口渲染组件
| | | |-- index.vue
| | | |-- pagecenter.scss
| | |-- pageRight // 右侧属性编辑组件
| | |-- index.vue
| | |-- pageright.scss
| |-- PageEditor // 落地页编辑页面,共享创建页面,根据路由判断实现不同业务需求
| | |-- index.vue
| | |-- pageeditor.scss
| |-- PageList // 落地页列表页面(新建、编辑、删除、状态管理等)
| | |-- index.vue
| | |-- pagelist.scss
| |-- PagePreview // 落地页预览页面
| |-- index.vue
| |-- pagepreview.scss
|-- assets // 资源目录(样式文件)
| |-- style
| |-- animate.css
| |-- common.scss
| |-- normalize.css
| |-- var.scss
|-- http // http请求的接口与方法
| |-- index.js
| |-- request.js
|-- router // 页面路由目录
| |-- index.js
| |-- permission.js
|-- store // vuex状态管理目录
| |-- actions.js
| |-- index.js
| |-- mutations.js
| |-- state.js
|-- utility // 常用工具类函数与静态配置文件目录
|-- const.js
|-- filter.js
|-- index.js
|-- landpage.js
页面元素组件化和数据化
对落地页中的布局和功能需求进行分析总结并抽象出大概元素组件,分为基础组件(图片、视频、轮播图)和营销组件(事件、商品)。
落地页面的数据结构如下:
page:{
//页面名称
name:'',
//页面标题
title:'',
//页面唯一的key
urlKey:'',
//是否需要有效期
needExpire:false,
expireDate:'',
//额外功能字段
extra:{
//页面背景色
backgroundColor:'',
//是否需要返回顶部按钮
needBackTop:false,
backTopImage:'',
//是否需要自定义分享
needShare:false,
shareText:'',
shareImage:'',
},
//页面中的组件
components:[
{ type:'Picture', name:'图片' }
]
}
落地页的创建页面大致如下,分为左侧元素组件列表、中间实时渲染窗口和右侧组件属性编辑区域。
页面中可以有多个元素组件,然后每个元素组件都有自己的特有属性,每个元素组件对应一个属性组件用来编辑该组件的特有属性,在公共组件目录中可以看到对应的Set组件。
从组件的拖拽到属性编辑与实时渲染
从左侧拖拽组件到中间窗口即为页面中添加一个元素组件,新组件为当前激活组件,右侧区域会根据当前激活组件动态加载对应的属性编辑组件。
当拖拽左侧元素组件时,使用@dragstart事件记录当前拖拽的元素,中间窗口使用@dragover计算正在拖拽元素的位置坐标实现拖拽排序和层级元素的判断,@drop事件中根据之前的记录和判断生成元素组件数据到page.components或元素组件的children层级数组中。
<!-- 中间窗口的渲染模版代码 -->
<div class="page-wraper" @dragover="dragover" @drop="drop" :style="style">
<div
v-for="(component) in components"
:key="component.id"
:class="`component-wraper`">
<!-- 动态组件加载 -->
<component :class="`${component.id==activeComponent.id?'active':''} ${relativeComponent&&relativeComponent.id==component.id?('hover-'+relativeType):''}`"
v-bind:is="component.type"
v-bind="component"
@contextmenu.native.prevent="contextmenu($event)"
@mousedown.native="setActiveComponent(component)"
@dragover.native.stop="checkDragOver(component,$event)"></component>
<!-- 动态层级子组件加载 -->
<component v-for="child in component.children"
:key="child.id"
:class="`component-child ${child.id==activeComponent.id?'active':''}`"
v-bind:is="child.type"
v-bind="child"
@contextmenu.native.prevent="contextmenu($event)"
@mousedown.native="setActiveComponent(child)"
@dragover.native.stop="checkDragOver(component, $event, child)"></component>
</div>
</div>
左侧组件到中间窗口组件及右侧属性编辑组件的通信使用vuex的commit触发响应。当编辑右侧属性组件时会触发修改vuex的state,中间窗口组件通过mapState映射当前编辑组件的实时数据,vue会自动响应中间窗口中元素组件的渲染更新。
动态渲染落地页面
请求接口获取当前落地页面数据,判断不同属性值添加不同功能,渲染页面的代码类似之前中间窗口的渲染模版代码,没有对应的事件处理。
<div class="page-wraper" :style="style">
<div class="component-wraper" v-for="(component) in components" :key="component.id">
<!-- 动态组件加载 -->
<component v-bind:is="component.type" v-bind="component"> </component>
<!-- 动态层级子组件加载 -->
<component v-for="child in component.children" class="component-child"
:key="child.id"
v-bind:is="child.type"
v-bind="child"></component>
</div>
<transition name="el-fade-in">
<div class="backTop" v-if="scrollTop&&page&&page.extra.needBackTop" @click="backTop">
<img :src="staticURL+page.extra.backTopImage" />
</div>
</transition>
</div>
vue内置组件component通过v-bind:is动态加载所需的元素组件,并传递组件数据。
元素组件内部获取组件属性并渲染组件,组件会根据环境的不同响应事件,例如:在中间渲染窗口中假如给图片组件添加链接属性,点击并没有响应,但是在落地页面中加载图片组件点击则会响应该事件。
待完善的功能
- 逐步实现事件组件、商品组件等。
- 小程序端的渲染实现(数据结构相同,需使用小程序语法实现),目前只实现H5端。
最后
篇幅有限,细节性问题就不在此描述啦,代码已开源,感兴趣的朋友欢迎 猛戳围观地址。
此次分享属个人拙见,如有不足之处欢迎指正,谢谢。
讨论是绝佳的反思,被别人指出问题正是改进的空间。