pl-drag-template
Github地址:https://github.com/livelyPeng/pl-drag-template
前言
想必你一定使用過易企秀或百度H5等微場景生成工具制作過炫酷的h5頁面,除了感嘆其神奇之處有沒有想過其實作方式呢?本文從零開始實作一個H5編輯器專案完整設計思路和主要實作步驟,并開源前后端代碼,有需要的小伙伴可以按照該教程從零實作自己的H5編輯器,(實作起來并不復雜,該教程只是提供思路,并非最佳實踐)
一個h5可視化編輯器種子, 高仿凡科建站模板,
點擊查看pl-drag-template在線demo
大概圖形:
拖動左邊組件到畫板區域釋放即可,或者點擊左邊區域的組件,
注意: 最好使用谷歌打開,點擊保存按鈕就是一串json資料,你可以吧這個資料拿到其他手機平臺進行渲染啦,有問題就加群 里面代碼注釋齊全,誰都看懂的哦
在這個模板的基礎上,你就可以實作類似凡科的模板(當然你還可以實作其他的類似模板),如下圖就是我們產品的模樣
專案目錄
src { apiUrl: 請路徑存放 assets: 專案資產存在(圖片等) components: 公用組件存放 module: 模塊位置 { 畫板模塊的配置如下: { components: 當前模塊的私有組件 { attributeConfig: 右邊屬性配置組件 ... 其他的都是畫板頁面的組件 } pluginLibrary: 畫板的插件/模塊/組件(非常重要) routers: 當前模塊的路由表 style: 當前畫板的樣式 utils: 公用js存放庫 vuex: 當前模塊的狀態存盤 viewPage: 當前模塊的頁面 index.js: 匯出當前模塊 } } vuex: 整個專案的狀態存盤匯集地方 themes: 整個專案的公用樣式表集中地方 utils: 整個專案的工具檔案夾 }
技術堆疊
前端:vue: 模塊化開發少不了angular,react,vue三選一,這里選擇了vue,vuex: 狀態管理less: css預編譯器,element-ui:不造輪子,有現成的優秀的vue組件庫當然要用起來,沒有的自己再封裝一些就可以了,loadsh:工具類
工程搭建
基于vue-cli2環境搭建
- 如何規劃好我們專案的目錄結構?首先我們需要有一個目錄作為前端專案,一個目錄作為后端專案,所以我們要對vue-cli 生成的專案結構做一下改造:
··· · |-- client // 原 src 目錄,改成 client 用作前端專案目錄 |-- server // 新增 server 用于服務端專案目錄 |-- engine-template // 新增 engine-template 用于頁面模板庫目錄 |-- docs // 新增 docs 預留撰寫專案檔案目錄 · ···
-
這樣的話 我們需要再把我們webpack組態檔稍作一下調整
-
module.exports = { resolve: { extensions: ['.ts', '.js', '.vue', '.json'], alias: { // 'vue$': 'vue/dist/vue.esm.js', '@': utils.resolve('src') } }, externals: { 'vue': 'Vue', "echarts": "echarts", 'vue-router': 'VueRouter', 'vuex': 'Vuex', 'element-ui': 'ELEMENT', 'moment': 'moment' }, module: { rules: [ ...(config.dev.useEslint ? [createLintingRule()] : []), { test: /\.vue$/, loader: 'vue-loader', options: { transformAssetUrls: { video: ['src', 'poster'], source: 'src', img: 'src', image: 'xlink:href' } } }, { test: /\.js$/, loader: 'babel-loader', exclude: file => /node_modules/.test(file) && !/\.vue\.js/.test(file) && !/element-ui(\\|\/)(src|packages)/.test(file) && !/pl-table/.test(file) }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash].[ext]') } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash].[ext]') } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash].[ext]') } }, { test: /\.less$/, use: [{ loader: process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'vue-style-loader' }, { loader: 'css-loader', options: { sourceMap: cssSourceMap } }, { loader: 'less-loader', options: { sourceMap: cssSourceMap } }, { loader: 'sass-resources-loader', options: { resources: [ path.resolve(__dirname, '../src/themes/publicStyle/common.less') ] } }] }, { test: /\.css$/, use: [{ loader: process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'vue-style-loader', }, { loader: 'css-loader', options: { sourceMap: cssSourceMap } }] }] }, plugins: [ new VueLoaderPlugin(), // 復制靜態資源到目錄中,如果有更多需要復制的資源,請在這里添加 new CopyWebpackPlugin([{ from: utils.resolve('static'), to: config.build.assetsSubDirectory, ignore: ['.*'] }]) ] }
這樣我們搭建起來一個簡易的專案目錄結構,
前端編輯器實作
編輯器的實作思路是:編輯器生成頁面JSON資料,服務端負責存取JSON資料,渲染時從服務端取資料JSON交給前端模板處理,
資料結構(非常重要)
/* * 注意注意注意: pluginLibrary里面組件的name值必須寫,然后必須寫下面的elName組件名 * 1. elName: 'pl-text', // 非常重要請正確寫上對應的vue組件的組件名,name值 如export default {name: 'PlButton'} 那么elName就是pl-button * 2. 除了容器的物件plContainer屬性,(注意:看容器的屬性請看下面的容器基本結構)其他配置表屬性的介紹如下 * title: 組件提示文字(左邊組件按鈕區域用到了) * icon: 組件圖示(左邊組件按鈕區域用到了,使用的是 Iconfont-阿里巴巴矢量圖示庫) * 以下全是組件本身的屬性,不是左邊組件按鈕區域串列的屬性 * elName: 組件名 * pointList: 控制組件拖動的方向(拖動的小圓點) pointList: ['lt' 左上, 'rt' 右上, 'lb' 左下, 'rb' 右下, 'l' 左, 'r' 右, 't' 上, 'b' 下], * // ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b' ] * value: '' // 輸入框的值,主要用在這個畫板元素上的輸入框型別組件上 * contenteditable: 組件輸入狀態是否可以被拖動 * placeholder: 輸入框型別的組件,空文本提示文字 * commonStyle:初始化的樣式,就是css不多介紹 * options:{ // 組件配置項 * classList: [], 當前組件的類集合 lineHeightChange: true // 表示行高需要隨著拖動的高度變化(只有可以拖動的元素有效) * } * module: boolean 為true代表當前組件不是個畫板元素,而是作為一個模塊的身份,(但是它依然存放在容器中) 什么是非畫板元素,就是不能再自由容器中拖動和自由組合,非畫板元素是模塊組件 * containerOptions: {} 如果我配置了module為true,代表當前是個模塊,模塊身份可以去配置容器物件的屬性 * propsValue: {} // 里面包含了組件所有的data物件屬性,它不需要再基本結構中配置,他會在生成組件的時候會放到該配置中來 */ import {pageWh, defaultStyle, moduleContainer} from './config' // 容器的基本結構 export const plContainer = { elName: 'pl-container', title: '自由容器', icon: 'iconfont iconrongqi', pointList: ['b'], // 模塊拖動的方向有哪些 // 容器最外層盒子的樣式 containerStyle: { // 容器大盒子的樣式 marginBottom: 10 }, allowed: true, // 代表我當前容器是個畫板,拖影片板元素可以放到容器上面 showTitle: true, // 是否顯示頭部 // 容器頭部的樣式 titleStyle: { height: 50, lineHeight: 50 }, titleBarName: '標題欄', // 容器畫板的默認樣式 commonStyle: { width: pageWh.width, height: 250, position: 'relative', minHeight: 50, // 容器里面的畫板最小高度值 backgroundColor: '#fff' }, childNode: [] // 容器子節點的集裝箱 } // 基礎組件 const BasicComponents = [ { title: '基礎組件', components: [ plContainer, { elName: 'pl-text', title: '文本', icon: 'iconfont iconwenbenyu', pointList: [], // 控制組件拖動的方向 contenteditable: false, placeholder: '點擊輸入內容', commonStyle: { ...defaultStyle, padding: 8, fontSize: 15, lineHeight: 17, height: 'auto', textAlign: 'left', minWidth: 35, width: 160 } }, { elName: 'pl-button', title: '按鈕', icon: 'iconfont iconanniu', pointList: ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b'], // 控制組件拖動的方向 contenteditable: false, options: { classList: [], lineHeightChange: true // 表示行高需要隨著拖動的高度變化 }, commonStyle: { ...defaultStyle, fontSize: 15, lineHeight: 36, height: 36, textAlign: 'center', minWidth: 35, minHeight: 36, width: 80 } }, { elName: 'cube-nav', title: '魔方導航', icon: 'iconfont iconfenlei', module: true, containerOptions: { ...moduleContainer, titleBarName: '魔方導航模塊' }, options: { classList: [] } }, { elName: 'carousel', title: '多圖文輪播', icon: 'iconfont iconlunbotu', module: true, containerOptions: { ...moduleContainer, titleBarName: '多圖文輪播' }, options: { classList: [] } } ] } ] const components = [...BasicComponents] // 遍歷判斷找出畫板元素的組件 // 在拖拽元素到畫板的時候,會判斷當前拖動的組件是否在這里面存在,存在才可以添加組件到畫板容器 // 必須是畫板組件 export const drawingComponent = components.map(item => item.components.map(con => { if (!con.module && con.elName !== 'pl-container') return con.elName }))[0].filter(item => item) export default components
頁面整體結構

核心代碼
編輯器核心代碼,基于 Vue 動態組件特性實作:

// 獲取需要繪畫的節點資料(整個可視化編輯器的最重要的東西) export const getNodeElement = (nodeData, type) => { // 如果不存在該組件就直接回傳 if (!nodeData || !componentsName.includes(camelCase(nodeData.elName).toLowerCase())) { Message.error({message: '沒有該模塊!', type: 'warning', duration: 2000}) return null } // 需要添加的節點元素物件 let nodeElement // 獲取當前組件的data資料(非常重要,它將是你原始組件的初始化資料,你右邊的屬性控制就是去更改的它) let props = getComponentProps(nodeData.elName) // 獲取需要添加的節點元素的資料結構 nodeElement = deepClone(getElementConfig({...nodeData, needProps: props})) // 注意注意注意: 如果我進來的不是容器,那么就需要包裝一層容器,在回傳節點 // type如果存在,代表我是往容器里面加節點不需要被容器包裹,就不需要執行if陳述句了 if (nodeElement.elName !== 'pl-container' && type !== '我是往容器里面加節點不需要被容器包裹') { // 獲取pl-container容器組件的data資料 let props = getComponentProps('pl-container') // 獲取容器的基本結構 let containerNodeData =https://www.cnblogs.com/plBlog/p/ getElementConfig({...plContainer, needProps: props}) // 什么是非畫板元素,就是不能再自由容器中拖動和自由組合,非畫板元素是模塊組件 // 下面if陳述句是做非畫板元素的關鍵,意思就是非畫板元素,它也屬于自由容器中,但是它不能拖動 // 如果當前組件是一個模塊, 就需要執行下面的陳述句 if (nodeElement.module) { // 如果是模塊,那么就去看是否改變了容器的樣式,沒有改變默認給個改變容器的基本值 let cops = judgeObject(nodeElement.containerOptions) ? nodeElement.containerOptions : moduleContainer // 合并容器的屬性(很好理解就是去覆寫掉原來容器的屬性,因為原來容器的屬性是為了畫板而生的,但是模塊本身也是被容器包裹的,所以需要去覆寫容器的配置) let newContainer = {...containerNodeData, ...cops} // 洗掉當前需要添加的節點,里面的配置容器物件 delete nodeElement.containerOptions // 然后再把需要添加的節點放入容器中 newContainer.childNode.push(nodeElement) return deepClone(newContainer) } // 把需要添加的元素放入到容器節點中 containerNodeData.childNode.push(nodeElement) // 匯出容器 return deepClone(containerNodeData) } // 回傳當前組件 return nodeElement }
組件庫
撰寫組件,考慮的是組件庫,所以我們竟可能讓我們的組件支持全域引入和按需引入,如果全域引入,那么所有的組件需要要注冊到Vue component 上,并匯出:
/** * 組件庫入口 * */ // 基礎組件 import plEditDiv from './editDiv' // 必須放第一個位置引入 因為下面的組件有用到它 import plText from './text' import plButton from './Button' import plContainer from './container' import cubeNav from './cubeNav' import carousel from './carousel' // 所有組件串列 const components = [ plEditDiv, plText, plButton, plContainer, cubeNav, carousel ] let plRegisterComponentsObject = {} let componentsName = [] components.forEach(item => { plRegisterComponentsObject[item.name] = item // 匯出當前組件的組件名 if (item.name && typeof item.name === 'string') { componentsName.push(item.name.toLowerCase()) } }) // 定義 install 方法,接收 Vue 作為引數 const install = function (Vue) { // 判斷是否安裝,安裝過就不繼續往下執行 if (install.installed) return install.installed = true // 遍歷注冊所有組件 components.map(component => Vue.component(component.name, component)) } export { componentsName, plEditDiv, cubeNav, plButton, carousel, plText, plContainer, plRegisterComponentsObject } export default { install }
啟動運行
npm run dev
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/3984.html
標籤:HTML5
下一篇:瀏覽器跨域問題分析


