前端服務化——頁面搭建工具的死與生

引言

我有個很是犀利的朋友,在得知我要去作可視化的頁面搭建工具時問了我一個問題:前端

「你本身會用這樣的工具嗎?」react

同時帶着意味深長的笑。jquery

然而這個問題並無如他所願改變個人想法。早在 jquery ui、bootstrap 盛行的時代,就有過無數這樣的工具,我沒有用過,也不會去用。緣由有一萬個:git

  • 業務需求太靈活,工具都是基於已有的組件庫,個性化的東西搭不出來。
  • 前端技術發展太快,工具整合沒有那麼迅速。
  • 有學習成本,不如手寫快,靈活性沒有手寫高。

在包括個人不少前端看來,這條路上屍骨累累,甚至有不少連痕跡都沒有留下。可是失敗者最多的路,並不必定是死路。若是都沒有拋開過頭腦裏的成見,沒有進行過獨立思考就放棄了,未免太盲目。這篇文章就當作我在求生之路上的記錄。也請讀者暫且忘掉全部的經驗,輕裝上陣,這趟旅途不會讓你失望。對具體設計不感興趣的讀者能夠直接閱讀《生門》一章,讀完那一章後或許你會火燒眉毛再從頭讀起。github

起點和方向

在接下來的兩章中,咱們將從項目背景一直討論到關鍵技術的實踐。這其中既會包括各類技術也會包括產品和交互的思考。web

項目的背景是,公司業務迅速擴張,有大量對內的系統頁面須要搭建。而前端人力是瓶頸,因此咱們但願能以服務化的方式輸出前端能力,讓公司內全部非前端出身但有編程能力的人都能使用這種服務快速地開發出較高質量的頁面。 從產品角度來講,它的目標已經很明確了:ajax

  • 使用人羣:非前端的開發者
  • 要提供的服務:能以中上等開發速度開發出中上等可維護性頁面的集成開發環境(如下簡稱開發環境)

有了這個目標,咱們就能夠開始設計產品形態了。編程

頁面分爲視圖和邏輯兩部分,在目前組件化的大背景下,視圖基本上能夠等同於組件樹。首先,什麼樣的頁面編輯方式學習成本最低同時最快速?固然是所見即所得,拖拽或者編輯樹型結構的數據這兩種方式均可以接受。實際測試中拖拽最容易上手,熟悉了快捷鍵的狀況下則編輯組件樹更快。redux

接着,怎樣讓用戶編寫頁面邏輯既能學習成本低,又能保障質量?學習成本低意味着概念要少,或者都是用戶已知的概念。保障質量這個概念比較大,咱們能夠從開發的兩個階段來考慮:bootstrap

  • 一是開發時最好有保障,例如前端開發時的 eslint 加上編輯器提示就能很好地提早避免一些低級錯誤。
  • 二是在開發完以後,代碼應該「有跡可循」,不管是排查問題,仍是擴展需求,都要讓用戶在頭腦裏第一時間就知道應該怎樣寫邏輯,寫在哪裏。也就意味着概念要完善,職責分明。同時,工具層面也能夠有些輔助功能,例如傳統編輯器的變量搜索等。

爲了給讀者一個更直觀的影響,咱們暫且來看一張兩張圖。

頁面編輯:
頁面編輯
邏輯編輯:
邏輯編輯

接下來分部分細化形態,梳理關係,來獲得一個明確的架構圖。目前看來可先拆分紅三個部分:

  • 一個編輯頁面和邏輯的工具,如下暫稱 IDE。
  • 搭建頁面所需的基礎組件。
  • 運行時框架(如下簡稱框架),由它將頁面的組件樹、和頁面邏輯結合在一塊兒渲染成最終的頁面。

很容易發現這三者的關係並非平行的。首先,IDE 在這三者中是直接給用戶使用的產物,它表明着咱們最終想要呈現給用戶什麼樣的東西。對其餘部分來講,它算是需求來源。

來看它和頁面以及組件的的關係。咱們最終但願用戶在點擊頁面上的某個組件或者組件樹上的節點時,就能查看、配置這個組件上的屬性,邏輯綁定到它觸發的事件上。

組建與屬性

所以它對組件的需求是:組件必須暴露出本身的全部屬性和事件,讓外部可讀。

再看 IDE 和框架的關係。用戶在編寫邏輯時,須要理解的概念都是屬於框架的, IDE 只是編輯工具。固然 IDE 能夠提供不少輔助功能,例如語法校驗,例如可視化地展現邏輯與組件的綁定關係。框架爲主,IDE 爲輔。

最後,框架和組件的關係。這裏頗有意思,按技術發展的現狀來講,一直都是先有組件庫,纔有上層應用框架。然而,組件規範其實應該是應用框架規範的一部分。舉個實際例子,若是應用框架要創建全局數據源(方便作回滾等高級功能),來保存全部狀態。那麼組件就再也不須要內部狀態,只要渲染就夠了,實現上簡單不少。這種上層建築與基礎設施的關係,很像高樓與磚瓦。摩天大樓須要鋼筋混凝土,負責燒土磚的工人一開始是想不到的。因此實施中,框架和組件庫之間一般還會有適配層。優秀的架構能力就體如今一開始就看到了足夠多的上層需求,提早避免了發展中的人力損耗。

理清了全部關係後,來看看總體架構:

總體架構

這其中將 IDE 底層和業務層進行了拆分,IDE 底層提供窗口、快捷鍵、Tab 等經常使用功能,IDE 上業務層才用來處理和可視化相關的內容。其中也包括爲了提供更好體驗,卻又不適合放到組件、和應用框架中的膠水代碼,例如組件屬性的說明,示例等等。IDE 的架構設計將會在另外一篇文章中介紹。

龍骨

總體的架構有了後,接下來就是關鍵技術——運行時框架的設計了。

在數據驅動的大背景下,應用框架處理的問題實際上只有一個:數據管理。其中「數據」既包括組件數據也包括業務數據,而「管理」既包括如何保存數據,也包括以何種方式讓用戶來讀寫數據。咱們仍然從使用場景出發,來分析出數據管理的應用場景,最後再考慮設計實現。在前端領域內,用戶對交互的需求是漸進增加的,業務的需求是漸進的,所以應用的複雜度總體看來也是漸進的。因此咱們只須要明確出最簡單和最複雜的狀況,就能夠勾勒出框架須要支持的範圍了:

  • 按業務經驗,最簡單的狀況無非就是純展現的「詳情頁」 或 「列表頁」。最符合本能的邏輯寫法應該是:
    • 拼好組件樹。若是是靜態的數據,直接在每一個組件上的屬性裏設置好便可,流程結束。
    • 若是是動態數據,那麼使用 ajax 獲取到數據。將獲取的數據格式化成組件所能接受的格式,而後使用 api set 進去。

在這個場景中用戶須要瞭解兩件事情:

  • 組件的數據格式,實際上就是組件的屬性,在 IDE 中已是直接暴露出來的。
  • 設置數據的 api 。

再接着看最複雜的場景,我所接觸過的最複雜的前端應用都是業務關聯極強的工具,例如雲計算平臺的控制檯,客服系統的控制檯,包括這個 IDE 也算。這類產品的複雜體如今兩個方面:

  • 有大量的交互細節,例如組件狀態要和權限結合(例如 按鈕的 disable 狀態)、組件要根據需求動態顯示或隱藏,表單的校驗,異步狀態的提示或管理(例如發送請求後,按鈕上出現loading)。
  • 除了組件數據,還有大量的業務數據要管理,而且是其中有不少聯動關係。例如在雲計算控制檯裏面有 ECS、LBS 等概念,ECS 和 LBS 有關聯關係,ECS若是更名了。不只要更新ECS本身的詳情顯示,還要自動更新關聯的LBS的顯示等。

有了這兩個端點,就找到了要提供的能力的上限和下限,接下來就是框架設計中最有意思也最困難的部分了——如何提供漸進式地開發體驗。這幾乎也是全部優秀框架的共有的一個品質。漸進式的體驗意味着用戶只要瞭解最基本的功能就能立刻開始工做,當要處理更高級的需求時才須要再學習高級的功能。更進一步話,最好這些高級功能也是用一種可擴展的機制來實現的,如中間件,學習一次機制,便可解決無限的問題。

在最簡場景裏能夠看到,用戶所需的最基本的功能就是一個可讀寫的,包含全部組件數據的數據源便可(如下簡稱組件數據源)。爲了便於讓用戶理解,這個數據源的數據格式最好與組件樹存在相似的對應關係。舉個註冊頁面的例子,咱們的組件樹可能長這樣:

<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 狀態

這個寫法能夠實現需求,但有兩個問題:

  • 用組件的位置做爲索引不友好,不能適應變化。例如組件的位置調整了一下順序,代碼裏就得相應改動。
  • 在用戶的業務邏輯中,並非全部組件的數據用戶都須要,例如Title。

爲什麼不讓用戶本身給想要數據的組件取名?這能夠一次性解決這兩個問題。

<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})
  })
}

稍微好了一點,可是任何開發者都仍然會以爲這段代碼太髒,它既處理了業務邏輯又處理了渲染邏輯,項目膨脹以後這樣的代碼不利於維護。

咱們須要一種機制來分離不一樣類型的處理邏輯,讓代碼更易維護。這個出發點也正是啓發後面設計的關鍵!

爲何這樣說?讓咱們來看看以前談到的複雜場景,其中提到了大量的交互狀態是複雜場景的特色之一,常見的交互有:

  • 異步狀態控制,如上面 button 在發請求時要設爲 disable 防止重複提交
  • 權限控制
  • 表單驗證狀態

如何分離這些交互細節?或者換個更具體的問題,你以爲用戶怎樣寫這些邏輯會最爽?仍然以上面的場景爲例子,用戶固然但願他代碼中的ajax一發送,按鈕就自動變成 disable,一結束又自動變回來。這對咱們來講不就是 ajax 狀態和組件狀態之間的自動映射嗎?咱們能不能提供一種機制讓用戶給 ajax 命名,同時能夠寫映射關係,如:

ajax('login', {name: 'xxx', password: 'xxx'})

映射關係:

function mapAjaxToButton({ajaxStates}){ // ajaxStates 由框架提供,保存着全部的ajax 狀態
  return {
    disabled: ajaxStates.login === 'pending'
  }
}

這樣,剛纔處理 ajax 的髒代碼就徹底分離出來了。咱們再看看這個方案中幾個概念的關係。

數據源架構1

打開這個思路後,你會發現幾乎其餘全部問題,均可以用這個方案來解了!爲專有的問題領域創建專有的數據源,同時創建數據源到組件數據源的映射關係。即能擴展能力,又能分離代碼。

咱們再看權限控制的例子。若是用戶不具備某權限時就把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 : ''
  }
}

到這裏,咱們已經徹底看到用專屬的數據源處理專有問題,最後映射到組件數據源上去所產生的效果了。它能很好地將全部將交互細節和業務邏輯劃分。

咱們進一步注意到,不管異步控制、表單驗證仍是權限,只要組件遵循某種屬性命名規則,那麼全部的映射函數就均可以寫成固定的!

所以,若是咱們爲組件制定一個屬性接口規範,就能夠利用提供更有好的方式自動生成映射代碼了。例如,規定帶驗證功能的表單類的屬性接口必須有:

  • status: 'normal' | 'valid' | 'invalid'
  • help : ''

那麼上面例子裏面的映射函數,就只須要用戶填寫 validateRule 就夠了,映射函數將 valid/message 字段映射到 組件的 status/help 屬性上。

至此,最後剩下的處理複雜場景中的大量業務數據的這一問題也迎刃而解了,一樣創建一個業務數據源,聲明業務數據與組件數據的映射關係便可。

數據源架構2

講完了邏輯的設計,最後再提一下組件的規範,正如前面所說,全部的組件狀態是由應用框架保存的。這和咱們現實中常見的經驗相悖。現實中的組件一般是數據、行爲、渲染邏輯三部分寫在一塊兒,使用 class 或者工廠方法來建立。若是是全面由框架接管,則應該打散,所有寫成聲明式。雖然不符經驗,可是聲明式的組件定義解決了《理想的應用框架》中提到的組件庫的兩個終極問題,「覆寫和擴展」。具體可參見以開源的組件規範 github.com/sskyy/react-lego,這裏再也不展開。

生門!

在尚未開始項目以前玉伯就提醒過我,IDE作得再酷炫,組件作得再豐富都不是活路。可視化的集成框架真正的問題在於:雖然對沒有前端能力的人來講,它更簡單。但相比手寫代碼它缺乏了靈活性,那麼在用戶前端能力加強後,你拿什麼來補償用戶,讓他仍然離不開你?這裏我能夠再清晰的回答一次。

任何一個有必定複雜度、會持續增加的應用最重視的,其實並非開發速度,而是可維護性和可擴展性。 這也是框架設計者們擺在首位的事情。可擴展性的好壞取決於框架的擴展機制。在咱們的上面的設計中須要擴展的有兩部分,組件和功能。組件的擴展能夠經過容許用戶提交自定義組件來實現。功能的擴展主要由框架開發者完成,可是也能夠考慮讓用戶能仿照異步管理數據源同樣創建本身專用的數據源來解決業務專有問題。

可維護性,在數據驅動的前提下,實際上等於」框架能不能很好的回答兩個問題「:

  • 數據如今是什麼樣的
  • 數據在哪裏被修改了,或者更細緻地分解爲「運行時告訴我數據此次在哪裏被修改了」,和「開發時告訴我數據有可能在哪裏被修改」。

第一個問題容易解決,創建統一的全局數據源,正如咱們所設計的。不只方便調試,還能夠作回滾,作應用快照等功能。

第二個問題,在已知的框架中有兩種常見的答案:

一種是利用某種設計模式,讓用戶將數據的變化集中在一個抽象裏。例如 redux 狀態機中的 reducer。這種方式的好處在於直接看代碼就能夠了解數據全部可能發生的變化。但靠代碼組織的問題在於它自己受文件系統影響,一但代碼拆分不合理仍是容易很差找。

另外一種方式則更常見,就是運行時記錄調用棧。在 《理想的應用框架》中也提到過。以」響應業務事件的聲明式代碼「做爲基礎單位,框架來控制調用流程,這樣框架便可產出一個和業務事件一致的調用棧,同時由於這種一致性,不管代碼拆分得多不合理,均可以展現合理的信息。但調用棧的方式也有個缺點,就是必定要運行,出問題時必定要運行到相應的那一步才能找到問題相應的信息。同時會受到循環、條件語句的影響,這在多步調試或者非冪等操做的場景下很是很差用。它只能回答「數據此次在哪裏被修改了」,不能回答「數據均可能在哪裏被修改」。

有沒有一種方式,既是靜態的,又能產出像調用棧同樣的數據結構方便作輔助工具呢?固然有!語法分析就能夠,它絕對準確,不受條件語句、異常等影響,甚至能作到提早預知人爲錯誤。Rust 在提早預知人爲錯誤這個方面上達到了一個新高度。它作到了」能編譯經過就不會出錯「 ,這讓工程質量產生了質的提高。舉個咱們系統中能夠理解的例子,在前面的設計中已經提到,組件是聲明式的,因此數據格式是已知而且可讀的,包括每一個字段的類型。在實現中咱們的後端使用了 graphQL 做爲接口層,所以接口返回的數據結構和字段類型也是已知的,當用戶在代碼中調用後端接口並嘗試把接口返回的數據塞到組件上來展現時,經過語法分析、變量追蹤,咱們就能夠在「運行前」自動檢測到用戶是否傳錯了接口參數,是否把不符合組件數據格式的數據塞給了組件等等。這樣強度的檢測幾乎能夠幫咱們避免平常開發中絕大多數人爲失誤。除了診斷,語法分析固然還能用來提供全局的依賴視圖,例如哪些接口在哪些邏輯裏被調用了。哪些數據被哪些邏輯修改了,會引發視圖的哪些部分改變等等。能夠完美地回答「數據在哪裏被修改了」 。

接下來就是如何實現的問題了。稍微想一想就會發現,基於手寫代碼的方式分析成本有點高,並且頗有可能實現不了。這裏面有兩個點比較麻煩:

  • 分析程序首先要理解基於文件系統的包管理機制,才能作全局的分析。
  • 若是用戶在代碼中作了二次抽象,分析程序的複雜度會翻倍。試想分析 store.set('xxx', 'yyy') 和 分析 store[method](name)的複雜度。

可是,咱們剛剛設計的系統不是放棄了靈活性嗎?用戶在使用 IDE 時不須要文件系統的概念,只須要如填空通常在函數中寫邏輯,全部依賴的變量也不須要本身關係,都是框架經過函數參數注入的。在這個背景下,用戶邏輯的目的提早知道了,全部的入參出參的用途也提早知道了,那麼要實現上述的「數據在哪裏被修改了」等功能,是否是隻須要追蹤用戶代碼裏的變量就夠了?!上面說的難點在咱們這裏不存在了。

到這裏,死門居然變成了生門!「開發環境經過對邏輯使用的限制,實現了對整個應用的控制達到了 100% 的狀態「!具體能夠從兩個方面來進一步理解:

  • 」對邏輯使用的限制「指的是具體作某件事的代碼寫在哪裏,必須怎麼寫都是由開發環境徹底指定的。這意味着開發環境徹底控制了全部代碼的語意背景。但同時也是由於這樣,開發環境說作不了的事情,就必定作不了,限制了用戶的自由發揮。
  • 」控制達到 100%「 指的是開發環境能夠分析理解全部用戶邏輯,你提的全部「什麼數據/接口/組件,在哪裏/何時,怎麼了?」這樣的問題它均可以回答。實際上 js 在這裏只是一種DSL了。舉幾個更具體的說法來表示 100%:
    • 除了用戶本身對業務理解的錯誤,開發環境幾乎能夠提早阻止全部人爲失誤,如前面所說的數據類型不匹配,ajax 參數錯誤等等。注意,這裏說的是提早阻止,不須要到運行時調試才發現
    • 開發環境可將全部邏輯和其中的依賴可視化,例如可完整地列舉出全部操做了某一數據的邏輯代碼。
    • 開發環境有足夠能力對用戶代碼進行自動升級轉換等工做。例如將 js 裏的全部數據操做自動變成 immutable,排除潛在的對象引用錯誤等。
    • 開發環境能夠深度分析運行時框架,提早注入運行時數據,提高運行時性能。例如提早分析哪些數據修改會致使哪些組件屬性,靜態注入這種依賴關係,這樣框架就再也不須要運行時再去判斷。這種數據到視圖的依賴綁定也正是過去 MVVM 類框架花了很大力氣去作的事情。

運行時分析示例:
運行時分析示例

靜態依賴分析示例:
靜態依賴分析示例

想到了這裏,纔算真正找到了活路。文章的前半部分,我強調過從頭思考,緣由很簡單,任什麼時候候經驗都是可能成爲束縛的。就像從框架開發者的角度來講,放棄了靈活性,把本身侷限在必定範圍內簡直是逆行倒施,但正是這樣的侷限纔有可能在開發速度上和可維護性上帶來質的飛昇。

在這兩年作框架開發的同時我也在作全棧教學的工做。這個過程當中也發現對公司來講」授人以魚」和「授人以漁「一樣重要。由於不管教學作得多麼成功,最後的產出物的質量仍然會受到受到學生的自身素質、工做內容等影響。特別是團隊人員變化快時,教學的收益會特別低。而將能力服務化再提供給受衆,能夠抵禦這種風險,由於服務自身能夠不斷沉澱、升級。後來在學習FBP時,與做者 J.P.Morrison 通訊瞭解到 IBM 時代的 FBP 可視化工具的應用場景和這個項目很是像,而 FBP 當時在 IBM 內部取得了成功,他們甚至成功把所有可視化編輯的系統賣給了一家銀行。這些信息也讓我進一步意識到團隊越大,構建上層建築越有意義。在不少大公司裏,光內部系統就有上百個,有大量複雜度在必定範圍內的頁面要開發,前端服務化的意義遠大於咱們站在本身固有的經驗中所看到的程度。

到這裏這一篇能夠先告一段路了,以後組件庫的碰到的常見問題和設計還有基於 web 的 IDE 通用架構會有另外的文章來講明。相比這些具體的技術實現,我更但願後面這些關於質變,以及如何造成質變的思考能帶給讀者更多收益。感謝閱讀。

最後放出幾張用戶製做的頁面:

答讀者問

  • 爲何社區歷來沒有流行過這樣的東西?
    • 這個問題其實比較模糊,這和問」怎樣作產品能成功」性質同樣,我很是建議讀者先讀讀純銀的文章,無論是技術仍是產品都有收益。可是我仍然嘗試回答一下。首先要作一套這樣的開發環境設計到的技術棧太多,組件庫、渲染引擎、IDE、分析引擎、後端服務每個方面都要耗費至關大的人力。就算社區有這樣的東西作出來了,也不多有團隊有同等人力能拿去用,任何嚴肅地投入生產的項目都不可能拿一個本身掌握不住的工具去用的。其次,這樣的東西就算有公司作出來而且在內部很流行,也很難爲外界所知,由於這種集成開發環境首先須要大量內部系統的積累,在咱們這裏就是組件庫、後端服務等。另外公司戰略上的支持也是必不可少的。實際上據我瞭解,無論流不流行,每一個大公司都有至少一套這樣的系統。
  • 框架部分爲何不用 redux ,如今的看起來像砍掉了 action 的 redux?
    • redux 本質上是個狀態機,action 的設計可以約束變化來源,屏蔽來源細節,同時寫代碼時能把全部變化和數據自己寫在一塊兒,解決「數據在哪裏,被怎麼了」的問題。然而咱們更傾向於像用戶暴露更少的概念,讓他用直覺來使用,由開發環境解決可維護性等問題。這是實際上是產品策略,和技術爭論無關。
相關文章
相關標籤/搜索