huangwei9527 / my-blog Goto Github PK
View Code? Open in Web Editor NEW个人博客网站
个人博客网站
想必你一定使用过易企秀或百度H5等微场景生成工具制作过炫酷的h5页面,除了感叹其神奇之处有没有想过其实现方式呢?本文从零开始实现一个H5编辑器项目完整设计思路和主要实现步骤,并开源前后端代码。有需要的小伙伴可以按照该教程从零实现自己的H5编辑器。(实现起来并不复杂,该教程只是提供思路,并非最佳实践)
编辑器预览:
前端:
vue
: 模块化开发少不了angular,react,vue三选一,这里选择了vue。
vuex
: 状态管理
sass
: css预编译器。
element-ui
:不造轮子,有现成的优秀的vue组件库当然要用起来。没有的自己再封装一些就可以了。
loadsh
:工具类
服务端:
koa
:后端语言采用nodejs,koa文档和学习资料也比较多,express原班人马打造,这个正合适。
mongodb
:一个基于分布式文件存储的数据库,比较灵活。
1、了解vue技术栈开发
2、了解koa
3、了解mongodb
基于vue-cli3环境搭建
···
·
|-- client // 原 src 目录,改成 client 用作前端项目目录
|-- server // 新增 server 用于服务端项目目录
|-- engine-template // 新增 engine-template 用于页面模板库目录
|-- docs // 新增 docs 预留编写项目文档目录
·
···
这样的话 我们需要再把我们webpack配置文件稍作一下调整,首先是把原先的编译指向src的目录改成client,其次为了 npm run build 能正常编译 client 我们也需要为 babel-loader 再增加一个编译目录:
根目录新增vue.config.js,目的是为了改造项目入口,改为:client/main.js
module.exports = {
pages: {
index: {
entry: "client/main.js"
}
}
}
babel-loader能正常编译 client, engine-template目录, 在vue.config.js新增如下配置
// 扩展 webpack 配置
chainWebpack: config => {
config.module
.rule('js')
.include.add(/engine-template/).end()
.include.add(/client/).end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
这样我们搭建起来一个简易的项目目录结构。
|-- client --------前端项目界面代码
|--common --------前端界面对应静态资源
|--components --------组件
|--config --------配置文件
|--eventBus --------eventBus
|--filter --------过滤器
|--mixins --------混入
|--pages --------页面
|--router --------路由配置
|--store --------vuex状态管理
|--service --------axios封装
|--App.vue --------App
|--main.js --------入口文件
|--permission.js --------权限控制
|-- server --------服务器端项目代码
|--confog --------数据库链接相关
|--middleware --------中间件
|--models --------Schema和Model
|--routes --------路由
|--views --------ejs页面模板
|--public --------静态资源
|--utils --------工具方法
|--app.js --------服务端入口
|-- common --------前后端公用代码模块(如加解密)
|-- engine-template --------页面模板引擎,使用webpack打包成js提供页面引用
|-- docs --------预留编写项目文档目录
|-- config.json --------配置文件
编辑器的实现思路是:编辑器生成页面JSON数据,服务端负责存取JSON数据,渲染时从服务端取数据JSON交给前端模板处理。
确认了实现逻辑,数据结构也是非常重要的,把一个页面定义成一个JSON数据,数据结构大致是这样的:
页面工程数据接口
{
title: '', // 标题
description: '', //描述
coverImage: '', // 封面
auther: '', // 作者
script: '', // 页面插入脚本
width: 375, // 高
height: 644, // 宽
pages: [], // 多页页面
shareConfig: {}, // 微信分享配置
pageMode: 0, // 渲染模式,用于扩展多种模式渲染,翻页h5/长页/PC页面等等
}
多页页面pages其中一页数据结构:
{
name: '',
elements: [], // 页面元素
commonStyle: {
backgroundColor: '',
backgroundImage: '',
backgroundSize: 'cover'
},
config: {}
}
元素数据结构:
{
elName: '', // 组件名
animations: [], // 图层的动画,可以支持多个动画
commonStyle: {}, // 公共样式,默认样式
events: [], // 事件配置数据,每个图层可以添加多个事件
propsValue: {}, // 属性参数
value: '', // 绑定值
valueType: 'String', // 值类型
isForm: false // 是否是表单控件,用于表单提交时获取表单数据
}
用户在左侧组件区域选择组件添加到页面上,编辑区域通过动态组件特性渲染出每个元素组件。
最后,点击保存将页面数据提交到数据库。至于数据怎么转成静态 HTML方法有很多。还有页面数据我们全部都有,我们可以做页面的预渲染,骨架屏,ssr,编译时优化等等。而且我们也可以对产出的活动页做数据分析~有很多想象的空间。
编辑器核心代码,基于 Vue 动态组件特性实现:
为大家附上 Vue 官方文档:cn.vuejs.org/v2/api/#is
编辑画板只需要循环遍历pages[i].elements数组,将里面的元素组件JSON数据取出,通过动态组件渲染出各个组件,支持拖拽改变位置尺寸.
在client目录新建plugins来管理组件库。也可以将该组件库发到npm上工程中通过npm管理
编写组件,考虑的是组件库,所以我们竟可能让我们的组件支持全局引入和按需引入,如果全局引入,那么所有的组件需要要注册到Vue component 上,并导出:
client/plugins下新建index.js入口文件
```
/**
* 组件库入口
* */
import Text from './text'
// 所有组件列表
const components = [
Text
]
// 定义 install 方法,接收 Vue 作为参数
const install = function (Vue) {
// 判断是否安装,安装过就不继续往下执行
if (install.installed) return
install.installed = true
// 遍历注册所有组件
components.map(component => Vue.component(component.name, component))
}
// 检测到 Vue 才执行,毕竟我们是基于 Vue 的
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
install,
// 所有组件,必须具有 install,才能使用 Vue.use()
Text
}
```
示例: text文本组件
client/plugins下新建text组件目录
|-- text --------text组件
|--src --------资源
|--index.vue --------组件
|--index.js --------入口
text/index.js
// 为组件提供 install 方法,供组件对外按需引入
import Component from './src/index'
Component.install = Vue => {
Vue.component(Component.name, Component)
}
export default Component
text/src/index.vue
<!--text.vue-->
<template>
<div class="qk-text">
{{text}}
</div>
</template>
<script>
export default {
name: 'QkText', // 这个名字很重要,它就是未来的标签名<qk-text></qk-text>
props: {
text: {
type: String,
default: '这是一段文字'
}
}
}
</script>
<style lang="scss" scoped>
</style>
编辑器里使用组件库:
// 引入组件库
import QKUI from 'client/plugins/index'
// 注册组件库
Vue.use(QKUI)
// 使用:
<qk-text text="这是一段文字"></qk-text>
按照这个组件开发方式我们可以扩展任意多的组件,来丰富组件库
需要注意的是这里的组件最外层宽高都要求是100%
Quark-h5编辑器左侧选择组件区域可以通过一个配置文件定义可选组件
新建一个ele-config.js配置文件:
export default [
{
title: '基础组件',
components: [
{
elName: 'qk-text', // 组件名,与组件库名称一致
title: '文字',
icon: 'iconfont iconwenben',
// 给每个组件配置默认显示样式
defaultStyle: {
height: 40
}
}
]
},
{
title: '表单组件',
components: []
},
{
title: '功能组件',
components: []
},
{
title: '业务组件',
components: []
}
]
公共方法中提供一个function 通过组件名和默认样式获取元素组件JSON,getElementConfigJson(elName, defaultStyle)方法
公共样式属性编辑比较简单就是对元素JSON对象commonStyles字段进行编辑操作
1.为组件的每一个prop属性开发一个属性编辑组件. 例如:QkText组件需要text属性,新增一个attr-qk-text组件来操作该属性
2.获取组件prop对象
3.遍历prop对象key, 通过key判断显示哪些属性编辑组件
动画效果引入Animate.css动画库。元素组件动画,可以支持多个动画。数据存在元素JSON对象animations数组里。
监听mouseover和mouseleave,当鼠标移入时将动画className添加入到元素上,鼠标移出时去掉动画lassName。这样就实现了hover预览动画
组件编辑时支持动画预览和单个动画预览。
封装一个动画执行方法
/**
* 动画方法, 将动画css加入到元素上,返回promise提供执行后续操作(将动画重置)
* @param $el 当前被执行动画的元素
* @param animationList 动画列表
* @param isDebugger 动画列表
* @returns {Promise<void>}
*/
export default async function runAnimation($el, animationList = [], isDebug , callback){
let playFn = function (animation) {
return new Promise(resolve => {
$el.style.animationName = animation.type
$el.style.animationDuration = `${animation.duration}s`
// 如果是循环播放就将循环次数置为1,这样有效避免编辑时因为预览循环播放组件播放动画无法触发animationend来暂停组件动画
$el.style.animationIterationCount = animation.infinite ? (isDebug ? 1 : 'infinite') : animation.interationCount
$el.style.animationDelay = `${animation.delay}s`
$el.style.animationFillMode = 'both'
let resolveFn = function(){
$el.removeEventListener('animationend', resolveFn, false);
$el.addEventListener('animationcancel', resolveFn, false);
resolve()
}
$el.addEventListener('animationend', resolveFn, false)
$el.addEventListener('animationcancel', resolveFn, false);
})
}
for(let i = 0, len = animationList.length; i < len; i++){
await playFn(animationList[i])
}
if(callback){
callback()
}
}
animationIterationCount 如果是编辑模式的化动画只执行一次,不然无法监听到动画结束animationend事件
执行动画前先将元素样式style缓存起来,当动画执行完再将原样式赋值给元素
let cssText = this.$el.style.cssText;
runAnimations(this.$el, animations, true, () => {
this.$el.style.cssText = cssText
})
提供事件mixins混入到组件,每个事件方法返回promise,元素被点击时按顺序执行事件方法
参考百度H5,将脚本以script标签形式嵌入。页面加载后执行。
这里也可以考虑mixins方式混入到页面或者组件,可根据业务需求自行扩展,都是可以实现的。
将psd每个设计图中的每个图层导出成图片保存到静态资源服务器中,
服务端安装psd依赖
cnpm install psd --save
加入psd.js依赖,并且提供接口来处理数据
var PSD = require('psd');
router.post('/psdPpload',async ctx=>{
const file = ctx.request.files.file; // 获取上传文件
let psd = await PSD.open(file.path)
var timeStr = + new Date();
let descendantsList = psd.tree().descendants();
descendantsList.reverse();
let psdSourceList = []
let currentPathDir = `public/upload_static/psd_image/${timeStr}`
for (var i = 0; i < descendantsList.length; i++){
if (descendantsList[i].isGroup()) continue;
if (!descendantsList[i].visible) continue;
try{
await descendantsList[i].saveAsPng(path.join(ctx.state.SERVER_PATH, currentPathDir + `/${i}.png`))
psdSourceList.push({
...descendantsList[i].export(),
type: 'picture',
imageSrc: ctx.state.BASE_URL + `/upload_static/psd_image/${timeStr}/${i}.png`,
})
}catch (e) {
// 转换不出来的图层先忽略
continue;
}
}
ctx.body = {
elements: psdSourceList,
document: psd.tree().export().document
};
})
最后把获取的数据转义并返回给前端,前端获取到数据后使用系统统一方法,遍历添加统一图片组件
这里只需要注意下图片跨域问题,官方提供html2canvas: proxy解决方案。它将图片转化为base64格式,结合使用设置(proxy: theProxyURL), 绘制到跨域图片时,会去访问theProxyURL下转化好格式的图片,由此解决了画布污染问题。
提供一个跨域接口
/**
* html2canvas 跨域接口设置
*/
router.get('/html2canvas/corsproxy', async ctx => {
ctx.body = await request(ctx.query.url)
})
在engine-template目录下新建swiper-h5-engine页面组件,这个组件接收到页面JSON数据就可以把页面渲染出来。跟编辑预览画板实现逻辑差不多。
然后使用vue-cli库打包命令将组件打包成engine.js库文件。ejs模板引入该页面组件配合json数据渲染出页面
提供两种方案解决屏幕适配
1、等比例缩放
在将json元素转换为dom元素的时候,对所有的px单位做比例转换,转换公式为 new = old * windows.x / pageJson.width,这里的pageJson.width是页面的一个初始值,也是编辑时候的默认宽度,同时viewport使用device-width。
2.全屏背景, 页面垂直居中
因为会存在上下或者左右有间隙的情况,这时候我们把背景颜色做全屏处理
页面垂直居中只适用于全屏h5, 以后扩展长页和PC页就不需要垂直居中处理。
package.json中新增打包命令
"lib:h5-swiper": "vue-cli-service build --target lib --name h5-swiper --dest server/public/engine_libs/h5-swiper engine-template/engine-h5-swiper/index.js"
执行npm run lib:h5-swiper 生成引擎模板js如图
ejs中引入模板
<script src="/third-libs/swiper.min.js"></script>
使用组件
<engine-h5-swiper :pageData="pageData" />
工程目录上文已给出,也可以使用 koa-generator 脚手架工具生成
app.js
//配置ejs-template 模板引擎
render(app, {
root: path.join(__dirname, 'views'),
layout: false,
viewExt: 'html',
cache: false,
debug: false
});
因为html2canvas需要图片允许跨域,所以在静态资源服务中所有资源请求设置'Access-Control-Allow-Origin':'*'
app.js
//配置静态web
app.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){
res.header( 'Access-Control-Allow-Origin', '*')
}});
app.js
const fs = require('fs')
fs.readdirSync(path.join(__dirname,'./routes')).forEach(route=> {
let api = require(`./routes/${route}`)
app.use(api.routes(), api.allowedMethods())
})
app.js
const jwt = require('koa-jwt')
app.use(jwt({ secret: 'yourstr' }).unless({
path: [
/^\/$/, /\/token/, /\/wechat/,
{ url: /\/papers/, methods: ['GET'] }
]
}));
middleware/formatresponse.js
module.exports = async (ctx, next) => {
await next().then(() => {
if (ctx.status === 200) {
ctx.body = {
message: '成功',
code: 200,
body: ctx.body,
status: true
}
} else if (ctx.status === 201) { // 201处理模板引擎渲染
} else {
ctx.body = {
message: ctx.body || '接口异常,请重试',
code: ctx.status,
body: '接口请求失败',
status: false
}
}
}).catch((err) => {
if (err.status === 401) {
ctx.status = 401;
ctx.body = {
code: 401,
status: false,
message: '登录过期,请重新登录'
}
} else {
throw err
}
})
}
当接口发布到线上,前端通过ajax请求时,会报跨域的错误。koa2使用koa2-cors这个库非常方便的实现了跨域配置,使用起来也很简单
const cors = require('koa2-cors');
app.use(cors());
我们使用mongodb数据库,在koa2中使用mongoose这个库来管理整个数据库的操作。
根目录下新建config文件夹,新建mongo.js
// config/mongo.js
const mongoose = require('mongoose').set('debug', true);
const options = {
autoReconnect: true
}
// username 数据库用户名
// password 数据库密码
// localhost 数据库ip
// dbname 数据库名称
const url = 'mongodb://username:password@localhost:27017/dbname'
module.exports = {
connect: ()=> {
mongoose.connect(url,options)
let db = mongoose.connection
db.on('error', console.error.bind(console, '连接错误:'));
db.once('open', ()=> {
console.log('mongodb connect suucess');
})
}
}
把mongodb配置信息放到config.json中统一管理
const mongoConf = require('./config/mongo');
mongoConf.connect();
...
服务端具体接口实现就不详细介绍了,就是对页面的增删改查,和用户的登录注册难度不大
npm run dev-client
npm run dev-server
注意:
如果没有生成过引擎模板js文件的,需要先编辑引擎模板,否则预览页面加载页面引擎.js 404报错
npm run lib:h5-swiper
相关文章推荐:
基于Koa2打造属于自己的MVC框架,仿egg的简易版本
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.