Vue + Koa從零打造一個H5頁面可視化編輯器——Quark-h5

前言

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

Github: 傳送門
演示地址:傳送門html

編輯器預覽:前端

技術棧

前端:
vue: 模塊化開發少不了angular,react,vue三選一,這裏選擇了vue。
vuex: 狀態管理
sass: css預編譯器。
element-ui:不造輪子,有現成的優秀的vue組件庫固然要用起來。沒有的本身再封裝一些就能夠了。
loadsh:工具類
vue

服務端:
koa:後端語言採用nodejs,koa文檔和學習資料也比較多,express原班人馬打造,這個正合適。
mongodb:一個基於分佈式文件存儲的數據庫,比較靈活。
node

閱讀前準備

一、瞭解vue技術棧開發
二、瞭解koa
三、瞭解mongodb
react

工程搭建

基於vue-cli3環境搭建
webpack

  • 如何規劃好咱們項目的目錄結構?首先咱們須要有一個目錄做爲前端項目,一個目錄做爲後端項目。因此咱們要對vue-cli 生成的項目結構作一下改造:
···
·
|-- 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 // 是不是表單控件,用於表單提交時獲取表單數據
}
複製代碼

編輯器總體設計

  • 一個組件選擇區,提供使用者選擇須要的組件
  • 一個編輯預覽畫板,提供使用者拖拽排序頁面預覽的功能
  • 一個組件屬性編輯,提供給使用者編輯組件內部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 = [
	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字段進行編輯操做

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){
	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,元素被點擊時按順序執行事件方法

頁面插入js腳本

參考百度H5,將腳本以script標籤形式嵌入。頁面加載後執行。 這裏也能夠考慮mixins方式混入到頁面或者組件,可根據業務需求自行擴展,都是能夠實現的。

redo/undo歷史操做紀錄

  1. 歷史操做紀錄存在狀態機store.state.editor.historyCache數組中。
  2. 每次修改編輯操做都把整個pageDataJson字段push到historyCache
  3. 點擊redo/undo時根據index獲取到pageDataJson從新渲染頁面

psd設計圖導入生成h5頁面

將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
	};
})
複製代碼

最後把獲取的數據轉義並返回給前端,前端獲取到數據後使用系通通一方法,遍歷添加統一圖片組件

  • psd源文件大小最好不要超過30M,過大會致使瀏覽器卡頓甚至卡死
  • 儘量合併圖層,並柵格化全部圖層
  • 較複雜的圖層樣式,如濾鏡、圖層樣式等沒法讀取

html2canvas生成縮略圖

這裏只須要注意下圖片跨域問題,官方提供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 腳手架工具生成

ejs-template 模板引擎配置

app.js

//配置ejs-template 模板引擎
render(app, {
	root: path.join(__dirname, 'views'),
	layout: false,
	viewExt: 'html',
	cache: false,
	debug: false
});
複製代碼

koa-static靜態資源服務

由於html2canvas須要圖片容許跨域,因此在靜態資源服務中全部資源請求設置'Access-Control-Allow-Origin':'*'

app.js

//配置靜態web
app.use(koaStatic(__dirname + '/public'), { gzip: true, setHeaders: function(res){
	res.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) => {
	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
		}
	})
}

複製代碼

koa2-cors跨域處理

當接口發佈到線上,前端經過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中統一管理

  • 而後在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
複製代碼
相關文章
相關標籤/搜索