首頁>技術>

前言

想必你一定使用過易企秀或百度H5等微場景生成工具製作過炫酷的h5頁面,除了感嘆其神奇之處有沒有想過其實現方式呢?本文從零開始實現一個H5編輯器專案完整設計思路和主要實現步驟,並開源前後端程式碼。有需要的小夥伴可以按照該教程從零實現自己的H5編輯器。(實現起來並不複雜,該教程只是提供思路,並非最佳實踐)

Github: https://github.com/huangwei9527/quark-h5

演示地址:http://47.104.247.183:4000/

演示帳號密碼均admin

編輯器預覽:

技術棧

前端: 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環境搭建

如何規劃好我們專案的目錄結構?首先我們需要有一個目錄作為前端專案,一個目錄作為後端專案。所以我們要對vue-cli 生成的專案結構做一下改造:
····|-- client\t\t\t\t// 原 src 目錄,改成 client 用作前端專案目錄|-- server\t\t\t\t// 新增 server 用於服務端專案目錄|-- engine-template\t\t// 新增 engine-template 用於頁面模板庫目錄|-- docs\t\t\t\t// 新增 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\t\t\t\t\t--------前端專案介面程式碼    |--common\t\t\t\t\t--------前端介面對應靜態資源    |--components\t\t\t\t--------元件    |--config\t\t\t\t\t--------配置檔案    |--eventBus\t\t\t\t\t--------eventBus    |--filter\t\t\t\t\t--------過濾器    |--mixins\t\t\t\t\t--------混入    |--pages\t\t\t\t\t--------頁面    |--router\t\t\t\t\t--------路由配置    |--store\t\t\t\t\t--------vuex狀態管理    |--service\t\t\t\t\t--------axios封裝    |--App.vue\t\t\t\t\t--------App    |--main.js\t\t\t\t\t--------入口檔案    |--permission.js\t\t\t--------許可權控制|-- server\t\t\t\t\t--------伺服器端專案程式碼    |--confog\t\t\t\t\t--------資料庫連結相關    |--middleware\t\t\t\t--------中介軟體    |--models\t\t\t\t\t--------Schema和Model    |--routes\t\t\t\t\t--------路由    |--views\t\t\t\t\t--------ejs頁面模板    |--public\t\t\t\t\t--------靜態資源    |--utils\t\t\t\t\t--------工具方法    |--app.js\t\t\t\t\t--------服務端入口|-- common\t\t\t\t\t--------前後端公用程式碼模組(如加解密)|-- engine-template\t\t\t--------頁面模板引擎,使用webpack打包成js提供頁面引用|-- docs\t\t\t\t\t--------預留編寫專案文件目錄|-- config.json\t\t\t\t--------配置檔案複製程式碼
前端編輯器實現

編輯器的實現思路是:編輯器生成頁面JSON資料,服務端負責存取JSON資料,渲染時從服務端取資料JSON交給前端模板處理。

資料結構

確認了實現邏輯,資料結構也是非常重要的,把一個頁面定義成一個JSON資料,資料結構大致是這樣的:

頁面工程資料介面

{\ttitle: '', // 標題\tdescription: '', //描述\tcoverImage: '', // 封面\tauther: '', // 作者\tscript: '', // 頁面插入指令碼\twidth: 375, // 高\theight: 644, // 寬\tpages: [], // 多頁頁面\tshareConfig: {}, // 微信分享配置\tpageMode: 0, // 渲染模式,用於擴充套件多種模式渲染,翻頁h5/長頁/PC頁面等等}複製程式碼

多頁頁面pages其中一頁資料結構:

{\tname: '',\telements: [], // 頁面元素\tcommonStyle: {\t\tbackgroundColor: '',\t\tbackgroundImage: '',\t\tbackgroundSize: 'cover'\t},\tconfig: {}}複製程式碼

元素資料結構:

{\telName: '', // 元件名\tanimations: [], // 圖層的動畫,可以支援多個動畫\tcommonStyle: {}, // 公共樣式,預設樣式\tevents: [], // 事件配置資料,每個圖層可以新增多個事件\tpropsValue: {}, // 屬性引數\tvalue: '', // 繫結值\tvalueType: 'String', // 值型別\tisForm: false // 是否是表單控制元件,用於表單提交時獲取表單資料}複製程式碼編輯器整體設計一個元件選擇區,提供使用者選擇需要的元件一個編輯預覽畫板,提供使用者拖拽排序頁面預覽的功能一個元件屬性編輯,提供給使用者編輯元件內部props、公共樣式和動畫的功能 如圖:

使用者在左側元件區域選擇元件新增到頁面上,編輯區域通過動態元件特性渲染出每個元素元件。

最後,點選儲存將頁面資料提交到資料庫。至於資料怎麼轉成靜態 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 = [\tText]// 定義 install 方法,接收 Vue 作為引數const install = function (Vue) {\t// 判斷是否安裝,安裝過就不繼續往下執行\tif (install.installed) return\tinstall.installed = true\t// 遍歷註冊所有元件\tcomponents.map(component => Vue.component(component.name, component))}// 檢測到 Vue 才執行,畢竟我們是基於 Vue 的if (typeof window !== 'undefined' && window.Vue) {\tinstall(window.Vue)}export default {\tinstall,\t// 所有元件,必須具有 install,才能使用 Vue.use()\tText}```複製程式碼
元件開發

示例: text文字元件

client/plugins下新建text元件目錄

|-- text                --------text元件    |--src              --------資源    \t|--index.vue    --------元件    |--index.js         --------入口複製程式碼

text/index.js

// 為元件提供 install 方法,供元件對外按需引入import Component from './src/index'Component.install = Vue => {\tVue.component(Component.name, Component)}export default Component複製程式碼

text/src/index.vue

// 引入元件庫import QKUI from 'client/plugins/index'// 註冊元件庫Vue.use(QKUI)// 使用:<qk-text text="這是一段文字"></qk-text>複製程式碼

按照這個元件開發方式我們可以擴充套件任意多的元件,來豐富元件庫

需要注意的是這裡的元件最外層寬高都要求是100%

配置檔案

Quark-h5編輯器左側選擇元件區域可以通過一個配置檔案定義可選元件 新建一個ele-config.js配置檔案:

export default [\t{\t\ttitle: '基礎元件',\t\tcomponents: [\t\t\t{\t\t\t\telName: 'qk-text', // 元件名,與元件庫名稱一致\t\t\t\ttitle: '文字',\t\t\t\ticon: 'iconfont iconwenben',\t\t\t\t// 給每個元件配置預設顯示樣式\t\t\t\tdefaultStyle: {\t\t\t\t\theight: 40\t\t\t\t}\t\t\t}\t\t]\t},\t{\t\ttitle: '表單元件',\t\tcomponents: []\t},\t{\t\ttitle: '功能元件',\t\tcomponents: []\t},\t{\t\ttitle: '業務元件',\t\tcomponents: []\t}]複製程式碼

公共方法中提供一個function 通過元件名和預設樣式獲取元素元件JSON,getElementConfigJson(elName, defaultStyle)方法

元素屬性編輯公共屬性樣式編輯

公共樣式屬性編輯比較簡單就是對元素JSON物件commonStyles欄位進行編輯操作

props屬性編輯

1.為元件的每一個prop屬性開發一個屬性編輯元件. 例如:QkText元件需要text屬性,新增一個attr-qk-text元件來操作該屬性 2.獲取元件prop物件 3.遍歷prop物件key, 通過key判斷顯示哪些屬性編輯元件

元素新增動畫實現

動畫效果引入Animate.css動畫庫。元素元件動畫,可以支援多個動畫。資料存在元素JSON物件animations數組裡。

選擇面板hover預覽動畫

監聽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){\tlet playFn = function (animation) {\t\treturn new Promise(resolve => {\t\t\t$el.style.animationName =  animation.type\t\t\t$el.style.animationDuration =  `${animation.duration}s`\t\t\t// 如果是迴圈播放就將迴圈次數置為1,這樣有效避免編輯時因為預覽迴圈播放元件播放動畫無法觸發animationend來暫停元件動畫\t\t\t$el.style.animationIterationCount =  animation.infinite ? (isDebug ? 1 : 'infinite') : animation.interationCount\t\t\t$el.style.animationDelay =  `${animation.delay}s`\t\t\t$el.style.animationFillMode =  'both'\t\t\tlet resolveFn = function(){\t\t\t\t$el.removeEventListener('animationend', resolveFn, false);\t\t\t\t$el.addEventListener('animationcancel', resolveFn, false);\t\t\t\tresolve()\t\t\t}\t\t\t$el.addEventListener('animationend', resolveFn, false)\t\t\t$el.addEventListener('animationcancel', resolveFn, false);\t\t})\t}\tfor(let i = 0, len = animationList.length; i < len; i++){\t\tawait playFn(animationList[i])\t}\tif(callback){\t\tcallback()\t}}複製程式碼

animationIterationCount 如果是編輯模式的化動畫只執行一次,不然無法監聽到動畫結束animationend事件

執行動畫前先將元素樣式style快取起來,當動畫執行完再將原樣式賦值給元素

let cssText = this.$el.style.cssText;runAnimations(this.$el, animations, true, () => {\tthis.$el.style.cssText = cssText})複製程式碼
元素新增事件

提供事件mixins混入到元件,每個事件方法返回promise,元素被點選時按順序執行事件方法

頁面插入js指令碼

參考百度H5,將指令碼以script標籤形式嵌入。頁面載入後執行。 這裡也可以考慮mixins方式混入到頁面或者元件,可根據業務需求自行擴充套件,都是可以實現的。

redo/undo歷史操作紀錄歷史操作紀錄存在狀態機store.state.editor.historyCache陣列中。每次修改編輯操作都把整個pageDataJson欄位push到historyCache點選redo/undo時根據index獲取到pageDataJson重新渲染頁面psd設計圖匯入生成h5頁面

將psd每個設計圖中的每個圖層匯出成圖片儲存到靜態資源伺服器中,

服務端安裝psd依賴

cnpm install psd --save複製程式碼

加入psd.js依賴,並且提供介面來處理資料

var PSD = require('psd');router.post('/psdPpload',async ctx=>{\tconst file = ctx.request.files.file; // 獲取上傳檔案\tlet psd = await PSD.open(file.path)\tvar timeStr = + new Date();\tlet descendantsList = psd.tree().descendants();\tdescendantsList.reverse();\tlet psdSourceList = []\tlet currentPathDir = `public/upload_static/psd_image/${timeStr}`\tfor (var i = 0; i < descendantsList.length; i++){\t\tif (descendantsList[i].isGroup()) continue;\t\tif (!descendantsList[i].visible) continue;\t\ttry{\t\t\tawait descendantsList[i].saveAsPng(path.join(ctx.state.SERVER_PATH, currentPathDir + `/${i}.png`))\t\t\tpsdSourceList.push({\t\t\t\t...descendantsList[i].export(),\t\t\t\ttype: 'picture',\t\t\t\timagesrc: ctx.state.BASE_URL + `/upload_static/psd_image/${timeStr}/${i}.png`,\t\t\t})\t\t}catch (e) {\t\t\t// 轉換不出來的圖層先忽略\t\t\tcontinue;\t\t}\t}\tctx.body = {\t\telements: psdSourceList,\t\tdocument: psd.tree().export().document\t};})複製程式碼

最後把獲取的資料轉義並返回給前端,前端獲取到資料後使用系統統一方法,遍歷新增統一圖片元件

psd原始檔大小最好不要超過30M,過大會導致瀏覽器卡頓甚至卡死儘可能合併圖層,並柵格化所有圖層較複雜的圖層樣式,如濾鏡、圖層樣式等無法讀取html2canvas生成縮圖

這裡只需要注意下圖片跨域問題,官方提供html2canvas: proxy解決方案。它將圖片轉化為base64格式,結合使用設定(proxy: theProxyURL), 繪製到跨域圖片時,會去訪問theProxyURL下轉化好格式的圖片,由此解決了畫布汙染問題。 提供一個跨域介面

/** * html2canvas 跨域介面設定 */router.get('/html2canvas/corsproxy', async ctx => {\tctx.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 腳手架工具生成

ejs-template 模板引擎配置

app.js

//配置ejs-template 模板引擎render(app, {\troot: path.join(__dirname, 'views'),\tlayout: false,\tviewExt: 'html',\tcache: false,\tdebug: false});複製程式碼koa-static靜態資源服務

因為html2canvas需要圖片允許跨域,所以在靜態資源服務中所有資源請求設定'Access-Control-Allow-Origin':'*'

app.js

//配置靜態webapp.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){\tres.header( 'Access-Control-Allow-Origin', '*')}});複製程式碼修改路由的註冊方式,通過遍歷routes資料夾讀取檔案

app.js

const fs = require('fs')fs.readdirSync('./routes').forEach(route=> { let api = require(`./routes/${route}`) app.use(api.routes(), api.allowedMethods())})複製程式碼新增jwt認證,同時過濾不需要認證的路由,如獲取token

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) => {\tawait next().then(() => {\t\tif (ctx.status === 200) {\t\t\tctx.body = {\t\t\t\tmessage: '成功',\t\t\t\tcode: 200,\t\t\t\tbody: ctx.body,\t\t\t\tstatus: true\t\t\t}\t\t} else if (ctx.status === 201) { // 201處理模板引擎渲染\t\t} else {\t\t\tctx.body = {\t\t\t\tmessage: ctx.body || '介面異常,請重試',\t\t\t\tcode: ctx.status,\t\t\t\tbody: '介面請求失敗',\t\t\t\tstatus: false\t\t\t}\t\t}\t}).catch((err) => {\t\tif (err.status === 401) {\t\t\tctx.status = 401;\t\t\tctx.body = {\t\t\t\tcode: 401,\t\t\t\tstatus: false,\t\t\t\tmessage: '登入過期,請重新登入'\t\t\t}\t\t} else {\t\t\tthrow err\t\t}\t})}複製程式碼koa2-cors跨域處理

當介面釋出到線上,前端通過ajax請求時,會報跨域的錯誤。koa2使用koa2-cors這個庫非常方便的實現了跨域配置,使用起來也很簡單

const cors = require('koa2-cors');app.use(cors());複製程式碼連線資料庫

我們使用mongodb資料庫,在koa2中使用mongoose這個庫來管理整個資料庫的操作。

建立配置檔案

根目錄下新建config資料夾,新建mongo.js

// config/mongo.jsconst 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中統一管理

然後在app.js中引入const mongoConf = require('./config/mongo');mongoConf.connect();複製程式碼

... 服務端具體介面實現就不詳細介紹了,就是對頁面的增刪改查,和使用者的登入註冊難度不大

啟動執行啟動前端npm run dev-client複製程式碼啟動服務端npm run dev-server複製程式碼

注意: 如果沒有生成過引擎模板js檔案的,需要先編輯引擎模板,否則預覽頁面載入頁面引擎.js 404報錯

編譯engine.js模板引擎npm run lib:h5-swiper

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 總結移動端H5開發常用技巧【乾貨滿滿哦】