我來聊聊前端應用表現層抽象

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文首發於"},{"type":"link","attrs":{"href":"https://ourai.ws/","title":""},"content":[{"type":"text","text":"歐雷流"}]},{"type":"text","text":"。由於我會時不時對文章進行補充、修正和潤色,爲了保證所看到的是最新版本,請閱讀"},{"type":"link","attrs":{"href":"https://ourai.ws/posts/abstraction-of-frontend-application-presentation-layer/","title":""},"content":[{"type":"text","text":"原文"}]},{"type":"text","text":"。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們處於變化很快的時代,無論是商業還是科技。一家公司看上去商業很成功,也許前腳剛上市,後腳就因爲什麼而退市,甚至倒閉;一項看似高大上的技術橫空出世,各類媒體爭先恐後地撰文介紹,熱度炒得老高,沒準沒多久就出現了競爭者、替代者。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這樣的大環境下,傳統的「web 前端開發」演變成了「泛客戶端開發」,前端開發者從「配置工程師」被「逼」成了「軟件工程師」。開發變得更復雜了,要處理的問題更多了,從業難度不知提升了多少倍——前端早就不再簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在衆多必須要處理的問題中的一個,就是表現層運行環境的兼容問題,像跨瀏覽器和跨端、平臺、技術棧。注意,這裏說的是「表現層」而不是「視圖層」。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"「表現層」與「視圖層」"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「表現層」的英文是「presentation tier」或「presentation layer」,具體是哪個取決於是物理上還是邏輯上劃分;而「視圖層」的英文是「view」。「表現層」是「視圖層」的超集,根據前端應用的架構設計,它們既可以不等又可以相等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"表現層"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「表現層」這個詞出自經典的三層架構(或多層架構),是其中一個分層。三層架構包括數據層、邏輯層和表現層,一般用在 C/S 架構中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a4ee334d0ae12f947122f99b2a409416.png","alt":null,"title":"三層架構","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼會在這篇講前端開發的文章中提到它?這是因爲,雖然在一些前端應用中用不到,尤其是快餐式應用,但在企業級複雜前端應用中就十分需要一個前端的「三層架構」。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"視圖層"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「視圖層」則來自表現層常用的「model-view-whatever」模式中的「view」,即「視圖」。至於說的時候在「視圖」後面加個「層」字合不合適,就不在這裏討論了,文中皆使用「視圖層」這個詞。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"運行環境兼容"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"跨瀏覽器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於各瀏覽器廠商對標準實現的不一致以及瀏覽器的版本等原因,會導致特性支持不同、界面顯示 bug 等問題的出現。但慶幸的是,他們基本是按照標準來的,所以在開發時源碼的語法幾乎沒什麼不同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所謂的「跨瀏覽器」實際上就是利用瀏覽器額外的私有特性和技術或輔以 JS 對瀏覽器的 bug 進行「修正」與功能支持。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"跨端、平臺、技術棧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,絕大部分的前端開發者是在做泛客戶端開發——開發 web 應用、客戶端應用和各類小程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在做 web 應用時需要考慮 PC 端和移動端是分開還是適配?技術選型是用 React、Vue?還是用 Web Components?或是用其他的?做客戶端應用、各類小程序時這些也會面臨技術選型的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果公司某個業務的功能覆蓋了上述所有場景,該如何去支撐?與跨瀏覽器不同的是,不同端、平臺、技術棧的源碼語法不一樣,要滿足業務需求就得各開發一遍。然而,這顯然成本過高,並且風險也有些大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,要怎麼解決這個問題呢?從源頭出發。根本的源頭是業務場景,然後是產品設計,但這些都不是開發人員可掌控的,幾乎無法改變。能夠完全被開發人員所左右的基本只有開發階段的事情,那就從這個階段的源頭入手——源碼編寫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"若是與業務相關的代碼只需編寫一次就能運行在不同的端、平臺、技術棧上,那真是太棒了!這將會大大地降低成本並減少風險!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"表現層的抽象"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了達到跨端、平臺、技術棧的目的,需要將表現層再劃分爲抽象層、運行層和適配層。其中,抽象層是爲了統一源碼的編寫方式,可以是 DSL、配置等,它是一種協議或約定;運行層就是需要被「跨」的端、平臺、技術棧;適配層則是將抽象層的產物轉換爲運行層正常運行所需要的形式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表現層中可以被抽象的大概有視圖結構、組件外觀、組件行爲等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"視圖結構"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 web 前端開發中,HTML 就是一種視圖結構的抽象,描述了界面中都有什麼,以及它們之間的層級關係。最終的顯示需要瀏覽器解析 HTML 後調用操作系統的 GUI 工具庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於業務支撐來說,無論是 HTML 還是其他什麼拼湊界面的方式,相對來說比較低級(是「low level」而不是「low」),視圖單元的劃分粒度比較細,在開發界面時就會花費更多的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們需要一種能夠屏蔽一些不必關注的細節的視圖結構抽象,在這個抽象中,每個視圖單元都有着其在業務上的意義,而不是有沒有都可以的角色。具體做法請看下文。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"組件外觀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大部分已存在的組件的視覺呈現是固定的,即某個組件的尺寸、形狀、顏色、字體等無法被定製。如果同樣的交互只是因爲視覺上有所差異就要重新寫組件,或者在組件外部重新寫份樣式進行覆蓋,那未免也太痛苦了……"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以將那些希望能夠被定製的視覺呈現抽象成「主題」的一部分,這部分可以被叫做「皮膚」。在進行定製時,分爲線下和線上兩種方式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「線下」是指在應用部署前的開發階段進行處理。在前端構建工具豐富的現在,寫頁面樣式時已經不會去直接寫 CSS,而是像 Sass 這種可編程式的預處理器。這樣就可以抽取出一些控制視覺呈現的 Sass 變量,需要定製時通過在外部對變量賦值進行覆蓋,而不需要費勁重寫組件或樣式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"「線上」則是部署後根據運行時數據動態改變。在皮膚定製即時預覽和低代碼平臺等場景,是基本沒機會去修改 Sass 變量並走一遍構建流程的,即使技術上能夠辦到。藉助 CSS 自定義屬性(CSS 變量)的力量可以較爲方便地做到視覺呈現的運行時變更。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"組件行爲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"組件除了外觀,其行爲也應當是可以定製的。看到「行爲」這個詞,第一反應就是跟用戶操作相關的事情,然而這裏還包括與組件內部結構相關的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於組件的外部來說,組件內部就是個黑盒子,其自身結構的組成部分有的可以被上文所說的視圖結構所控制,有的則無能爲力:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5c68d0676ec8dee0d5ce5cedd53b8415.jpeg","alt":null,"title":"搜索組件","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖是一個比較複雜的搜索組件,雖然外觀和佈局看起來有所不同,但「它們」確實是同一個組件。外觀不同的解決方案上面已經大體說明,這類視圖結構無法控制的佈局問題,需要枚舉場景後在組件內進行支持,然後作爲「主題」的一部分存在。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跟用戶操作相關的行爲有組件自身的交互規則及與業務邏輯的結合這兩類。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"交互規則又有兩種:一種是像表單是在字段值發生改變時就校驗還是在點擊按鈕時校驗這樣;另一種是像字段值是在輸入框的值改變("},{"type":"codeinline","content":[{"type":"text","text":"input"}]},{"type":"text","text":" 事件)時更新還是失焦("},{"type":"codeinline","content":[{"type":"text","text":"change"}]},{"type":"text","text":" 事件)時更新這樣,或是像下拉菜單的彈出層是在懸停("},{"type":"codeinline","content":[{"type":"text","text":"hover"}]},{"type":"text","text":" 事件)時出現還是點擊("},{"type":"codeinline","content":[{"type":"text","text":"click"}]},{"type":"text","text":" 事件)時出現這樣。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前者的解決方式與上面說的視圖結構無法控制的佈局問題差不多,後者則是需要組件支持事件映射,即外部可以指定組件某些交互的觸發事件。當然,這兩者同樣也可以作爲「主題」的一部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在寫組件時有件事是需要極力避免卻往往難以避免——組件中耦合業務邏輯。組件決定的應該只是外貌與交互形態,裏面只有交互邏輯及控制展現的狀態,不應該牽扯到任何具體業務相關的邏輯。只要長得一樣、操作一樣,那麼就應該是同一個組件,具體業務相關的邏輯注入進去。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段十分「個性化」的業務邏輯,說白了就是響應用戶操作的變化以及業務數據的變化去更改組件內部的狀態:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n // 組件事件\n events: {\n // 組件的一個點擊事件\n 'click-a': function() {},\n // 組件的另一個點擊事件\n 'click-b': function() {},\n // 組件的一個改變事件\n 'change-c': function() {},\n },\n // 業務數據變化的回調\n watch: function( contextValue ) {},\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行時會注入一個上下文給上述對象方法的 "},{"type":"codeinline","content":[{"type":"text","text":"this"}]},{"type":"text","text":",組件還可以添加工具方法給上下文。該上下文的內置屬性與方法有:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"interface IDomainSpecificComponentContext {\n getState(key: string): any;\n setState(key: string, value: any): void;\n setState(stateMap: { [key: string]: any }): void;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"視圖結構描述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面說了我們需要一種比 HTML 之類的更進一步的視圖結構抽象,下面就來說說這部分的大體思路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"技術選型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在做視圖結構抽象時最常用到的技術就是 XML-based 或 XML-like 以及 JSON-based 的某種技術。XML-base 和 XML-like 的技術都是符合 XML 語法的,唯一的區別是前者要完全符合 XML 的標準規範,像 Angular 和 Vue 的模板就是後者;同樣的,JSON-based 的技術是完全符合 JSON 的標準規範的技術,像 JSON Schema。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自從 React 問世以來,其帶來的 XML-like 的 JSX 也會被用於視圖結構抽象,但基本僅限於編輯時(edit time)。一段 JSX 代碼並不是純聲明式的,作爲視圖結構描述來說可讀性較低,解析難度較高,並且通用性很低。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JSON-based 的技術對前端運行時最爲友好,解析成本幾乎爲零;相反的,其可讀性很低,JSON 結構是縱向增長的,指定區域內的表達力十分受限,無法很直觀地看出層級關係與視圖單元的屬性:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"tag\": \"view\",\n \"attrs\": {\n \"widget\": \"form\"\n },\n \"children\": [{\n \"tag\": \"group\",\n \"attrs\": {\n \"title\": \"基本信息\",\n \"widget\": \"fieldset\"\n },\n \"children\": [{\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"name\",\n \"label\": \"姓名\",\n \"widget\": \"input\"\n }\n }, {\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"gender\",\n \"label\": \"性別\",\n \"widget\": \"radio\"\n }\n }, {\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"age\",\n \"label\": \"年齡\",\n \"widget\": \"number\"\n }\n }, {\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"birthday\",\n \"label\": \"生日\",\n \"widget\": \"date-picker\"\n }\n }]\n }, {\n \"tag\": \"group\",\n \"attrs\": {\n \"title\": \"寵物\",\n \"widget\": \"fieldset\"\n },\n \"children\": [{\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"dogs\",\n \"label\": \"🐶\",\n \"widget\": \"select\"\n }\n }, {\n \"tag\": \"field\",\n \"attrs\": {\n \"name\": \"cats\",\n \"label\": \"🐱\",\n \"widget\": \"select\"\n }\n }]\n }]\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果一個應用的設計是不需要人工寫視圖結構描述的話,可以考慮使用 JSON-based 的技術。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"像 Angular 和 Vue 的模板那種 XML-like 的技術是相對來說最適合做視圖結構描述的——純聲明式,結構是向水平與垂直兩個方向增長,無論是可讀性還是表達力都更強,解析難度適中,並且具備通用性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的模板代碼所描述的內容與上面那段 JSON 代碼一模一樣,深呼吸,好好感受一下兩者之間的差異:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n \n \n \n \n \n \n \n \n \n \n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,視圖結構描述最終該選用哪種技術,想必無須多言。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/55/558fa4697b94a646bbb4aaff0bf11bbe.png","alt":null,"title":"雞哥(小雞)","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"設計思路"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"毋庸置疑,模板的語法要符合 XML 語法是前提,再在此基礎上根據需求進行定製、擴展。首先要定義標籤集。所謂的「標籤集」就是一個元素庫,其中的每個元素都要具備一定語義,使其在業務上有存在意義。然後是制定描述元素的 schema 並實現其對應的解析、校驗等邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"元素 schema 大概是長這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"// 屬性值類型\ntype PropType = 'boolean' | 'number' | 'string' | 'regexp' | 'json';\n\n// 屬性描述符\ntype PropDescriptor = {\n type: PropType | PropType[];\n required: boolean; // 是否必需\n};\n\n// 元素 schema\ntype ElementSchema = {\n name: string; // 元素名\n tag?: string; // 標籤名,不指定時取元素名\n props?: {\n [key: string]: PropDescriptor;\n };\n attrs?: {\n resolve: (key: string, val: any) => any;\n };\n // 節點行爲,是作爲父節點的子節點還是屬性存在\n behavior?: {\n type: 'append' | 'attach';\n // 以下都用於 `type` 是 `'attach'` 時\n host?: string; // 宿主(屬性名)\n keyed?: boolean; // 是否爲鍵值對集合,值爲 `true` 且 `merge` 爲 `false` 時以節點 ID 爲鍵\n merge?: boolean; // 當值爲 `true` 時將 `reduce` 的返回值與 `host` 指定的屬性的值進行合併後重新賦值給 `host`\n reduce?: (node: ITemplateNode) => any; // 轉換節點信息\n restore?: (reduced: any, node?: ITemplateNode) => ITemplateNode | Partial;\n };\n};"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到 schema 中有 "},{"type":"codeinline","content":[{"type":"text","text":"props"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"attrs"}]},{"type":"text","text":",它們共同組成了模板元素的屬性(XML attributes),區別是:模板解析後的屬性如果是在 "},{"type":"codeinline","content":[{"type":"text","text":"props"}]},{"type":"text","text":" 中定義的並且滿足屬性描述符的 "},{"type":"codeinline","content":[{"type":"text","text":"type"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"required"}]},{"type":"text","text":" 所指定的限制條件,會成爲模板節點的 "},{"type":"codeinline","content":[{"type":"text","text":"props"}]},{"type":"text","text":" 屬性;剩餘沒在 "},{"type":"codeinline","content":[{"type":"text","text":"props"}]},{"type":"text","text":" 中定義的則成爲模板節點的 "},{"type":"codeinline","content":[{"type":"text","text":"attrs"}]},{"type":"text","text":" 屬性,通過 "},{"type":"codeinline","content":[{"type":"text","text":"resolve"}]},{"type":"text","text":" 方法能夠對屬性根據自己的規則進行值的轉換。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然在模板中元素總是以嵌套的形式展示出層級關係,但一個元素並不一定就是其父級的結構,還可能是配置。因此,元素 schema 中的 "},{"type":"codeinline","content":[{"type":"text","text":"behavior"}]},{"type":"text","text":" 用於設置當前元素在模板解析後是作爲一個節點的子節點存在還是作爲某個屬性存在。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述的模板設計是純視圖結構描述的,並且只對元素這種「塊」進行處理,我認爲這樣夠用了。根據情況,可以擴展爲像 Angular 和 Vue 的模板那樣支持文本、插值和指令等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果懶癌發作並且沒什麼特殊需求,模板解析的工作可以交給魔改後的 Vue 2.6 編譯器,再適配爲模板節點樹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個模板節點的結構大致爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"interface ITemplateNode {\n id: string;\n name: string;\n tag: string;\n props: {\n [key: string]: any;\n };\n attrs: {\n [key: string]: any;\n };\n parent: ITemplateNode | null;\n children: ITemplateNode[];\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,通過適配層將模板節點樹轉爲運行層的組件樹,並把渲染的控制權也轉交給了最終的運行環境。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一個複雜的前端應用中,如果不對其進行分層,那它的擴展性和可維護性等真的會不忍直視……通常是採用經典的三層架構,從下到上分別爲數據層、邏輯層和表現層。本文以表現層爲例,將其再次劃分出抽象層、運行層和適配層這三層,實際上數據層和邏輯層也可以套用這種模式——就像在生日蛋糕上切上四刀——我稱其爲「九宮格」模型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a7/a77fc8a8ad175da4b9e4fa16d1b6f163.jpeg","alt":null,"title":"「九宮格」模型","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在表現層的各種抽象中,本文着重闡述了視圖結構描述的技術選型與設計思路,可以看出 XML-like 的模板從編寫到解析再到渲染這一整條流程,與 Angular 和 Vue 的模板及 HTML 大體上一致;其他抽象只是稍微提了提,以後有機會再展開來說。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前也寫過幾篇與模板相關的文章:從提效角度與「面向組件」做對比的《"},{"type":"link","attrs":{"href":"https://ourai.ws/posts/template-based-frontend-web-development/","title":null},"content":[{"type":"text","text":"我來聊聊面向模板的前端開發"}]},{"type":"text","text":"》;從可定製性角度講的《"},{"type":"link","attrs":{"href":"https://ourai.ws/posts/configuration-driven-frontend-web-development/","title":null},"content":[{"type":"text","text":"我來聊聊配置驅動的視圖開發"}]},{"type":"text","text":"》;從低代碼平臺的核心理念「模型驅動」出發的《"},{"type":"link","attrs":{"href":"https://ourai.ws/posts/model-driven-frontend-web-development/","title":null},"content":[{"type":"text","text":"我來聊聊模型驅動的前端開發"}]},{"type":"text","text":"》。可以說,本文的內容是它們有關表現層描述的「根基」。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"無論一家公司是不是做低代碼平臺的,或者內部有沒有低代碼平臺,都應該從表現層抽象出視圖結構描述,至少要有如此意識。"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":"center","origin":null},"content":[{"type":"text","marks":[{"type":"size","attrs":{"size":12}}],"text":"歡迎關注微信公衆號以及時閱讀最新的技術文章"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/15/15ca057e8c81c613f5a34d41b84b7125.jpeg","alt":null,"title":"Coding as Hobby","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章