對於互聯網公司而言,活動運營頁面發佈頻率高,每次開發又要人力成本不划算。所以須要有一套平臺系統來知足運營產品自我快速建站。此類軟件產品最先追溯到QQ空間,凡客建站等。html
此項目旨在用最少的代碼實現可視化搭建、發佈、預覽、調試等核心功能。並對每個關鍵原理進行說明。
在此以前,但願你對Javascript(ES2015,React hooks)、nodejs、webpack等基礎知識有所瞭解,即可輕鬆設計開發屬於你的建站平臺(也許只須要你花費一週的時間ヽ( ̄▽ ̄)ノ)。前端
首先咱們來看看效果:
node
主要劃分爲4個部分:編輯器、預覽頁、服務端、組件倉庫
用戶在編輯器內拖入組件倉庫已開發的組件,設置樣式與自定義屬性,爲頁面生成JSON配置,將配置提交服務端保存。預覽頁請求返回配置,預覽頁根據配置動態下載組件文件並渲染。
react
不論在編輯器內仍是預覽頁,頁面都是根據JSON配置來遞歸渲染webpack
{ "name": "View", "style": { "position": "relative", "width": "1089px", "height": "820px" }, "props": { "lazy": true }, "el": "wc12", "children": [ //{ ...} ] }
這是一個簡單的佈局組件所映射的JSON結構,其中包括了該組件的樣式,傳入組件的props屬性,以及其惟一的key(el)值,還有他的子組件children數組,數組裏的內容就是其包裹組件的JSON結構。經過這樣的嵌套關係,能夠將其映射成組件樹。
git
當頁面JSON配置發生變化時,依靠react單向數據流會從新渲染,此時咱們須要一個通用方法,來遞歸的建立組件的佔位DIV,可是須要注意的是,首次建立的只是一個空殼,return的子組件爲null。
與此同時咱們調用異步加載組件js的方法,等該組件下載好後自動注入到這個殼裏。這個方法的特色在於,咱們每次加載新的組件會優先從window.comp
下找是否有已緩存的組件對象,若是爲undefined
,說明這是一個全新的組件,就請求對應的JS下載,而且將window.comp
下的這個組件標識爲正在請求的Promise
,這樣若是相同組件併發調用此方法,會awati
同一個Promise
不會重複請求,並且組件緩存後也能夠直接用await
拿到組件對象。
github
經過上述的幾個方法,咱們已經可以將JSON配置渲染爲頁面DOM,而且動態加載組件JS文件了~web
既然咱們的頁面是根據JSON配置來渲染的,那麼對頁面任何的增刪改查,均可以抽象爲對JSON樹內某個節點的數據結構修改。咱們須要一個通用的搜索方法,來搜索JSON樹,並傳入一個標識,來指明此次操做的類型。searchTree
是全部操做的通用方法,本質上是對JSON配置樹的BFS搜索,只要找到對應的key
節點,根據EnumEdit
中的枚舉類型操做數據後返回修改後的結果樹,dispatch
新的配置樹通知react從新渲染。數據庫
除此以外,具體操做這裏涉及到各類鍵盤、鼠標事件的綁定,這部分暫不作贅述,可自行查詢MDN文檔。express
咱們在右側編輯區填寫的內容,都會在渲染時注入到對應的組件裏,樣式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
根據指引操做