Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
想必你一定使用過(guò)易企秀或百度H5等微場(chǎng)景生成工具制作過(guò)炫酷的h5頁(yè)面,除了感嘆其神奇之處有沒(méi)有想過(guò)其實(shí)現(xiàn)方式呢?本文從零開(kāi)始實(shí)現(xiàn)一個(gè)H5編輯器項(xiàng)目完整設(shè)計(jì)思路和主要實(shí)現(xiàn)步驟,并開(kāi)源前后端代碼。有需要的小伙伴可以按照該教程從零實(shí)現(xiàn)自己的H5編輯器。(實(shí)現(xiàn)起來(lái)并不復(fù)雜,該教程只是提供思路,并非最佳實(shí)踐)
Github: https://github.com/huangwei9527/quark-h5
演示地址:http://47.104.247.183:4000/
演示帳號(hào)密碼均admin
編輯器預(yù)覽:
前端: vue: 模塊化開(kāi)發(fā)少不了angular,react,vue三選一,這里選擇了vue。 vuex: 狀態(tài)管理 sass: css預(yù)編譯器。 element-ui:不造輪子,有現(xiàn)成的優(yōu)秀的vue組件庫(kù)當(dāng)然要用起來(lái)。沒(méi)有的自己再封裝一些就可以了。 loadsh:工具類
服務(wù)端: koa:后端語(yǔ)言采用nodejs,koa文檔和學(xué)習(xí)資料也比較多,express原班人馬打造,這個(gè)正合適。 mongodb:一個(gè)基于分布式文件存儲(chǔ)的數(shù)據(jù)庫(kù),比較靈活。
1、了解vue技術(shù)棧開(kāi)發(fā) 2、了解koa 3、了解mongodb
基于vue-cli3環(huán)境搭建
···
·
|-- client // 原 src 目錄,改成 client 用作前端項(xiàng)目目錄
|-- server // 新增 server 用于服務(wù)端項(xiàng)目目錄
|-- engine-template // 新增 engine-template 用于頁(yè)面模板庫(kù)目錄
|-- docs // 新增 docs 預(yù)留編寫項(xiàng)目文檔目錄
·
···
復(fù)制代碼
這樣我們搭建起來(lái)一個(gè)簡(jiǎn)易的項(xiàng)目目錄結(jié)構(gòu)。
|-- client --------前端項(xiàng)目界面代碼
|--common --------前端界面對(duì)應(yīng)靜態(tài)資源
|--components --------組件
|--config --------配置文件
|--eventBus --------eventBus
|--filter --------過(guò)濾器
|--mixins --------混入
|--pages --------頁(yè)面
|--router --------路由配置
|--store --------vuex狀態(tài)管理
|--service --------axios封裝
|--App.vue --------App
|--main.js --------入口文件
|--permission.js --------權(quán)限控制
|-- server --------服務(wù)器端項(xiàng)目代碼
|--confog --------數(shù)據(jù)庫(kù)鏈接相關(guān)
|--middleware --------中間件
|--models --------Schema和Model
|--routes --------路由
|--views --------ejs頁(yè)面模板
|--public --------靜態(tài)資源
|--utils --------工具方法
|--app.js --------服務(wù)端入口
|-- common --------前后端公用代碼模塊(如加解密)
|-- engine-template --------頁(yè)面模板引擎,使用webpack打包成js提供頁(yè)面引用
|-- docs --------預(yù)留編寫項(xiàng)目文檔目錄
|-- config.json --------配置文件
復(fù)制代碼
編輯器的實(shí)現(xiàn)思路是:編輯器生成頁(yè)面JSON數(shù)據(jù),服務(wù)端負(fù)責(zé)存取JSON數(shù)據(jù),渲染時(shí)從服務(wù)端取數(shù)據(jù)JSON交給前端模板處理。
確認(rèn)了實(shí)現(xiàn)邏輯,數(shù)據(jù)結(jié)構(gòu)也是非常重要的,把一個(gè)頁(yè)面定義成一個(gè)JSON數(shù)據(jù),數(shù)據(jù)結(jié)構(gòu)大致是這樣的:
頁(yè)面工程數(shù)據(jù)接口
{
title: '', // 標(biāo)題
description: '', //描述
coverImage: '', // 封面
auther: '', // 作者
script: '', // 頁(yè)面插入腳本
width: 375, // 高
height: 644, // 寬
pages: [], // 多頁(yè)頁(yè)面
shareConfig: {}, // 微信分享配置
pageMode: 0, // 渲染模式,用于擴(kuò)展多種模式渲染,翻頁(yè)h5/長(zhǎng)頁(yè)/PC頁(yè)面等等
}
復(fù)制代碼
多頁(yè)頁(yè)面pages其中一頁(yè)數(shù)據(jù)結(jié)構(gòu):
{
name: '',
elements: [], // 頁(yè)面元素
commonStyle: {
backgroundColor: '',
backgroundImage: '',
backgroundSize: 'cover'
},
config: {}
}
復(fù)制代碼
元素?cái)?shù)據(jù)結(jié)構(gòu):
{
elName: '', // 組件名
animations: [], // 圖層的動(dòng)畫,可以支持多個(gè)動(dòng)畫
commonStyle: {}, // 公共樣式,默認(rèn)樣式
events: [], // 事件配置數(shù)據(jù),每個(gè)圖層可以添加多個(gè)事件
propsValue: {}, // 屬性參數(shù)
value: '', // 綁定值
valueType: 'String', // 值類型
isForm: false // 是否是表單控件,用于表單提交時(shí)獲取表單數(shù)據(jù)
}
復(fù)制代碼
用戶在左側(cè)組件區(qū)域選擇組件添加到頁(yè)面上,編輯區(qū)域通過(guò)動(dòng)態(tài)組件特性渲染出每個(gè)元素組件。
最后,點(diǎn)擊保存將頁(yè)面數(shù)據(jù)提交到數(shù)據(jù)庫(kù)。至于數(shù)據(jù)怎么轉(zhuǎn)成靜態(tài) HTML方法有很多。還有頁(yè)面數(shù)據(jù)我們?nèi)慷加校覀兛梢宰鲰?yè)面的預(yù)渲染,骨架屏,ssr,編譯時(shí)優(yōu)化等等。而且我們也可以對(duì)產(chǎn)出的活動(dòng)頁(yè)做數(shù)據(jù)分析~有很多想象的空間。
編輯器核心代碼,基于 Vue 動(dòng)態(tài)組件特性實(shí)現(xiàn):
為大家附上 Vue 官方文檔:cn.vuejs.org/v2/api/#is
編輯畫板只需要循環(huán)遍歷pages[i].elements數(shù)組,將里面的元素組件JSON數(shù)據(jù)取出,通過(guò)動(dòng)態(tài)組件渲染出各個(gè)組件,支持拖拽改變位置尺寸.
在client目錄新建plugins來(lái)管理組件庫(kù)。也可以將該組件庫(kù)發(fā)到npm上工程中通過(guò)npm管理
編寫組件,考慮的是組件庫(kù),所以我們竟可能讓我們的組件支持全局引入和按需引入,如果全局引入,那么所有的組件需要要注冊(cè)到Vue component 上,并導(dǎo)出:
client/plugins下新建index.js入口文件
```
/**
* 組件庫(kù)入口
* */
import Text from './text'
// 所有組件列表
const components = [
Text
]
// 定義 install 方法,接收 Vue 作為參數(shù)
const install = function (Vue) {
// 判斷是否安裝,安裝過(guò)就不繼續(xù)往下執(zhí)行
if (install.installed) return
install.installed = true
// 遍歷注冊(cè)所有組件
components.map(component => Vue.component(component.name, component))
}
// 檢測(cè)到 Vue 才執(zhí)行,畢竟我們是基于 Vue 的
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
install,
// 所有組件,必須具有 install,才能使用 Vue.use()
Text
}
```
復(fù)制代碼
示例: text文本組件
client/plugins下新建text組件目錄
|-- text --------text組件
|--src --------資源
|--index.vue --------組件
|--index.js --------入口
復(fù)制代碼
text/index.js
// 為組件提供 install 方法,供組件對(duì)外按需引入
import Component from './src/index'
Component.install = Vue => {
Vue.component(Component.name, Component)
}
export default Component
復(fù)制代碼
text/src/index.vue
<!--text.vue-->
<template>
<div class="qk-text">
{{text}}
</div>
</template>
<script>
export default {
name: 'QkText', // 這個(gè)名字很重要,它就是未來(lái)的標(biāo)簽名<qk-text></qk-text>
props: {
text: {
type: String,
default: '這是一段文字'
}
}
}
</script>
<style lang="scss" scoped>
</style>
復(fù)制代碼
編輯器里使用組件庫(kù):
// 引入組件庫(kù)
import QKUI from 'client/plugins/index'
// 注冊(cè)組件庫(kù)
Vue.use(QKUI)
// 使用:
<qk-text text="這是一段文字"></qk-text>
復(fù)制代碼
按照這個(gè)組件開(kāi)發(fā)方式我們可以擴(kuò)展任意多的組件,來(lái)豐富組件庫(kù)
需要注意的是這里的組件最外層寬高都要求是100%
Quark-h5編輯器左側(cè)選擇組件區(qū)域可以通過(guò)一個(gè)配置文件定義可選組件 新建一個(gè)ele-config.js配置文件:
export default [
{
title: '基礎(chǔ)組件',
components: [
{
elName: 'qk-text', // 組件名,與組件庫(kù)名稱一致
title: '文字',
icon: 'iconfont iconwenben',
// 給每個(gè)組件配置默認(rèn)顯示樣式
defaultStyle: {
height: 40
}
}
]
},
{
title: '表單組件',
components: []
},
{
title: '功能組件',
components: []
},
{
title: '業(yè)務(wù)組件',
components: []
}
]
復(fù)制代碼
公共方法中提供一個(gè)function 通過(guò)組件名和默認(rèn)樣式獲取元素組件JSON,getElementConfigJson(elName, defaultStyle)方法
公共樣式屬性編輯比較簡(jiǎn)單就是對(duì)元素JSON對(duì)象commonStyles字段進(jìn)行編輯操作
1.為組件的每一個(gè)prop屬性開(kāi)發(fā)一個(gè)屬性編輯組件. 例如:QkText組件需要text屬性,新增一個(gè)attr-qk-text組件來(lái)操作該屬性 2.獲取組件prop對(duì)象 3.遍歷prop對(duì)象key, 通過(guò)key判斷顯示哪些屬性編輯組件
動(dòng)畫效果引入Animate.css動(dòng)畫庫(kù)。元素組件動(dòng)畫,可以支持多個(gè)動(dòng)畫。數(shù)據(jù)存在元素JSON對(duì)象animations數(shù)組里。
監(jiān)聽(tīng)mouseover和mouseleave,當(dāng)鼠標(biāo)移入時(shí)將動(dòng)畫className添加入到元素上,鼠標(biāo)移出時(shí)去掉動(dòng)畫lassName。這樣就實(shí)現(xiàn)了hover預(yù)覽動(dòng)畫
組件編輯時(shí)支持動(dòng)畫預(yù)覽和單個(gè)動(dòng)畫預(yù)覽。
封裝一個(gè)動(dòng)畫執(zhí)行方法
/**
* 動(dòng)畫方法, 將動(dòng)畫css加入到元素上,返回promise提供執(zhí)行后續(xù)操作(將動(dòng)畫重置)
* @param $el 當(dāng)前被執(zhí)行動(dòng)畫的元素
* @param animationList 動(dòng)畫列表
* @param isDebugger 動(dòng)畫列表
* @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`
// 如果是循環(huán)播放就將循環(huán)次數(shù)置為1,這樣有效避免編輯時(shí)因?yàn)轭A(yù)覽循環(huán)播放組件播放動(dòng)畫無(wú)法觸發(fā)animationend來(lái)暫停組件動(dòng)畫
$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()
}
}
復(fù)制代碼
animationIterationCount 如果是編輯模式的化動(dòng)畫只執(zhí)行一次,不然無(wú)法監(jiān)聽(tīng)到動(dòng)畫結(jié)束animationend事件
執(zhí)行動(dòng)畫前先將元素樣式style緩存起來(lái),當(dāng)動(dòng)畫執(zhí)行完再將原樣式賦值給元素
let cssText = this.$el.style.cssText;
runAnimations(this.$el, animations, true, () => {
this.$el.style.cssText = cssText
})
復(fù)制代碼
提供事件mixins混入到組件,每個(gè)事件方法返回promise,元素被點(diǎn)擊時(shí)按順序執(zhí)行事件方法
參考百度H5,將腳本以script標(biāo)簽形式嵌入。頁(yè)面加載后執(zhí)行。 這里也可以考慮mixins方式混入到頁(yè)面或者組件,可根據(jù)業(yè)務(wù)需求自行擴(kuò)展,都是可以實(shí)現(xiàn)的。
將psd每個(gè)設(shè)計(jì)圖中的每個(gè)圖層導(dǎo)出成圖片保存到靜態(tài)資源服務(wù)器中,
服務(wù)端安裝psd依賴
cnpm install psd --save
復(fù)制代碼
加入psd.js依賴,并且提供接口來(lái)處理數(shù)據(jù)
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) {
// 轉(zhuǎn)換不出來(lái)的圖層先忽略
continue;
}
}
ctx.body = {
elements: psdSourceList,
document: psd.tree().export().document
};
})
復(fù)制代碼
最后把獲取的數(shù)據(jù)轉(zhuǎn)義并返回給前端,前端獲取到數(shù)據(jù)后使用系統(tǒng)統(tǒng)一方法,遍歷添加統(tǒng)一圖片組件
這里只需要注意下圖片跨域問(wèn)題,官方提供html2canvas: proxy解決方案。它將圖片轉(zhuǎn)化為base64格式,結(jié)合使用設(shè)置(proxy: theProxyURL), 繪制到跨域圖片時(shí),會(huì)去訪問(wèn)theProxyURL下轉(zhuǎn)化好格式的圖片,由此解決了畫布污染問(wèn)題。 提供一個(gè)跨域接口
/**
* html2canvas 跨域接口設(shè)置
*/
router.get('/html2canvas/corsproxy', async ctx => {
ctx.body = await request(ctx.query.url)
})
復(fù)制代碼
在engine-template目錄下新建swiper-h5-engine頁(yè)面組件,這個(gè)組件接收到頁(yè)面JSON數(shù)據(jù)就可以把頁(yè)面渲染出來(lái)。跟編輯預(yù)覽畫板實(shí)現(xiàn)邏輯差不多。
然后使用vue-cli庫(kù)打包命令將組件打包成engine.js庫(kù)文件。ejs模板引入該頁(yè)面組件配合json數(shù)據(jù)渲染出頁(yè)面
提供兩種方案解決屏幕適配 1、等比例縮放 在將json元素轉(zhuǎn)換為dom元素的時(shí)候,對(duì)所有的px單位做比例轉(zhuǎn)換,轉(zhuǎn)換公式為 new = old * windows.x / pageJson.width,這里的pageJson.width是頁(yè)面的一個(gè)初始值,也是編輯時(shí)候的默認(rèn)寬度,同時(shí)viewport使用device-width。 2.全屏背景, 頁(yè)面垂直居中 因?yàn)闀?huì)存在上下或者左右有間隙的情況,這時(shí)候我們把背景顏色做全屏處理
頁(yè)面垂直居中只適用于全屏h5, 以后擴(kuò)展長(zhǎng)頁(yè)和PC頁(yè)就不需要垂直居中處理。
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"
執(zhí)行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
});
復(fù)制代碼
因?yàn)閔tml2canvas需要圖片允許跨域,所以在靜態(tài)資源服務(wù)中所有資源請(qǐng)求設(shè)置'Access-Control-Allow-Origin':'*'
app.js
//配置靜態(tài)web
app.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){
res.header( 'Access-Control-Allow-Origin', '*')
}});
復(fù)制代碼
app.js
const fs = require('fs')
fs.readdirSync('./routes').forEach(route=> {
let api = require(`./routes/${route}`)
app.use(api.routes(), api.allowedMethods())
})
復(fù)制代碼
app.js
const jwt = require('koa-jwt')
app.use(jwt({ secret: 'yourstr' }).unless({
path: [
/^\/$/, /\/token/, /\/wechat/,
{ url: /\/papers/, methods: ['GET'] }
]
}));
復(fù)制代碼
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 || '接口異常,請(qǐng)重試',
code: ctx.status,
body: '接口請(qǐng)求失敗',
status: false
}
}
}).catch((err) => {
if (err.status === 401) {
ctx.status = 401;
ctx.body = {
code: 401,
status: false,
message: '登錄過(guò)期,請(qǐng)重新登錄'
}
} else {
throw err
}
})
}
復(fù)制代碼
當(dāng)接口發(fā)布到線上,前端通過(guò)ajax請(qǐng)求時(shí),會(huì)報(bào)跨域的錯(cuò)誤。koa2使用koa2-cors這個(gè)庫(kù)非常方便的實(shí)現(xiàn)了跨域配置,使用起來(lái)也很簡(jiǎn)單
const cors = require('koa2-cors');
app.use(cors());
復(fù)制代碼
我們使用mongodb數(shù)據(jù)庫(kù),在koa2中使用mongoose這個(gè)庫(kù)來(lái)管理整個(gè)數(shù)據(jù)庫(kù)的操作。
根目錄下新建config文件夾,新建mongo.js
// config/mongo.js
const mongoose = require('mongoose').set('debug', true);
const options = {
autoReconnect: true
}
// username 數(shù)據(jù)庫(kù)用戶名
// password 數(shù)據(jù)庫(kù)密碼
// localhost 數(shù)據(jù)庫(kù)ip
// dbname 數(shù)據(jù)庫(kù)名稱
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, '連接錯(cuò)誤:'));
db.once('open', ()=> {
console.log('mongodb connect suucess');
})
}
}
復(fù)制代碼
把mongodb配置信息放到config.json中統(tǒng)一管理
const mongoConf = require('./config/mongo');
mongoConf.connect();
復(fù)制代碼
... 服務(wù)端具體接口實(shí)現(xiàn)就不詳細(xì)介紹了,就是對(duì)頁(yè)面的增刪改查,和用戶的登錄注冊(cè)難度不大
npm run dev-client
復(fù)制代碼
npm run dev-server
復(fù)制代碼
注意: 如果沒(méi)有生成過(guò)引擎模板js文件的,需要先編輯引擎模板,否則預(yù)覽頁(yè)面加載頁(yè)面引擎.js 404報(bào)錯(cuò)
npm run lib:h5-swiper
為 WEB 開(kāi)發(fā)人員,每天編輯 HTML 和 CSS 不是什么大的問(wèn)題,有甚者有個(gè)記事本就能完成(大神請(qǐng)繞步),但對(duì)我們大多數(shù)來(lái)說(shuō),有個(gè)順手的編輯器可以節(jié)少不少的精力和時(shí)間。
子曰:工欲善其事,必先利其器。
下面就盤點(diǎn)幾個(gè)最好用的 WEB 開(kāi)發(fā)利器。
Visual Studio Code 可能是目前最流行的代碼編輯器了,它不僅支持JavaScript,而且還支持Node.js,TypeScript,并且對(duì)其他語(yǔ)言(包括C ++,C#,Python,PHP等)的插件生態(tài)系統(tǒng)。
主要功能:
Sublime 可以說(shuō)是最好用的免費(fèi)代碼編輯器了。支持的語(yǔ)言也多,插件多,最重要一點(diǎn)是它小且快。另外它本身是用thon 構(gòu)建的。
WebStorm 就是存粹是為 WEB 開(kāi)發(fā)而生的了。它支持它支持JavaScript,HTML和CSS等技術(shù),還支持Angular JS,TypeScript,Node.js,Meteor,ECMAScript,React,Vue.js,Cordova等。
HBuilder 是國(guó)產(chǎn)的比較不錯(cuò)的代碼編輯器。它不僅可以開(kāi)發(fā) WEB 應(yīng)用,而且它還可以開(kāi)發(fā)小程序跟輕應(yīng)用,對(duì)于初學(xué)者來(lái)說(shuō)很容易上手的代碼編輯器。
Atom 是github專門為程序員推出的一個(gè)跨平臺(tái)文本編輯器。具有簡(jiǎn)潔和直觀的圖形用戶界面,并有很多有趣的特點(diǎn):支持CSS,HTML,JavaScript等網(wǎng)頁(yè)編程語(yǔ)言,不過(guò)它有個(gè)問(wèn)題就是比較慢。
一、項(xiàng)目簡(jiǎn)介
今天說(shuō)的這個(gè)軟件是一款基于 vue 可視化拖拽頁(yè)面生成工具。
商品搜索
標(biāo)題文本
圖片廣告
圖文導(dǎo)航
底部導(dǎo)航
魔方
公告
視頻
富文本
分割線
店鋪信息
單元格
自定義模塊等
可以導(dǎo)入導(dǎo)出json
預(yù)覽
查看json
vue
html
css
js
五、源碼地址
私信回復(fù):可視化
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。