帶你7天玩轉視覺化建站平臺

心動音符發表於2020-05-18

前言


對於網際網路公司而言,活動運營頁面釋出頻率高,每次開發又要人力成本不划算。因此需要有一套平臺系統來滿足運營產品自我快速建站。此類軟體產品最早追溯到QQ空間,凡客建站等。

此專案旨在用最少的程式碼實現視覺化搭建、釋出、預覽、除錯等核心功能。並對每一個關鍵原理進行說明。
在此之前,希望你對Javascript(ES2015,React hooks)、nodejs、webpack等基礎知識有所瞭解,便可輕鬆設計開發屬於你的建站平臺(也許只需要你花費一週的時間ヽ( ̄▽ ̄)ノ)。

首先我們來看看效果:
視覺化編輯器皮膚

目前支援的功能

編輯器相關:
  • 拖拽選單元件放入畫布,能放置的位置標綠框,不能放的標紅框;選中錨點、移入高亮。
  • 滑鼠按住拖動畫布內元件四周改變寬高,拖動中心改變定位
  • 屬性皮膚輸入樣式、自定義屬性配置,實時更新畫布預覽
  • 頁面編輯快捷鍵操作,包括:儲存(Ctrl+S),撤銷(Ctrl+Z),恢復(Ctrl+Y),刪除(DEL),複製(Ctrl+C),剪下(Ctrl+X),貼上(Ctr+V),上移節點(↑),下移節點(↓),縮放移動畫布(空格按下+左鍵拖動+滾輪縮放)
  • 直接拖動左下方樹結構批量移動節點
服務層相關:
  • 提供平臺前端頁面(編輯器、預覽頁)的請求介面與路由模板
  • 打包構建:對元件倉庫的分包,對編輯器SDK的打包
  • 開發元件除錯模式的命令列指令碼
預覽相關:
  • 將頁面搭建配置建立React元件樹,動態載入所需元件JS檔案
  • 元件懶載入功能

實現原理


主要劃分為4個部分:編輯器、預覽頁、服務端、元件倉庫
使用者在編輯器內拖入元件倉庫已開發的元件,設定樣式與自定義屬性,為頁面生成JSON配置,將配置提交服務端儲存。預覽頁請求返回配置,預覽頁根據配置動態下載元件檔案並渲染。
整體流程

一、頁面JSON配置與渲染的關係

不論在編輯器內還是預覽頁,頁面都是根據JSON配置來遞迴渲染

{
        "name": "View",
        "style": {
          "position": "relative",
          "width": "1089px",
          "height": "820px"
        },
        "props": {
          "lazy": true
        },
        "el": "wc12",
        "children": [
             //{ ...}
        ]
  }

這是一個簡單的佈局元件所對映的JSON結構,其中包括了該元件的樣式,傳入元件的props屬性,以及其唯一的key(el)值,還有他的子元件children陣列,陣列裡的內容就是其包裹元件的JSON結構。通過這樣的巢狀關係,可以將其對映成元件樹。
compile.js

當頁面JSON配置發生變化時,依靠react單向資料流會重新渲染,此時我們需要一個通用方法,來遞迴的建立元件的佔位DIV,但是需要注意的是,首次建立的只是一個空殼,return的子元件為null。
global.js
與此同時我們呼叫非同步載入元件js的方法,等該元件下載好後自動注入到這個殼裡。這個方法的特點在於,我們每次載入新的元件會優先從window.comp下找是否有已快取的元件物件,如果為undefined,說明這是一個全新的元件,就請求對應的JS下載,並且將window.comp下的這個元件標識為正在請求的Promise,這樣如果相同元件併發呼叫此方法,會awati同一個Promise不會重複請求,而且元件快取後也可以直接用await拿到元件物件。
compile.js -> CompBox

通過上述的幾個方法,我們已經能夠將JSON配置渲染為頁面DOM,並且動態載入元件JS檔案了~

二、編輯器內的操作

既然我們的頁面是根據JSON配置來渲染的,那麼對頁面任何的增刪改查,都可以抽象為對JSON樹內某個節點的資料結構修改。我們需要一個通用的搜尋方法,來搜尋JSON樹,並傳入一個標識,來指明這次操作的型別

common.js
searchTree是所有操作的通用方法,本質上是對JSON配置樹的BFS搜尋,只要找到對應的key節點,根據EnumEdit中的列舉型別運算元據後返回修改後的結果樹,dispatch新的配置樹通知react重新渲染。

除此之外,具體操作這裡涉及到各種鍵盤、滑鼠事件的繫結,這部分暫不做贅述,可自行查詢MDN文件。

三、樣式、自定義屬性的注入

我們在右側編輯區填寫的內容,都會在渲染時注入到對應的元件裡,樣式style會注入在包裹元件的殼裡,自定義屬性會當做prop傳入子元件,在元件開發中,我們可以從props中拿到編輯器內填寫的屬性值。
compile.js -> CompBox

四、編輯歷史記錄管理

歷史記錄為一個佇列的資料結構,如果我們儲存1000條記錄,每修改一次JSON配置,就將其入隊,每次入隊時發現記錄大於1000,就將佇列頭部拋棄。
當前頁面顯示的配置為一個指標,指向佇列中某條記錄,撤銷就指標後移,恢復就指標前移。每次觸發compile時,將新的配置樹計入佇列,不需要手動記錄。利用hooks自帶的快取機制非常容易實現。

record.js

五、畫布的縮放處理

在搭建使用程中,我們寄希望於畫布設計尺寸永遠為1920(移動端則為750),但是視口顯然沒那麼大,所以我們要將畫布以左上角為縮放焦點transform-origin: 0 0,拖動導航slide或按下空格利用滾輪縮放。這個過程中不斷改變transform: scale來重新整理檢視。
需要注意的是,scale的改變為瀏覽器重繪,並不會改變原有的DOM佔位尺寸,因此縮小畫布會有很大的空白區域,為了解決這個問題,我們需要在畫布外再包一層div,每次畫布改變縮放後,利用getBoundingClientRect()來獲取縮放後畫布實際的寬高,並將這個數值定義在外層div上,外層div設定為overflow: hidden,這樣視窗滾動的距離就會依據外層容器來出滾動條。

畫布的高度計算時,要計算出一個min-height,為當前搭建區域的offsetHeight,保證畫布內沒有元件撐開時,也能夠鋪滿一個螢幕。

此外對畫布根節點要設定一個padding-bottom: 300px,作用是保證底部永遠有一個空白區域,能夠讓搭建者拖入新的元件到根節點下。

六、元件的開發

每一個元件都的固有結構,index.jsconfig.json是必須存在的(服務層會根據此檔案構建,稍後會提到):
comp/Image
入口檔案即為業務程式碼,配置為一個JSON檔案,決定了編輯器內所能編輯的自定義選項:

{
    "name": "圖片",
    "staticProps": [
        {
            "name": "點選連結",
            "prop": "link",
            "size": "long"
        },
        {
            "name": "是否在新視窗開啟連結",
            "prop": "blank",
            "type": "switch",      // 配置型別,目前支援`text`預設,`select`,`switch`,`color`
            "size": "long"          // 配置是否佔滿編輯器一行
        },
        {
            "name": "圖片地址",
            "prop": "src",
            "size": "long"
        }
    ],
    "defaultStyles": {        // 拖入元件到畫布時預設的樣式
        "position": "relative",
        "width": "180px",
        "height": "180px",
        "marginTop": "0px"
    },
    "defaultProps": {      // 拖入元件到畫布時預設的自定義屬性
        "src": "http://r.photo.store.qq.com/psb?/V14dALyK4PrHuj/h50SMf97hSy.BJlJw31fagrw.NUaJD83gvydmoGN77w!/r/dLgAAAAAAAAA",
        "blank": true
    },
    "hasChild": false,        // 是否允許有子元件,如果不允許拖拽的時候移入會標紅,提示當前節點不能被注入
    "canResizeByMouse": true       // 是否允許通過拖動九宮格蒙版來修改元件的寬高位置
}

上述這樣的一個圖片元件,在編輯器內對應的配置項即為:

七、元件的構建打包

這是構建階段非常重要的一環,我們上面說過,每一個元件對應一個JS檔案,那麼我們就需要在頁面生成前將當前所有元件都構建好。
webpack.config.comp.js
這裡首先找出倉庫中的元件,加入打包的entry入口,然後利用webpack的library,libraryTarget配置,將元件打包到window[name]下,name為元件名(比如Image,View),我們來看看打包後的元件程式碼:

果不其然 ,元件js下載執行後直接被掛載到window下了,此時此刻你可以回頭看開頭提到的loadAsync載入元件的方法,是否恍然大悟了呢。
這裡你可能又發現一個問題,元件都依賴react庫,那每個元件單獨打包,豈不是都要載入一遍,那包得多大?

從JS體積可以看出,實際上根本沒有打包這些通用庫,只包含了業務程式碼而已。這裡同樣利用webpack的externals屬性,可以指定某些依賴直接從window下取:
webpack.config.comp.js
那麼是什麼時候將react注入window下的呢?
global.js
在編輯器或者預覽頁面,載入全域性配置,也就是SDK初始化之前,就將元件所依賴的全域性物件注入好了,這樣後續元件非同步下載後就可以直接執行。
關於編輯器和預覽頁的打包不做特別說明,就是普通的webpack配置打包,記得抽出公共模組就好。

七、元件的代理除錯

平臺開發好了,這個時候我們要往裡開發業務元件了,那麼如何除錯呢。
通過npm run dev:comp debug=XXX,YYY命令(XXX為元件名)來執行除錯指令碼

指令碼首先通過process.argv傳入的引數獲取要除錯的元件
debugComp.js
然後使用node API來呼叫webpack-dev-server
需要注意的是,這裡僅僅是在本地建立了元件的代理,還需要在元件資源載入上區分哪些元件需要請求本地除錯地址,詳情可見上方loadAsync方法,我們通過在預覽頁和編輯器後方加入debug_comp=XXX引數來告訴此方法該元件要請求本地除錯地址
server/index.js
最後記得如果當前使用者請求的URL是除錯模式,在node express服務的ejs模板介面裡加上webpack-dev-server的程式碼script標籤,

八、服務端對頁面配置的管理

因為此專案為演示專案,並沒有對頁面配置用id進行區分,每次提交都是存取同一個配置檔案page.json
opPageJSON.js
生產環境下需要連線資料庫,將每一份配置生產一個ID,在開啟編輯時取對應的請求ID返回配置。
要額外注意的一點,我們在返回配置介面資料時,要去搜尋當前構建資料夾中存在的js與雜湊值的對映,這樣保證前端頁面能正確的載入最新構建的js地址

專案結構


├─config.js            // 前後端通用配置
├─comp                // 元件倉庫
│  ├─Image                   // 元件名
│  │      config.json            // 元件配置    
│  │      index.js                // 元件入口
│  │      index.less            // 元件樣式
│  │      
│  ├─Text
│  │    ...
│  │      
│  └─View
│       ...
│          
├─script                // 配置指令碼
│      debugComp.js                    // 元件除錯指令碼
│      webpack.config.comp.js        // 元件打包配置
│      webpack.config.edit.js        // 編輯器打包配置
│      webpack.config.page.js        // 預覽頁打包配置
│      
├─server                // 建站平臺服務端
│  │  getCompUrlHook.js        // 生成元件js檔案雜湊對映
│  │  getCompJSONconfig.js        // 查詢元件倉庫內當前所有存在的元件配置
│  │  index.js            // 服務端總入口 
│  │  opPageJSON.js        // 存取頁面對應的配置JSON樹
│  │  
│  └─template            // 模板
│          index.ejs            // html渲染模板
│          page.json            // 頁面配置JSON樹
│          
└─src                // 建站平臺前端SDK
    │  context.js            // 全域性狀態物件
    │  global.js            // 全域性配置依賴
    │  reducer.js            // 全域性狀態管理
    │  
    ├─edit                // 編輯器
    │  │  compile.js        // 編譯配置樹為元件樹
    │  │  board.js            // 編輯器可視區域皮膚
    │  │  index.js            // 編輯器總入口
    │  │  menu.js            // 編輯器元件選單
    │  │  option.js            // 編輯器屬性操作皮膚
    │  │  record.js            // 操作歷史記錄管理
    │  │  tree.js            // 搭建樹層級展示
    │  │  search.js        // 搜尋頁面配置樹方法
    │  │  
    │  └─style            // 編輯器樣式
    │          
    └─page            // 預覽頁
        compile.js    // 渲染元件配置
        index.js        // 預覽頁總入口

結語


此專案對視覺化建站的整體前後端流程有一個完整實現。基於此基礎上,可以根據需要擴充定製化的編輯器功能、頁面渲染功能等。

因篇幅原因文中縮減了很多程式碼片段,但是本身程式碼量也不多,我儘可能在每一個方法都標有詳細的註釋說明。更多詳情可以下載此專案,直接npm start根據指引操作

專案地址:https://github.com/yukilzw/web_channel

相關文章