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

發表於2017-03-04

引言

我有個非常犀利的朋友,在得知我要去做視覺化的頁面搭建工具時問了我一個問題:

“你自己會用這樣的工具嗎?”

同時帶著意味深長的笑。

然而這個問題並沒有如他所願改變我的想法。早在 jquery ui、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的顯示等。

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

在最簡場景裡可以看到,使用者所需的最基本的功能就是一個可讀寫的,包含所有元件資料的資料來源即可(以下簡稱元件資料來源)。為了便於讓使用者理解,這個資料來源的資料格式最好與元件樹存在類似的對應關係。舉個註冊頁面的例子,我們的元件樹可能長這樣:

那麼元件資料來源可表述為:

使用者的讀寫操作可以設計成這樣:

這個寫法可以實現需求,但有兩個問題:

  • 用元件的位置作為索引不友好,不能適應變化。例如元件的位置調整了一下順序,程式碼裡就得相應改動。
  • 在使用者的業務邏輯中,並不是所有元件的資料使用者都需要,例如Title。

為何不讓使用者自己給想要資料的元件取名?這可以一次性解決這兩個問題。

得到的資料來源:

再看看使用者的提交邏輯如何寫(這個邏輯繫結在 Button 的 onClick 事件上):

稍微好了一點,但是任何開發者都仍然會覺得這段程式碼太髒,它既處理了業務邏輯又處理了渲染邏輯,專案膨脹之後這樣的程式碼不利於維護。

我們需要一種機制來分離不同型別的處理邏輯,讓程式碼更易維護。這個出發點也正是啟發後面設計的關鍵!

為什麼這樣說?讓我們來看看之前談到的複雜場景,其中提到了大量的互動狀態是複雜場景的特點之一,常見的互動有:

  • 非同步狀態控制,如上面 button 在發請求時要設為 disable 防止重複提交
  • 許可權控制
  • 表單驗證狀態

如何分離這些互動細節?或者換個更具體的問題,你覺得使用者怎樣寫這些邏輯會最爽?仍然以上面的場景為例子,使用者當然希望他程式碼中的ajax一傳送,按鈕就自動變成 disable,一結束又自動變回來。這對我們來說不就是 ajax 狀態和元件狀態之間的自動對映嗎?我們能不能提供一種機制讓使用者給 ajax 命名,同時可以寫對映關係,如:

對映關係:

這樣,剛才處理 ajax 的髒程式碼就完全分離出來了。我們再看看這個方案中幾個概念的關係。

資料來源架構1

開啟這個思路後,你會發現幾乎其他所有問題,都可以用這個方案來解了!為專有的問題領域建立專有的資料來源,同時建立資料來源到元件資料來源的對映關係。即能擴充套件能力,又能分離程式碼。

我們再看許可權控制的例子。如果使用者不具有某許可權時就把button disable 掉,對映關係我們可以寫成:

非常直觀。

再看錶單驗證狀態。建立驗證資料結果的資料來源,讓使用者配置哪些元件需要進行校驗,校驗時機(例如正在輸入或者離開焦點時)。例如:

validator 對映寫法的和前面的例子異區同工,使用者希望的當然是我只需要告訴你什麼情況下是通過,什麼不通過即可,同時也可以加上一些必要的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 的設計能夠約束變化來源,遮蔽來源細節,同時寫程式碼時能把所有變化和資料本身寫在一起,解決“資料在哪裡,被怎麼了”的問題。然而我們更傾向於像使用者暴露更少的概念,讓他用直覺來使用,由開發環境解決可維護性等問題。這是其實是產品策略,和技術爭論無關。

相關文章