我有個很是犀利的朋友,在得知我要去作可視化的頁面搭建工具時問了我一個問題:前端
「你本身會用這樣的工具嗎?」react
同時帶着意味深長的笑。jquery
然而這個問題並無如他所願改變個人想法。早在 jquery ui、bootstrap 盛行的時代,就有過無數這樣的工具,我沒有用過,也不會去用。緣由有一萬個:git
在包括個人不少前端看來,這條路上屍骨累累,甚至有不少連痕跡都沒有留下。可是失敗者最多的路,並不必定是死路。若是都沒有拋開過頭腦裏的成見,沒有進行過獨立思考就放棄了,未免太盲目。這篇文章就當作我在求生之路上的記錄。也請讀者暫且忘掉全部的經驗,輕裝上陣,這趟旅途不會讓你失望。對具體設計不感興趣的讀者能夠直接閱讀《生門》一章,讀完那一章後或許你會火燒眉毛再從頭讀起。github
在接下來的兩章中,咱們將從項目背景一直討論到關鍵技術的實踐。這其中既會包括各類技術也會包括產品和交互的思考。web
項目的背景是,公司業務迅速擴張,有大量對內的系統頁面須要搭建。而前端人力是瓶頸,因此咱們但願能以服務化的方式輸出前端能力,讓公司內全部非前端出身但有編程能力的人都能使用這種服務快速地開發出較高質量的頁面。 從產品角度來講,它的目標已經很明確了:ajax
有了這個目標,咱們就能夠開始設計產品形態了。編程
頁面分爲視圖和邏輯兩部分,在目前組件化的大背景下,視圖基本上能夠等同於組件樹。首先,什麼樣的頁面編輯方式學習成本最低同時最快速?固然是所見即所得,拖拽或者編輯樹型結構的數據這兩種方式均可以接受。實際測試中拖拽最容易上手,熟悉了快捷鍵的狀況下則編輯組件樹更快。redux
接着,怎樣讓用戶編寫頁面邏輯既能學習成本低,又能保障質量?學習成本低意味着概念要少,或者都是用戶已知的概念。保障質量這個概念比較大,咱們能夠從開發的兩個階段來考慮:bootstrap
爲了給讀者一個更直觀的影響,咱們暫且來看一張兩張圖。
頁面編輯:
邏輯編輯:
接下來分部分細化形態,梳理關係,來獲得一個明確的架構圖。目前看來可先拆分紅三個部分:
很容易發現這三者的關係並非平行的。首先,IDE 在這三者中是直接給用戶使用的產物,它表明着咱們最終想要呈現給用戶什麼樣的東西。對其餘部分來講,它算是需求來源。
來看它和頁面以及組件的的關係。咱們最終但願用戶在點擊頁面上的某個組件或者組件樹上的節點時,就能查看、配置這個組件上的屬性,邏輯綁定到它觸發的事件上。
所以它對組件的需求是:組件必須暴露出本身的全部屬性和事件,讓外部可讀。
再看 IDE 和框架的關係。用戶在編寫邏輯時,須要理解的概念都是屬於框架的, IDE 只是編輯工具。固然 IDE 能夠提供不少輔助功能,例如語法校驗,例如可視化地展現邏輯與組件的綁定關係。框架爲主,IDE 爲輔。
最後,框架和組件的關係。這裏頗有意思,按技術發展的現狀來講,一直都是先有組件庫,纔有上層應用框架。然而,組件規範其實應該是應用框架規範的一部分。舉個實際例子,若是應用框架要創建全局數據源(方便作回滾等高級功能),來保存全部狀態。那麼組件就再也不須要內部狀態,只要渲染就夠了,實現上簡單不少。這種上層建築與基礎設施的關係,很像高樓與磚瓦。摩天大樓須要鋼筋混凝土,負責燒土磚的工人一開始是想不到的。因此實施中,框架和組件庫之間一般還會有適配層。優秀的架構能力就體如今一開始就看到了足夠多的上層需求,提早避免了發展中的人力損耗。
理清了全部關係後,來看看總體架構:
這其中將 IDE 底層和業務層進行了拆分,IDE 底層提供窗口、快捷鍵、Tab 等經常使用功能,IDE 上業務層才用來處理和可視化相關的內容。其中也包括爲了提供更好體驗,卻又不適合放到組件、和應用框架中的膠水代碼,例如組件屬性的說明,示例等等。IDE 的架構設計將會在另外一篇文章中介紹。
總體的架構有了後,接下來就是關鍵技術——運行時框架的設計了。
在數據驅動的大背景下,應用框架處理的問題實際上只有一個:數據管理。其中「數據」既包括組件數據也包括業務數據,而「管理」既包括如何保存數據,也包括以何種方式讓用戶來讀寫數據。咱們仍然從使用場景出發,來分析出數據管理的應用場景,最後再考慮設計實現。在前端領域內,用戶對交互的需求是漸進增加的,業務的需求是漸進的,所以應用的複雜度總體看來也是漸進的。因此咱們只須要明確出最簡單和最複雜的狀況,就能夠勾勒出框架須要支持的範圍了:
在這個場景中用戶須要瞭解兩件事情:
再接着看最複雜的場景,我所接觸過的最複雜的前端應用都是業務關聯極強的工具,例如雲計算平臺的控制檯,客服系統的控制檯,包括這個 IDE 也算。這類產品的複雜體如今兩個方面:
有了這兩個端點,就找到了要提供的能力的上限和下限,接下來就是框架設計中最有意思也最困難的部分了——如何提供漸進式地開發體驗。這幾乎也是全部優秀框架的共有的一個品質。漸進式的體驗意味着用戶只要瞭解最基本的功能就能立刻開始工做,當要處理更高級的需求時才須要再學習高級的功能。更進一步話,最好這些高級功能也是用一種可擴展的機制來實現的,如中間件,學習一次機制,便可解決無限的問題。
在最簡場景裏能夠看到,用戶所需的最基本的功能就是一個可讀寫的,包含全部組件數據的數據源便可(如下簡稱組件數據源)。爲了便於讓用戶理解,這個數據源的數據格式最好與組件樹存在相似的對應關係。舉個註冊頁面的例子,咱們的組件樹可能長這樣:
<div> <Title>註冊</Title> <Input label="姓名"/> <Input label="密碼" type="password"/> <Button text="提交"/> </div>
那麼組件數據源可表述爲:
{ 0: { text: '註冊', size: 'large' }, 1: { value: '', label: '姓名' type: 'text', }, 2: { value: '', label: '密碼' type: 'password', }, 3: { text: '提交', type: 'normal' } }
用戶的讀寫操做能夠設計成這樣:
// 借用 redux 中的 store 做爲數據源的名字 store.get('1.value') // 讀取第一個 Input 的值 store.set('3.type', 'loading') // 將 Button 設爲 loading 狀態
這個寫法能夠實現需求,但有兩個問題:
爲什麼不讓用戶本身給想要數據的組件取名?這能夠一次性解決這兩個問題。
<div> <Title>註冊</Title> <Input bind="name" label="姓名"/> <Input bind="password" label="密碼" type="password"/> <Button bind="submit" text="提交"/> </div>
獲得的數據源:
{ name: { value: '', label: '姓名' type: 'text', }, password: { value: '', label: '密碼' type: 'password', }, submit: { text: '提交', disabled: false } }
再看看用戶的提交邏輯如何寫(這個邏輯綁定在 Button 的 onClick 事件上):
// 經過注入的方式把數據源管理對象交給用戶 function({store}){ store.set('submit', {disabled: true}) // 爲了防止重複提交 ajax({ url : 'xxx', data: { name: store.get('name').value, password: store.get('password').value } }).finally(() => { store.set('submit', {disabled: false}) }) }
稍微好了一點,可是任何開發者都仍然會以爲這段代碼太髒,它既處理了業務邏輯又處理了渲染邏輯,項目膨脹以後這樣的代碼不利於維護。
咱們須要一種機制來分離不一樣類型的處理邏輯,讓代碼更易維護。這個出發點也正是啓發後面設計的關鍵!
爲何這樣說?讓咱們來看看以前談到的複雜場景,其中提到了大量的交互狀態是複雜場景的特色之一,常見的交互有:
如何分離這些交互細節?或者換個更具體的問題,你以爲用戶怎樣寫這些邏輯會最爽?仍然以上面的場景爲例子,用戶固然但願他代碼中的ajax一發送,按鈕就自動變成 disable,一結束又自動變回來。這對咱們來講不就是 ajax 狀態和組件狀態之間的自動映射嗎?咱們能不能提供一種機制讓用戶給 ajax 命名,同時能夠寫映射關係,如:
ajax('login', {name: 'xxx', password: 'xxx'})
映射關係:
function mapAjaxToButton({ajaxStates}){ // ajaxStates 由框架提供,保存着全部的ajax 狀態 return { disabled: ajaxStates.login === 'pending' } }
這樣,剛纔處理 ajax 的髒代碼就徹底分離出來了。咱們再看看這個方案中幾個概念的關係。
打開這個思路後,你會發現幾乎其餘全部問題,均可以用這個方案來解了!爲專有的問題領域創建專有的數據源,同時創建數據源到組件數據源的映射關係。即能擴展能力,又能分離代碼。
咱們再看權限控制的例子。若是用戶不具備某權限時就把button disable 掉,映射關係咱們能夠寫成:
function mapAuthToButton({auth}){ return { disabled: !auth.has('xxx') } }
很是直觀。
再看錶單驗證狀態。創建驗證數據結果的數據源,讓用戶配置哪些組件須要進行校驗,校驗時機(例如正在輸入或者離開焦點時)。例如:
<Input bind='name' onBlur={state => {validation.validate(state)}} />
validator 映射寫法的和前面的例子異區同工,用戶但願的固然是我只須要告訴你什麼狀況下是經過,什麼不經過便可,同時也能夠加上一些必要的message:
function validateRule(state) { return { valid: state.value !== 'xxx', message: state.value !== 'xxx' ? 'success' : 'value must be xxx', } }
有了輸入源,接下來仍然按以前思路將驗證數據源映射到組件數據源上:
function mapValidationToInput({validation}) { const hasFeedback = validation.get('name') !== undefined return { status: hasFeedback ? (validation.get('name').valid ? 'valid': 'invalid') : 'normal', help: hasFeedback ? validation.get('name').message : '' } }
到這裏,咱們已經徹底看到用專屬的數據源處理專有問題,最後映射到組件數據源上去所產生的效果了。它能很好地將全部將交互細節和業務邏輯劃分。
咱們進一步注意到,不管異步控制、表單驗證仍是權限,只要組件遵循某種屬性命名規則,那麼全部的映射函數就均可以寫成固定的!
所以,若是咱們爲組件制定一個屬性接口規範,就能夠利用提供更有好的方式自動生成映射代碼了。例如,規定帶驗證功能的表單類的屬性接口必須有:
那麼上面例子裏面的映射函數,就只須要用戶填寫 validateRule 就夠了,映射函數將 valid/message 字段映射到 組件的 status/help 屬性上。
至此,最後剩下的處理複雜場景中的大量業務數據的這一問題也迎刃而解了,一樣創建一個業務數據源,聲明業務數據與組件數據的映射關係便可。
講完了邏輯的設計,最後再提一下組件的規範,正如前面所說,全部的組件狀態是由應用框架保存的。這和咱們現實中常見的經驗相悖。現實中的組件一般是數據、行爲、渲染邏輯三部分寫在一塊兒,使用 class 或者工廠方法來建立。若是是全面由框架接管,則應該打散,所有寫成聲明式。雖然不符經驗,可是聲明式的組件定義解決了《理想的應用框架》中提到的組件庫的兩個終極問題,「覆寫和擴展」。具體可參見以開源的組件規範 github.com/sskyy/react-lego,這裏再也不展開。
在尚未開始項目以前玉伯就提醒過我,IDE作得再酷炫,組件作得再豐富都不是活路。可視化的集成框架真正的問題在於:雖然對沒有前端能力的人來講,它更簡單。但相比手寫代碼它缺乏了靈活性,那麼在用戶前端能力加強後,你拿什麼來補償用戶,讓他仍然離不開你?這裏我能夠再清晰的回答一次。
任何一個有必定複雜度、會持續增加的應用最重視的,其實並非開發速度,而是可維護性和可擴展性。 這也是框架設計者們擺在首位的事情。可擴展性的好壞取決於框架的擴展機制。在咱們的上面的設計中須要擴展的有兩部分,組件和功能。組件的擴展能夠經過容許用戶提交自定義組件來實現。功能的擴展主要由框架開發者完成,可是也能夠考慮讓用戶能仿照異步管理數據源同樣創建本身專用的數據源來解決業務專有問題。
可維護性,在數據驅動的前提下,實際上等於」框架能不能很好的回答兩個問題「:
第一個問題容易解決,創建統一的全局數據源,正如咱們所設計的。不只方便調試,還能夠作回滾,作應用快照等功能。
第二個問題,在已知的框架中有兩種常見的答案:
一種是利用某種設計模式,讓用戶將數據的變化集中在一個抽象裏。例如 redux 狀態機中的 reducer。這種方式的好處在於直接看代碼就能夠了解數據全部可能發生的變化。但靠代碼組織的問題在於它自己受文件系統影響,一但代碼拆分不合理仍是容易很差找。
另外一種方式則更常見,就是運行時記錄調用棧。在 《理想的應用框架》中也提到過。以」響應業務事件的聲明式代碼「做爲基礎單位,框架來控制調用流程,這樣框架便可產出一個和業務事件一致的調用棧,同時由於這種一致性,不管代碼拆分得多不合理,均可以展現合理的信息。但調用棧的方式也有個缺點,就是必定要運行,出問題時必定要運行到相應的那一步才能找到問題相應的信息。同時會受到循環、條件語句的影響,這在多步調試或者非冪等操做的場景下很是很差用。它只能回答「數據此次在哪裏被修改了」,不能回答「數據均可能在哪裏被修改」。
有沒有一種方式,既是靜態的,又能產出像調用棧同樣的數據結構方便作輔助工具呢?固然有!語法分析就能夠,它絕對準確,不受條件語句、異常等影響,甚至能作到提早預知人爲錯誤。Rust 在提早預知人爲錯誤這個方面上達到了一個新高度。它作到了」能編譯經過就不會出錯「 ,這讓工程質量產生了質的提高。舉個咱們系統中能夠理解的例子,在前面的設計中已經提到,組件是聲明式的,因此數據格式是已知而且可讀的,包括每一個字段的類型。在實現中咱們的後端使用了 graphQL 做爲接口層,所以接口返回的數據結構和字段類型也是已知的,當用戶在代碼中調用後端接口並嘗試把接口返回的數據塞到組件上來展現時,經過語法分析、變量追蹤,咱們就能夠在「運行前」自動檢測到用戶是否傳錯了接口參數,是否把不符合組件數據格式的數據塞給了組件等等。這樣強度的檢測幾乎能夠幫咱們避免平常開發中絕大多數人爲失誤。除了診斷,語法分析固然還能用來提供全局的依賴視圖,例如哪些接口在哪些邏輯裏被調用了。哪些數據被哪些邏輯修改了,會引發視圖的哪些部分改變等等。能夠完美地回答「數據在哪裏被修改了」 。
接下來就是如何實現的問題了。稍微想一想就會發現,基於手寫代碼的方式分析成本有點高,並且頗有可能實現不了。這裏面有兩個點比較麻煩:
store.set('xxx', 'yyy')
和 分析 store[method](name)
的複雜度。可是,咱們剛剛設計的系統不是放棄了靈活性嗎?用戶在使用 IDE 時不須要文件系統的概念,只須要如填空通常在函數中寫邏輯,全部依賴的變量也不須要本身關係,都是框架經過函數參數注入的。在這個背景下,用戶邏輯的目的提早知道了,全部的入參出參的用途也提早知道了,那麼要實現上述的「數據在哪裏被修改了」等功能,是否是隻須要追蹤用戶代碼裏的變量就夠了?!上面說的難點在咱們這裏不存在了。
到這裏,死門居然變成了生門!「開發環境經過對邏輯使用的限制,實現了對整個應用的控制達到了 100% 的狀態「!具體能夠從兩個方面來進一步理解:
運行時分析示例:
靜態依賴分析示例:
想到了這裏,纔算真正找到了活路。文章的前半部分,我強調過從頭思考,緣由很簡單,任什麼時候候經驗都是可能成爲束縛的。就像從框架開發者的角度來講,放棄了靈活性,把本身侷限在必定範圍內簡直是逆行倒施,但正是這樣的侷限纔有可能在開發速度上和可維護性上帶來質的飛昇。
在這兩年作框架開發的同時我也在作全棧教學的工做。這個過程當中也發現對公司來講」授人以魚」和「授人以漁「一樣重要。由於不管教學作得多麼成功,最後的產出物的質量仍然會受到受到學生的自身素質、工做內容等影響。特別是團隊人員變化快時,教學的收益會特別低。而將能力服務化再提供給受衆,能夠抵禦這種風險,由於服務自身能夠不斷沉澱、升級。後來在學習FBP時,與做者 J.P.Morrison 通訊瞭解到 IBM 時代的 FBP 可視化工具的應用場景和這個項目很是像,而 FBP 當時在 IBM 內部取得了成功,他們甚至成功把所有可視化編輯的系統賣給了一家銀行。這些信息也讓我進一步意識到團隊越大,構建上層建築越有意義。在不少大公司裏,光內部系統就有上百個,有大量複雜度在必定範圍內的頁面要開發,前端服務化的意義遠大於咱們站在本身固有的經驗中所看到的程度。
到這裏這一篇能夠先告一段路了,以後組件庫的碰到的常見問題和設計還有基於 web 的 IDE 通用架構會有另外的文章來講明。相比這些具體的技術實現,我更但願後面這些關於質變,以及如何造成質變的思考能帶給讀者更多收益。感謝閱讀。
最後放出幾張用戶製做的頁面: