前言
對於網際網路公司而言,活動運營頁面釋出頻率高,每次開發又要人力成本不划算。因此需要有一套平臺系統來滿足運營產品自我快速建站。此類軟體產品最早追溯到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結構。通過這樣的巢狀關係,可以將其對映成元件樹。
當頁面JSON配置發生變化時,依靠react單向資料流會重新渲染,此時我們需要一個通用方法,來遞迴的建立元件的佔位DIV,但是需要注意的是,首次建立的只是一個空殼,return的子元件為null。
與此同時我們呼叫非同步載入元件js的方法,等該元件下載好後自動注入到這個殼裡。這個方法的特點在於,我們每次載入新的元件會優先從window.comp
下找是否有已快取的元件物件,如果為undefined
,說明這是一個全新的元件,就請求對應的JS下載,並且將window.comp
下的這個元件標識為正在請求的Promise
,這樣如果相同元件併發呼叫此方法,會awati
同一個Promise
不會重複請求,而且元件快取後也可以直接用await
拿到元件物件。
通過上述的幾個方法,我們已經能夠將JSON配置渲染為頁面DOM,並且動態載入元件JS檔案了~
二、編輯器內的操作
既然我們的頁面是根據JSON配置來渲染的,那麼對頁面任何的增刪改查,都可以抽象為對JSON樹內某個節點的資料結構修改。我們需要一個通用的搜尋方法,來搜尋JSON樹,並傳入一個標識,來指明這次操作的型別。searchTree
是所有操作的通用方法,本質上是對JSON配置樹的BFS搜尋,只要找到對應的key
節點,根據EnumEdit
中的列舉型別運算元據後返回修改後的結果樹,dispatch
新的配置樹通知react重新渲染。
除此之外,具體操作這裡涉及到各種鍵盤、滑鼠事件的繫結,這部分暫不做贅述,可自行查詢MDN文件。
三、樣式、自定義屬性的注入
我們在右側編輯區填寫的內容,都會在渲染時注入到對應的元件裡,樣式style
會注入在包裹元件的殼裡,自定義屬性會當做prop
傳入子元件,在元件開發中,我們可以從props
中拿到編輯器內填寫的屬性值。
四、編輯歷史記錄管理
歷史記錄為一個佇列的資料結構,如果我們儲存1000條記錄,每修改一次JSON配置,就將其入隊,每次入隊時發現記錄大於1000,就將佇列頭部拋棄。
當前頁面顯示的配置為一個指標,指向佇列中某條記錄,撤銷就指標後移,恢復就指標前移。每次觸發compile時,將新的配置樹計入佇列,不需要手動記錄。利用hooks自帶的快取機制非常容易實現。
五、畫布的縮放處理
在搭建使用程中,我們寄希望於畫布設計尺寸永遠為1920(移動端則為750),但是視口顯然沒那麼大,所以我們要將畫布以左上角為縮放焦點transform-origin: 0 0
,拖動導航slide或按下空格利用滾輪縮放。這個過程中不斷改變transform: scale
來重新整理檢視。
需要注意的是,scale的改變為瀏覽器重繪,並不會改變原有的DOM佔位尺寸,因此縮小畫布會有很大的空白區域,為了解決這個問題,我們需要在畫布外再包一層div,每次畫布改變縮放後,利用getBoundingClientRect()
來獲取縮放後畫布實際的寬高,並將這個數值定義在外層div上,外層div設定為overflow: hidden
,這樣視窗滾動的距離就會依據外層容器來出滾動條。
畫布的高度計算時,要計算出一個min-height
,為當前搭建區域的offsetHeight
,保證畫布內沒有元件撐開時,也能夠鋪滿一個螢幕。
此外對畫布根節點要設定一個padding-bottom: 300px
,作用是保證底部永遠有一個空白區域,能夠讓搭建者拖入新的元件到根節點下。
六、元件的開發
每一個元件都的固有結構,index.js
,config.json
是必須存在的(服務層會根據此檔案構建,稍後會提到):
入口檔案即為業務程式碼,配置為一個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檔案,那麼我們就需要在頁面生成前將當前所有元件都構建好。
這裡首先找出倉庫中的元件,加入打包的entry
入口,然後利用webpack的library
,libraryTarget
配置,將元件打包到window[name]
下,name為元件名(比如Image,View),我們來看看打包後的元件程式碼:
果不其然 ,元件js下載執行後直接被掛載到window下了,此時此刻你可以回頭看開頭提到的loadAsync
載入元件的方法,是否恍然大悟了呢。
這裡你可能又發現一個問題,元件都依賴react
庫,那每個元件單獨打包,豈不是都要載入一遍,那包得多大?
從JS體積可以看出,實際上根本沒有打包這些通用庫,只包含了業務程式碼而已。這裡同樣利用webpack的externals
屬性,可以指定某些依賴直接從window下取:
那麼是什麼時候將react
注入window下的呢?
在編輯器或者預覽頁面,載入全域性配置,也就是SDK初始化之前,就將元件所依賴的全域性物件注入好了,這樣後續元件非同步下載後就可以直接執行。
關於編輯器和預覽頁的打包不做特別說明,就是普通的webpack
配置打包,記得抽出公共模組就好。
七、元件的代理除錯
平臺開發好了,這個時候我們要往裡開發業務元件了,那麼如何除錯呢。
通過npm run dev:comp debug=XXX,YYY
命令(XXX為元件名)來執行除錯指令碼
指令碼首先通過process.argv
傳入的引數獲取要除錯的元件
然後使用node API來呼叫webpack-dev-server
需要注意的是,這裡僅僅是在本地建立了元件的代理,還需要在元件資源載入上區分哪些元件需要請求本地除錯地址,詳情可見上方loadAsync
方法,我們通過在預覽頁和編輯器後方加入debug_comp=XXX
引數來告訴此方法該元件要請求本地除錯地址
最後記得如果當前使用者請求的URL是除錯模式,在node express服務的ejs
模板介面裡加上webpack-dev-server
的程式碼script標籤,
八、服務端對頁面配置的管理
因為此專案為演示專案,並沒有對頁面配置用id進行區分,每次提交都是存取同一個配置檔案page.json
生產環境下需要連線資料庫,將每一份配置生產一個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
根據指引操作