1. 程式人生 > 其它 >極客時間前端實戰訓練營

極客時間前端實戰訓練營

愛共享 愛生活 加油 2021

背景

前端視覺化編輯器在現在大公司中都有各樣的實現方式,不同的業務會賦予不一樣的功能,可以參考下面這個文章:https://github.com/taowen/awesome-lowcode,裡面介紹了國內很多廠商低程式碼平臺的實現方式。
行業裡有許多利用 AI 去切割頁面、生產頁面的案例,但在短時間要實現產出,需要消耗大量的人力和精力,不建議在一般團隊去做這樣的事情。

目的

設計這樣一款視覺化編輯器,需要達成以下目的

  • 視覺化編輯,可以在畫布拖拽元素

  • 豐富的樣式配置

  • 將常用業務抽離成元件,並可以配置元件的引數

設計思路

將這個專案分為三個部分:

  • 編輯器
    提供使用者管理專案、拖拽元素、配置引數、預覽釋出等能力

  • 元件庫
    抽離業務邏輯,將其封裝成元件庫管理維護

  • 服務端
    儲存管理專案,最重要的是將編輯器生產出來的資料進行解析生成頁面

技術棧

  1. 由於團隊裡對vue比較熟悉,前端的編輯器和元件庫都使用 vue 作為開發框架

  2. 服務端使用 egg.js,考慮到 egg 較為成熟,不需要自己再從0搭起。當然,也可以選擇其他框架。

技術可以根據個人或者團隊偏好自己決定。

設計思路

image

一、JSON Schema

整個專案除了設計成三個核心工程之外,還有一段JSON Schema,這段JSON在整個專案中起到至關重要的部分,它是構成頁面的基石。JSON Schema 是由編輯器生產,並且每個生產出的JSON Scheme的格式必須保持一致,這樣,服務端渲染的時候才可以對專案的JSOn進行解析,並且最終渲染出頁面。
將 JSON 劃分為三個層級

1. 專案級

專案級的 JSON 主要是介紹專案的基本資訊

{  id: '', // 專案唯一標誌  name: '', // 專案名稱  label: [], // 專案標籤  description: '', // 專案描述  author: '', // 作者  pages: [] // 頁面}

2. 頁面級

頁面級的 JSON 可以理解為我們的一個 html 頁面,裡面包括頁面指令碼,頁面樣式、元素等

{  id: '', // 頁面唯一標誌  type: '', // 頁面型別  route: '', // 頁面路由  title: '', // 頁面標題  style: { // 頁面根元素的樣式    width: '',    height: '',    ......  },  elements: [], // 元件  plugins: [], // 頁面外掛服務}

3. 元件級

元件是組成頁面的核心部分,可以理解為頁面都是由一個個具有特定功能的積木堆積而成。

{  id: '', // 元件唯一標誌  elementName: '', // 元件名  style: { // 元件通用樣式    position: '',    width: '',    height: '',    border: ''  },  props: {}, // 元件屬性引數  children: [], // 子元件  ......}

其中值得一提的是,props 可以理解 vue 裡的 props,通過給元件傳入 props,元件可以根據引數給於相對應的表現,相信學習過 vue 的元件相關知識的人一定理解。

最後一個完整的專案的 JSON 長這樣

{  id: '_clv_fPQu',  name: 'FAB頁',  label: ['fab', '遊戲'],  description: 'Feature、Advantage和Benefit,按照這樣的順序來介紹,就是說服性演講的結構,它達到的效果就是讓客戶相信你的是最好的',  author: 'lujintao',  pages: [    {      id: '_Tmy_CdpY',      type: 'pc',      route: 'fab',      title: '遊戲頁',      style: {        width: '750px',        height: '1334px',        position: 'relative',        margin: '0 auto',        padding: '',        overflow: '',        backgroundColor: '',        backgroundImage: '',        backgroundSize: '',        backgroundRepeat: '',        backgroundPosition: ''      },      elements: [        {          id: '_aIv_SesL',          elementName: 'aicc-image',          style: {            position: 'absolute',            width: '100px',            height: '100px',            border: '1px solid #eeeeee'          },          props: {            src: '***/test1.png'          },          children: [],        }      ],      plugins: [        {          name: 'mobile',          state: true        },        {          name: 'weixinShare',          state: true,          data: {            title: '微信分享的標題',            description: '微信分享的文案',            img: '***/wxshare.png'          }        }      ],    }  ]}

二、元件庫

作為頁面的積木,元件是十分重要的部分。元件除了需要有像文字元件、圖片元件、視訊元件等一些基礎的功能,還需要一些貼近業務的元件。比如在官網製作中經常可以看到的輪播圖元件,亦或者導航欄元件等等,這些元件都需要結合業務進行提煉出通用能力,才能方便使用者快速搭建一個完整頁面的。
搭建元件庫需要開發兩份程式碼,一份是元件本身,一份是用來配置元件props的配置面板。

1. 元件

元件的作用有兩個,一個是提供給編輯器,在畫板上能夠展示元件,另外一個作用則是將元件打包後提供給服務端作為指令碼插入。
比如寫一個視訊元件,程式碼很簡單,定義好 props 就可以了

<template>    <video :src="src" :autoplay="autoplay" /></template><script>export default {  name: 'aicc-video',  props: {    src: {      type: String,      default: ''    },    autoplay: {      type: String,      default: ''    }  }}</script>
  • 在編輯器裡的引入方式
import AICCVideo from '@/components/AICCVideo'
  • 在服務端的引入方式,在瀏覽器環境下,元件是已經被打包好作為script指令碼插入。
<script src="https:**/**/aiccvideo.min.js">

2. 元件配置面板

跟元件需要應用到服務端不同,元件配置面板所要做的事情就是生產出自定義資料(JSON Scheme),因此只需要提供給編輯器註冊。
還是拿上面的視訊元件舉例

<template>  <div class="props-video">    <input v-model="propsValue.src" />    <input v-model="propsValue.autoplay" />  </div></template><script>export default {  name: 'props-video',  props: {    value: { // 配置面板在編輯器裡掛載的時候是通過v-model="props"傳入的      type: Object,      default: () => {}    }  },  data() {    propsValue: {}  },  watch: {    value(val) { this.propsValue = val },    // 配置面板更新的時候,子傳父    propsValue(val) { this.$emit('input', val) }  }}</script>

元件配置面板在編輯器的引入方式和元件的引入一致,只需要 require 進來註冊掛載到 component 元件上就好了,同時 v-model 綁定當前元件的 props 資料。

三、編輯器

編輯器作為使用者使用的唯一視窗,在幾個板塊裡顯得重中之重。我們已經知道,整個專案的基石是第一點所提到的 JSON Schema。編輯器也不例外,編輯器本質上做的事情就是修改/新增/刪除這段 JSON 資料的各種引數。

1. 如何維護資料

在編輯器中,只需要維護一份 JSON 資料,所以考慮使用 vuex 去管理專案資料。根據 vuex 官方描述,十分符合編輯器這種中大型的專案

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化

實現過程

  1. 寫一個 name 為 editor 的 store

    const state = {  projectData: {}, // 工程專案資料  activePageUUID: '', // 當前正在編輯的頁面  activeElementUUID: '', //當前被選中的元件UUID}
    
    // 初始化專案export function initProject({ commit, state }, data) { let projectData = data if (!data) projectData = initProject() // 空專案時生成預設 JSON Data dispatch('setActivePageUUID', projectData.pages[0].uuid)}// 設定當前被選中的頁面的UUIDexport function setActivePageUUID({ commit }, uuid) {  state.activePageUUID = uuid}// 設定當前被選中的元素的UUIDexport function setActiveElementUUID({ commit }, uuid) {  state.activeElementUUID = uuid}
    
    // 當前選中頁面的 JSON 資料export function activePage(state) {  const idx = state.projectData.pages.findIndex(p => { return state.activePageUUID === p.uuid })  return state.projectData.pages[idx]}// 當前選中元件的 JSON 資料export function activeElement(state) {  const pIdx = state.projectData.pages.findIndex(p => { return state.activePageUUID === p.uuid })  const activePage = state.projectData.pages[pIdx]  const eIdx = activePage.elements.findIndex(e => { return state.activeElementUUID === e.uuid })}
    
  • getter.js

  • action.js

  • state.js: 儲存要維護的資料

  1. 編輯器裡各頁面繫結資料,通過 mapGetters 獲取當前頁面,當前元素,以及專案資料
<template>  <div>    <component :is="activeElement.elementName" v-model="activeElement.props" />  </div></template> <script>import { mapGetters, mapState } from 'vuex'export default {  computed: {    ...mapGetters({      'activeElement': 'editor/activeElement'    }),    ...mapState({      pages: state => state.editor.projectData.pages,      activePageUUID: state => state.editor.activePageUUID,      activeElementUUID: state => state.editor.activeElementUUID,    })  }}</script>

2. 如何實現畫布拖拽

對於視覺化編輯器來說,使用者希望可以能夠拖拽畫布中的元素進行更改位置。實現思路十分簡單

  • 選中元素,監聽 mousedown 事件

  • 獲取當前按下元素的 offsetTop 和 offsetLeft

  • 獲取當前按下滑鼠的座標位置(e.clientX, e.cliengY)

  • 監聽 mousemove 事件,獲取滑鼠移動的長度,計算出距離

  • 元素的新座標 = 距離 + 按下時元素的 offset 資訊

程式碼如下

function mousedown(e) {  let newTop = null  let newLeft = null  // 記錄按下時當前元素位置  const cTop = e.currentTarget.offsetTop  const cLeft = e.currentTarget.offsetLeft  // 記錄按下時當前滑鼠位置  const mouseX = e.clientX  const mouseY = e.clientY    const move = mEvent => {    // 只是單純移動位置,不需要傳遞事件給後代    mEvent.stopPropagation()    mEvent.preventDefault()    const cX = mEvent.clientX    const cY = mEvent.clientY    // 移動的位置    const distanceX = cX - mouseX    const distanceY = cY - mouseY    // 新座標    newTop = distanceX + cTop    newLeft = distanceY + cLeft  }  const up = () => {    document.removeEventListener('mousemove', move, true)    document.removeEventListener('mouseup', up, true)  }  document.addEventListener('mousemove', move, true)  document.addEventListener('mouseup', up, true)}

這裡其實有優化的空間,在元素進行拖拽的時候,如果實時去改變top,left時,會引起重排。解決辦法也很簡單,在 mousemove 的過程中我們使用 transform 去實時顯示當前元件的位置,等 mouseup 釋放滑鼠的時候,我們再把真實的座標位置賦值給元件的 Style 裡。

四、服務端渲染

我們在前面編輯器裡生產出來的專案 JSON 資料,最終需要讓服務端這邊進行 DSL 解析。由於我們採納的技術棧是 Vue,想要生成一個結構化的頁面也十分容易,使用 vue 的 render 函式。
具體可以參考官方文件:渲染函式 & jSX
拿第一部分 JSON Schema 的示例,渲染步驟大致如下:

  • 拿到頁面的 page 資訊生成頁面的 title/seo 等頁面配置資訊,根據路由生成對應的 ${name}.html 檔案

  • 掛載打包好的元件庫 <script src="*/*/aiccvideo.cdn.js">

  • 由於使用的render函式,不需要編譯器,只需要掛載 vue.runtime.js 指令碼即可

  • 使用模板引擎進行字串替換,替換的資訊有頁面資訊、render 函式裡的元件陣列等

  • 遍歷 elements,用 vue 的 render 函式 createElement('元件名', { props: { ...生產出來的element JSON資料 }})

  • 解析 style 的 JSON 資料,生成 選擇器{ ${key}: ${value} }的樣式表

  • 寫入檔案fs.writeFileSync(檔案路徑,htmlString)

結語

文章到這就差不多結束了,因為只是簡單說明一下整個專案怎麼搭建,在具體細節裡沒有太詳盡描述。要寫下來,每一塊都可以作為一個單元去寫。比如

  • 元件庫如何打包

  • 專案的管理維護,怎麼使用 lerna 管理專案

  • 如何實現拖拽元素實時改變元素大小

  • 如何保證提交資料的格式符合要求

  • 如何實現外掛化服務,如微信分享等

……
要上線一個完整能夠生產的產品,細節的地方還有很多技術點可以討論,在這裡僅僅只是談及整個專案技術的組成部分和基本實現原理。
歡迎有更好的想法~