想必你必定使用過易企秀或百度H5等微場景生成工具製做過炫酷的h5頁面,除了感嘆其神奇之處有沒有想過其實現方式呢?本文從零開始實現一個H5編輯器項目完整設計思路和主要實現步驟,並開源先後端代碼。有須要的小夥伴能夠按照該教程從零實現本身的H5編輯器。(實現起來並不複雜,該教程只是提供思路,並不是最佳實踐)css
編輯器預覽:前端
前端:vue
: 模塊化開發少不了angular,react,vue三選一,這裏選擇了vue。vuex
: 狀態管理sass
: css預編譯器element-ui
:不造輪子,有現成的優秀的vue組件庫固然要用起來。沒有的本身再封裝一些就能夠了。loadsh
:工具類vue
服務端:koa
:後端語言採用nodejs,koa文檔和學習資料也比較多,express原班人馬打造,這個正合適。mongodb
:一個基於分佈式文件存儲的數據庫,比較靈活。node
一、瞭解vue技術棧開發
二、瞭解koa
三、瞭解mongodbreact
基於vue-cli3環境搭建webpack
··· · |-- client // 原 src 目錄,改爲 client 用做前端項目目錄 |-- server // 新增 server 用於服務端項目目錄 |-- engine-template // 新增 engine-template 用於頁面模板庫目錄 |-- docs // 新增 docs 預留編寫項目文檔目錄 · ···
這樣的話 咱們須要再把咱們webpack配置文件稍做一下調整,首先是把原先的編譯指向src的目錄改爲client,其次爲了 npm run build 能正常編譯 client 咱們也須要爲 babel-loader 再增長一個編譯目錄:ios
根目錄新增vue.config.js,目的是爲了改造項目入口,改成:client/main.jsgit
module.exports = { pages: { index: { entry: "client/main.js" } } }
babel-loader能正常編譯 client, engine-template目錄, 在vue.config.js新增以下配置github
// 擴展 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數據渲染出頁面
提供兩種方案解決屏幕適配
一、等比例縮放
在將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('./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