jquery那個年代過來的人,組件化帶來的感覺是很是讚的,你們都知道,react項目是由不少小的組件組合構成的,小組件又組合成大組件,大組件組合成項目級別的組件,那組件也有大小,有生命週期,那麼下面的 <App />
也是一個組件。javascript
render(<App />, document.getElementById('App'))
這裏咱們所指的組件是:可複用的組件,是經過 npm install xxx
安裝使用的組件。css
此次分享的項目是h5ds
,一個超級大的react組件,能夠經過npm install h5ds
安裝使用這個組件。目前來看,這應該是目前最大的React組件了吧。html
H5DS (HTML5 design software)能夠理解成一款作H5的在線工具,H5就是在手機上滑動的頁面,像易企秀,百度H5,wps秀堂,Maka同樣的在線工具。前端
最初我只是想作一艘獨木舟,作着作着,感受加個帆要好點,而後就一發不可收拾,不停的添加新的功能,獨木舟變成了大船,最終變成了一艘戰艦。vue
任何項目只要一進入迭代週期。哪怕是一個很小的項目,最終也可能成長成爲一個龐大的項目,因此不要低估本身處理問題的能力,只要放手去幹,總會有驚喜。java
工具截圖示例:node
製做的H5示例:mysql
接下來會講解我開發這個項目的所有過程和技術方案。react
技術選型原則:儘可能以節約開發量爲主,不重複造輪子,咱們是爲了生產一個成品,不是原料加工廠,有現成的組件就用上,有節約開發工做量的框架也用上。
【前端:】jquery
React
: 模塊化開發少不了,angular,react,vue三選一,這裏選擇了react。
Jquery
:雖然不少同窗說這個已經淘汰了,不過對於小團隊開發而言,開發資源是很是寶貴的,能一句代碼解決的問題用兩句代碼來解決就是浪費研發資源。固然使用這個還有另一個更重要的緣由,爲了支持海量的jquery插件,咱們犧牲了一部分性能。
mobx
: 從技術角度,通過咱們分析的最佳方案並非mobx,而是redux,redux更適合這樣的工具項目,可是考慮到代碼量和二次開發的成本,我最終選擇了mobx,對於使用過vuex的用戶而言,mobx也方便掌握,也很容易從vue轉到react項目中來。
less
: 沒有選擇sass的緣由主要是由於node-sass包不是很穩定,常常出錯,固然less已經能知足咱們的需求了。
antd
:咱們不造輪子,這樣的工具項目對組件的需求是很是大的,有現成的優秀的react組件庫固然要用起來。沒有的本身再封裝一些就能夠了。
loadsh
:工具類項目對數據的處理是很是多的,這裏用到了loadsh裏面的一些方法去處理數據。
【後端:】
koa
:後端語言採用nodejs,koa文檔和學習資料也比較多,express原班人馬打造,這個正合適。
mysql
:免費的關係型數據庫,這個算是比較常規的了。
Sequelize
:Sequelize是一款基於Nodejs功能強大的異步ORM框架,既然有現成的,咱們也不本身去封裝了。工做量能節約就節約。重心放到業務上。
需求用一句話歸納:編輯器生成H5頁面,能夠在手機端打開。
那麼編輯器和H5頁面應該是分開的兩個項目,H5裏面有不少模塊模塊(plugins插件),好比圖片,文本,形狀,視頻,音樂等,後續可能還會新增其餘插件,因此這塊業務就必須是可擴展的。由於咱們採用mobx管理數據,數據咱們採用json數據,這裏咱們至少有三種方案:
優勢:服務端直接返回HTML頁面,能夠作SEO缺點:插件須要在服務端加載使用,脫離服務端將無法跑起來,若是訪問量大,由於服務器壓力比較大。
優勢:服務端直接返還HTML頁面,能夠作SEO,服務端訪問壓力小,可脫離服務端獨立運行缺點:數據不夠靈活,只能經過編輯器修改後從新發布才能夠,後續升級預覽頁面的代碼無法作到同步升級。
優勢:服務端壓力小,可脫離服務端獨立運行,數據靈活,能實現模版同步升級缺點:不支持SEO
綜合評估,咱們選擇了第三種方案,預覽頁面直接依賴插件,優勢明顯,後續可升級的空間大。若是要作SEO,也能夠作SSR,可是目前對SEO的需求不是很大,由於主要是微信H5爲主。
架構方案肯定後,數據結構也是很是重要的,提供H5頁面需求來看,數據結構大體是這樣的:
{ ...infos, // 記錄H5的信息,名稱,主圖,描述 ...options, // 記錄H5的配置信息,滑動效果,類型等 pages: [ // 記錄頁面數據 { ...infos, // 頁面信息 ...options, // 頁面配置 layers: [...] // 記錄頁面的圖層信息 } ] }
總體看上去數據結構就很是清晰了,圖層和頁面的概念也是H5的核心。
一個頁面由多個圖層構成,而圖層又有多個種類(圖片,文本,形狀,視頻,音樂),也就是以前說的可擴展插件(plugins插件)
實際上數據結構是很是複雜的,咱們的數據以下:
h5ds json數據結構v1.0版本
appJSON數據結構:
{ version: 5.0.0, // 當前的H5DS版本 img: 'http://cdn.h5ds.cn/static/images/img-null.png', // 主圖 desc: '點石H5,官方網站h5ds.cn', // 描述信息 name: '點石H5', // 應用名稱 type: 'phone', // h5類型 phone or pc slider: { // 全局的翻頁設置 speed: 0.5, // 切換速度 effect: 'slide', // 翻頁動畫 slide, fade, coverflow autoplay: false, // 是否自動翻頁 time: 5 // 自動翻頁時間 }, style: { // app的樣式 width, height }, fixeds: [ // 浮動層有兩個,不可刪除和添加。上層浮動和下層浮動 { id: null, // div.id className: null, // div.class keyid: util.randomID(), // keyid 是一個不重複的隨機數,至關因而id name: '浮動層上', // 名稱 desc: '頁面描述', style: { // 浮動層的樣式 height, width }, layers: [ layerJSON ] // 浮動層中的layer集合 }, { id: null, className: null, keyid: util.randomID(), name: '浮動層下', desc: '頁面描述', style: { height, width }, layers: [] } ], popups: [ pageJSON ], // 彈窗數據集合, pages: [ // 頁面數據集合 { id: null, className: null, keyid: util.randomID(), name: '頁面名稱', desc: '頁面描述', style: { height, width }, // 頁面樣式,background-color樣式會單獨在外層div渲染 layers: [ layerJSON ], // 當前頁面的圖層 slider: { // 當前頁面的翻頁設置 autoplay: false, lock: false, time: 5 } } ] }
pageJSON 數聽說明:
{ id: null, // 頁面id className: null, // 頁面 class keyid: util.randomID(), // 惟一標識 name: '頁面名稱', desc: '頁面描述', style: { height, width }, // 頁面樣式,background-color樣式會單獨在外層div渲染 layers: [ layerJSON ], // 當前頁面的圖層 slider: { // 當前頁面的翻頁設置 autoplay: false, lock: false, time: 5 } }
layerJSON 數聽說明:
// 每一個layerJSON數據是不同的,他們都遵循必定的規則,data參數是不同的 { version: '1.0.0', // 插件版本號 name: '地圖插件', // 插件名稱 pid: 'h5ds_map', // 插件的id id: null, // 圖層的id className: null, // 圖層的class set: { hide, lock, lockWideHigh, noEvent }, // 鎖定,隱藏, lockWideHigh鎖定寬高比 等設置, noEvent 表示能夠事件穿透 animate: [ animateJSON ], // 圖層的動畫,能夠支持多個動畫 data: {...}, // 組件差別化相關的數據存放位置 style: { width: 100, height: 100, top: 0, left: 0 }, // 圖層組件的外層默認樣式 estyle: {}, // element div的樣式,style樣式只有4個參數,其餘的樣式均寫到estyle中,好比背景,由於動畫參數設置在element div上,因此這裏不能設置 transform樣式 events: [ eventJSON ] // 事件配置數據,每一個圖層能夠添加多個事件 }
咱們的數據丟到mobx進行管理,數據變化,直接更新視圖,這個很vue的數據管理有點類似。裏面會涉及到不少數據問題。這時候就須要咱們去定義一些全局的方法。我定義了一個h5ds的store,在store裏面保存了兩個數據(edata, data),其中edata是 editor data的簡寫,用於記錄用戶在操做編輯器的交互數據。好比當前選中了哪一個頁面,哪一個圖層,以及一些全局的配置參數。data則用於保存H5的json數據。爲了保證瀏覽器忽然崩潰,致使用戶數據被清除,我作了歷史操做數據,會把編輯的數據保存到localstorage。
操做記錄是保存了當前的edata和data數據到內存,爲了節約內存,只保存了20次最新的操做,隨時能夠經過撤銷回退到上一步操做。若是使用了redux,自然數據回滾,很是方便。
在編輯器內部數據更新是很是頻繁的,由於數據嵌套太深,mobx的proxy監聽數據是不會去監聽對象或者數組內部的數據,因此須要手動觸發視圖更新,這裏寫了一個全局的方法updateCanvas()
去強制更新整個視圖,爲了作性能方面的考慮,在拖動位置或者修改大小的時候,只去修改某個圖層的視圖。每一個layer有一個key,經過修改這個key能夠實現單個組件的更新。
圖層插件在編輯器中和預覽頁面都會用到,這裏就會涉及到複用了,咱們把插件分爲Layer.js 和 Editor.js 。其中 Layer.js 是在編輯器和H5預覽頁面都會用到,Editor.js 只在編輯器中用到。爲了減小預覽頁面的代碼,咱們單獨打包了2份UMD包,在不一樣的地方去使用,最初咱們採用requirejs去管理UMD包,可是後來發展requirejs有各類問題,可能和一些第三方的包衝突,因此咱們把插件直接掛載到window對象下面,使用H5DS_GLOBAL對象存儲起來,雖然很暴力,可是這種方法真的很是實用。
針對拖動縮放旋轉,這塊頗有意思,若是用react的方式,會在每一個選中的元素外面包裹一層拖動的組件,若是是用判斷去動態加載這個拖動組件,圖層必定會被更新的,因此咱們用了一種很巧妙的方法,用jquery封裝了一個托拉拽的插件,在componentDidMount
裏面去綁定事件,初始化這個插件,這種方法雖然是不被推薦的,能夠用奇巧淫技來形容吧。但的確大大減小了咱們的維護成本。用起來也很是方便。
咱們能夠結合防抖函數去作性能優化,控制或者選擇性的去更新視圖。下面舉個例子:
import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import debounce from 'lodash/debounce'; @inject('h5ds') @observer class Demo extends Component { constructor(props) { super(props); this.state = { count: props.h5ds.count // 默認是1 } } // 防抖函數控制性能 updateOtherRender = debounce(() => { const { count } = this.state; // 若是大於10纔會去更新其餘地方的視圖 if(count > 10) { this.props.h5ds.count = this.state.count } }, 500) changeValue = e => { this.setState({count: e.target.value}, this.updateOtherRender) } render() { return <input type="number" value={this.state.count} onChange={this.changeValue}/> } }
咱們在不少地方都有用到上面這種寫法,react提倡的最小模塊化,咱們也但願模塊之間的影響會最小,若是一個參數在多個模塊中被使用,在快速輸入的時候務必會變的很慢。
手機的分辨率太多了,要兼容所有機型,一種兼容方案是遠遠不夠的,這裏我提供了多種兼容方案。
第一種: 等比縮放
咱們的默認寬度高度是 320px * 514px,而後進行縮放,自動撐滿高或者寬度,如圖所示的陰影部分,固然上下或者左右可能預留一部分邊框沒有任何顯示。
第二種:全屏背景
由於會存在上下或者左右有間隙的狀況,這時候咱們把背景顏色作全屏處理,好比紅色是背景色,若是高度超過了,咱們的H5頁面會自動出現滾動條。
第三種:吸附定位
吸附定位這個名詞是我本身取的,有時候要兼容到iphoneX是很麻煩的,吸附定位表示能夠吸附到頂部或者其餘相對位置,咱們的吸附定位提供了8個位置能夠吸附。吸附後,會以相對window的位置進行定位,而不是相對陰影的定位。好比針對小紅心:
開啓吸附前:
開啓左上角吸附後:
npm install h5ds --save
安裝依賴包。externals: ['React', 'ReactDOM', 'ReactRouter', 'ReactRouterDOM', 'mobx', '_', 'antd', 'PubSub', 'moment']
html模版:
<!DOCTYPE html> <html> <head lang="zh-cn"> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="description" content=""> <meta name="keywords" content=""> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>H5DS5.0</title> <meta name="renderer" content="webkit"> <!-- No Baidu Siteapp--> <meta http-equiv="Cache-Control" content="no-siteapp" /> <meta name="apple-mobile-web-app-title" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta http-equiv="Cache-Control" content="no-siteapp" /> <link rel="shortcut icon" href="/assets/images/favicon.ico"> <link rel="stylesheet" href="https://at.alicdn.com/t/font_1160472_ybl2xl0ao8.css"> <link rel="stylesheet" href="https://at.alicdn.com/t/font_157397_ujac0trx9i.css"> <link href="https://cdn.bootcss.com/antd/3.23.0-beta.0/antd.min.css" rel="stylesheet"> <link href="https://cdn.h5ds.com/lib/plugins/swiper.min.css" rel="stylesheet"> <script src="https://cdn.h5ds.com/lib/plugins/swiper.min.js"></script> <script src="https://cdn.h5ds.com/lib/plugins/jquery.min.js"></script> <script src="https://cdn.h5ds.com/lib/plugins/h5ds.vendor.min.js"></script> <script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdn.bootcss.com/antd/3.23.0-beta.0/antd.min.js"></script> </head> <body> <div id="App"></div> </body> </html>
react代碼:
import 'h5ds/editor/style.css'; import { render } from 'react-dom'; import React, { Component } from 'react'; import H5dsEditor from 'h5ds/editor'; class Editor extends Component { constructor(props) { super(props); this.state = { data: null }; } /** * 保存APP */ saveApp = async data => { console.log('saveApp ->', data); }; /** * 發佈 app */ publishApp = async data => { console.log('publishApp ->', data); }; componentDidMount() { // 模擬異步加載數,設置 defaultData 會默認加載一個初始化數據 setTimeout(() => { this.setState({ data: 'defaultData' }); }, 100); } /** * 使用編輯器部分 */ render() { const { data } = this.state; return ( <H5dsEditor plugins={[]} // 第三方插件包 data={data} options={{ publishApp: this.publishApp, saveApp: this.saveApp, // 保存應用 appId: 'test_app_id' // 當前appId }} /> ); } } // render render( <Editor />, document.getElementById('App') );
最後感謝各位的閱讀!
咱們的官方網站是:http://www.h5ds.com
咱們的git地址是https://github.com/h5ds/h5ds
工具編輯器發佈目前已經比較成熟,還在迭代中,咱們但願能有更多的開發者能參與進來,開發插件,讓H5DS編輯器能給各類領域帶來便利。
技術交流QQ羣:549856478
若是你以爲有錯誤的地方,或者有任何好的建議,歡迎issue咱們!