Categories
程式開發

面向頁面的移動端架構設計


前言

本文非常長,閱讀需要勇氣。

作者嘗試在移動端總結出一套面向頁面的架構設計,暫定命名為POA(page-oriented architecture),因為核心的關注點在於page,閱讀本文更多的是了解移動端架構的方式方法。

另外,本文主要是方法論層面的闡述,具體案例因為每一種編程語言的不一樣實現會有所不同,所以文中代碼均為偽代碼。作者已經部分實現這套方案,但具體的實現並不重要,重要的是希望這套方法論能給讀者帶來一些收穫。

定義

面向頁面的架構的定義:

以頁面為自治單元,定義頁面的唯一標識,對頁面進行模塊化,解決頁面的註冊、頁面棧的治理、頁面間通信、頁面的分層架構、頁面的組件化等問題。

這裡要先給出模塊和組件的相關的定義,本文中所說的這些概念均基於以下的定義:

模塊化,根據業務領域的劃分進行頁面的劃分的過程。模塊,模塊化的產物,模塊盡量保持自治;頁面,頁面的構成基本上依賴於產品的設計,不會像模塊化或者組件化一樣存在劃分的靈活性,頁面由組件構成,可以且必須保持自治;組件化,根據業務上的通用程度對頁面佈局進行劃分的過程。組件,組件化的產物,組件不強調自治,但能自治當然更好,組件之間需要進行組合,最終由多個組件組合成一個頁面,組件依賴於頁面進行部署到App上。

以頁面為自治單元

為什麼要以頁面為自治單元呢?

首先理解下自治單元,在SOA 的場景下,一個service 是一個自治單元,但是service 的劃分是無法標準化的,這增加了SOA 架構設計的難度,直到出現FAAS(function as a service)出現,function 是一個很好標準化的自治單元,極大降低了SOA 架構設計的不確定性。

那麼以頁面為自治單元有以下一些原因:

頁面是天然可標準化的自治單元,模塊和組件的劃分都存在較多不確定性; 如果以模塊為自治單元,還要徒增模塊間通信的複雜性,且模塊劃分不合理可能導致模塊間的通信過於復雜; 如果以組件為自治單元,不只是組件的劃分的不確定性,組件間的依賴如何解耦也是另外一個難題了; 頁面的路由是必不可少的工作,頁面間傳參可以認為是頁面路由的必備功能,對頁面間傳參的能力進行擴展,就可以實現頁面間的通信; 頁面可路由的一個前提是頁面具有唯一標識(下面會講到),而頁面間通信剛好可以服用這個唯一標識,無序增加更多複雜性; 移動端App天然是頁面的集合,以頁面為自治單元可以極大的增強頁面的可複用性,當然這不是目的,僅僅是一個副作用。

下面附上為POA設計的模塊、頁面、組件的物理架構圖模板,物理架構是樹形結構的,從模塊到頁面,從頁面到組件,都是如此。

面向頁面的移動端架構設計 1

以頁面為自治單元是本文的 POA 架構設計的基石。

頁面的唯一標識

在前端領域,一個頁面必然對應一個URL,但是在移動端領域,這個標準不存在的,雖然iOS 和Android 系統自身會提供一些URL 來調用系統頁面的能力,但更多時候我們需要自己寫一個UIViewController或者Activity,然後直接通過依賴其類型來打開頁面,這就造成了頁面間更多的依賴性。

為了解耦頁面間的依賴,直接採納前端的方案,引入 URL 作為頁面的唯一標識。通過這個唯一標識,我們可以打開、關閉頁面,也可以跟頁面進行通信。

需要注意的是,這個唯一標識只是在當前系統上下文內是唯一的,跨系統的情形不在本文的套路範疇之內,或者你可以認為我們主要討論的是內鏈,跨系統的頁面URL 是外鏈,內鏈默認都是可信的,外鏈才需要關注安全性。外鏈可以通過轉發內鏈來打開頁面。

對 URL 的具體定義可以根據頁面在樹上的路徑來,類似於如下的規則:

/[module]/[page]/ /[module]/[submodule]/[page]

當頁面更複雜的時候,可能需要增加一個 feature 路徑

/[module]/[feature]/[page]/ /[module]/[submodule]/[feature]/[page]

頁面的模塊化

頁面的模塊化,我們需要決定哪些頁面應該分在哪個模塊下面,前面也提到,這是一個存在不確定性的過程。

如何進行模塊化

從業務的角度,我們很多時候需要通過面向對象的思想來分析,抽象名詞,選出合適的名詞作為關鍵實體,關鍵實體可能就是我們需要劃分出來的模塊。再往深一點看,我們需要對業務進行建模,這裡不展開,我了解也比較膚淺。

模塊劃分之後,有一個辦法可以用來驗證模塊劃分的合理性。首先模塊是由頁面組成的,當我們拋棄模塊之間臆想出來的依賴之後,剩下的就是這些實實在在物理層面的頁面依賴,根據下面的方式來驗證:

如果兩個模塊之間存在較多的頁面依賴,那麼這兩個模塊的頁面劃分可能是存在問題的;如果一個模塊內部存在兩組以上基本不依賴的頁面,那這個模塊可能需要繼續進行拆分。

模塊化應遵循的原則

模塊的劃分,在物理層面應該是可以獨立發布的包,所以同樣適用於以下設計原則,wiki“:

重用發布等價原則(REP)共同封閉原則(CRP)共同重用原則(CCP)無環依賴原則(ADP)穩定依賴原則(SDP)穩定抽象原則(SAP)

前三個原則,較簡單,不做闡述,後三個原則在後續章節會有更詳細的說明。

模塊的模塊化編程

模塊化編程的具體含義可以參考 wiki“。

從我們對頁面的定義看,一個頁面本身就可以是一個最小的模塊,頁面座位自治單元,完全可以遵循模塊化編程的規範,那麼模塊自然就很好遵循模塊化編程的規範。模塊的接口即是所有模塊包含的頁面。最小的模塊可以只包含一個頁面。遵循模塊化編程可以讓模塊具有以下優點:

易設計,模塊經過劃分之後,使得模塊本身的設計變得更為簡單,因為需要關注的範圍變小了,需要解決的問題變得簡單了; 易實現,模塊化適合團隊開發,實現模塊的成員不需要了解整個系統的全貌,人員的更替也不會帶來很高的成本; 易測試,模塊可以獨立開發,也可以獨立測試,到集成測試階段才需要對整個系統進行測試; 易擴展,增加新功能只需要增加新模塊或子模塊,偶爾對現有模塊的重新劃分(就是對模塊進行重構),在我們的場景下面,頁面本身已經是自治單元了,模塊重新劃分的成本還是比較低的,只是對頁面的重新編排; 易重用,以頁面為自治單元的一個副作用,就是更容易重用頁面,合理的模塊化也使得模塊易於重用。

模塊間的依賴

頁面間的依賴通過路由系統可以解耦掉,但模塊間的依賴是不可避免的。

模塊間應避免環依賴

因為最終整個系統要由各個模塊構成,系統會直接或者間接的依賴於所有模塊。模塊間的依賴不能違反無環依賴原則(ADP),這個在模塊劃分之前就應該解決掉。

模塊間理想的依賴

那麼我們充電來看看如何減小模塊之間的依賴。從前文的系統物理架構上看,整個系統的實際上是一個樹形結構,我們對模塊間的依賴要通過系統的羅架構圖來描述,最理想的情況下,邏輯架構圖跟物理架構圖是一致的。但現實的業務系統中這是不可能存在的。

知道了理想情況是什麼後,我們所能做的就是如何往理想的情況去設計就可以了。如何做到呢,其實也不算多深奧吧,前問已經提的穩定依賴原則和穩定抽象原則。

怎麼樣才能算是穩定依賴呢?一個模塊如果不依賴任何模塊,那就是最穩定的模塊,從整個上下文看,單一頁面的模塊是最穩定的(頁面本身依賴的外部代碼不在模塊化的範疇之內)。另外頁面變更的可能性在整個系統的生命週期中非常少,所以可以認為單一頁面的模塊是穩定模塊,但如果我們每個頁面都搞成一個模塊,這個反而可能增加了整個系統的模塊間依賴複雜度。

模塊間依賴的量化

要對模塊間依賴進行量化,可以通過不穩定度和抽象度來進行。

不穩定度的計算公式:I=Ce/(Ce+Ca)

Ce:代表離心耦合(Efferent Coupling)模塊依賴的外部模塊的數量

Ca:代表向心耦合(Afferent Coupling)模塊被依賴的外部模塊的數量

I:不穩定度,介於[0,1]

當Ce = 0的時候,模塊被依賴的外部模塊數量為0,結果是 I = 0,表示該模塊最穩定。

當Ca = 0的時候,模塊依賴的外部模塊數量為0,結果是 I = 1,表示該模塊最不穩定。

下面的圖沿著箭頭的方向會(依賴的方向),數字是不穩定度的值,穩定性逐漸升高。

比較簡單的場景如下,依賴層級為1的情況如下圖,整個App永遠是最不穩定的

面向頁面的移動端架構設計 2

依賴層級為2的情況如下圖,當增加依賴之後,比如 Module1、Module2 的不穩定度都增加了,依賴越多,不穩定度增加的越高,所以要盡量最少依賴。

面向頁面的移動端架構設計 3

依賴層級為2,且產生菱形依賴的情況如下圖,Module6 被Module2 和Module3 同時依賴,問題好像不大,因為Module6的不穩定度未發生變化,Module3 因為要依賴Module6,所以不穩定度從0 變成了0.5。

面向頁面的移動端架構設計 4

我們再看看下圖中,如果 Module3 實際上只是依賴於 Module6,但通過依賴 Module2 間接的依賴於 Module6 也能達成目的。結果是,Module3的不穩定度一樣從 0 變成 0.5,Module2 的不穩定度反而從 0.6 變成了 0.5,變得更穩定了。

所以,在增加依賴關係的時候,應該選擇對更少的模塊的不穩定度產生影響的方案。通俗一點來說,可以認為是最小依賴。

面向頁面的移動端架構設計 5

抽象度的計算公式:A = Na / Nc

Na 表示模塊中抽像類的數量

Nc 表示模塊中所有類​​的數量

A 表示模塊的抽象度 Abstractness

抽像類在很多編程語言中都會存在,只是形式不一樣而已,比如:Objective C語言中的protocol,Java語言的Interface,dart語言的mixin和abstract類等。

但在本文中,需要對抽象度做更進一步的說明,我們的模塊主要是由頁面來組成的,如何說明抽象度是一個需要解決的問題。在前文中我們也提到通過 URL 來打開、關閉頁面,或者與其它頁面進行通信,那麼抽象的計算可以通過 需要定義 URL 的頁面數量除以 所有頁面的數量來表示。這裡所說的需要定義 URL 的頁面,表示這些頁面會由其它模塊來打開、關閉、或者與其進行通信的頁面。所以對抽象度的計算公式,Na 表示模塊中被外部模塊依賴的頁面,Nc 表示模塊中所有頁面的數量。

面向頁面的移動端架構設計 6

通過結合抽象度和不平衡度,我們得到上圖的四個極端情況:

當不穩定度為0,抽象度為1的時候,這種情況一般是單頁模塊,且處於依賴樹的葉子上; 當不穩定度為1,抽象度為0的時候,這種情況一般是系統自己,處於依賴樹的根部; 當不穩定度為1,抽象度為1的時候,這種情況一樣是不存在的,無效區域; 當不穩定度為0,抽象度為0的時候,這種情況下依賴了這個模塊也是無用的。

一個合理的模塊,應該處於平衡線的附近,需要注意的時候,很多人可能認為單頁模塊會是最好最簡單的模塊劃分方式,理論上可以這麼認為,但實際上我們一般不會這麼做。開發成本過高,系統限制等等因素導致我們幾乎不可能做到。

再進一步的,我可以通過計算與平衡線的距離佔比來計算平衡度 B,

公式如下:B = abs(1-I-A)

B介於[0,1]之間B = 0 的時候表示在平衡線上B = 1 的時候表示無用或無效B 的值越小越好

雖然我們可以通過量化的數值來一定程度的表示模塊的合理性,但現實中模塊的依賴關係不會如此單純,依然需要從不同測切面切入去理解各個切面下模塊的合理性,所謂全局的合理性是不存在的,或者計算出來也是沒有意義的。

頁面的註冊

我們在頁面的唯一標識” 中談到了對頁面的URL,這是頁面間解耦依賴的方式,使得我們可以通過URL 就能打開、關閉頁面或者與頁面進行通信。我們下面是來一步步看看這種解耦方式的細節。

那麼通過 URL 我們如何才能打開一個頁面呢?關閉頁面和與頁面進行通信會在 頁面的棧” 相關的章節中有說明。

頁面註冊表

在移動端,如果是 web 的 URL,我們需要調起嵌套 webview 的頁面來打開這個 URL,如果指向的是原生的頁面,處理方式是找到 URL 對應的頁面並打開。那這裡就存在一個 URL 和 頁面之間的對應關係,頁面的註冊 就是將 URL 對應到頁面的行為,並產生一個 URL -> Page 的註冊表,在圖表中名為 PageRegister。 PageRegister 可以讓我們不必依賴具體頁面的模塊也可以間接打開頁面。

如下圖,PageRegister 成為一個穩定包存在,Module是模塊化的需要依賴的包,Module依賴於PageRegister 來實現註冊註冊頁面的切面,其它所有模塊依賴於Module 包,當一個模塊需要註冊頁面的時候,我們只需要依賴於Module 就可以了。

面向頁面的移動端架構設計 7

模塊掛載樹

提供了頁面註冊表,我們還要解決在哪裡註冊頁面的問題。為此,沿著模塊的物理架構,增加一個模塊的掛載樹。

面向頁面的移動端架構設計 8

從圖上看,掛載樹就是模塊部分的結構,沿著箭頭的方向,將所有模塊一一掛載到樹上。父模塊負責掛載子模塊。

註冊頁面的切面

有了模塊掛載樹,我們可以在模塊之上增加一個切面,用來註冊頁面。當一個模塊需要註冊頁面的時候,可以通過繼承的方式增加一個註冊頁面的函數入口,這個函數沿著上一節圖中的箭頭方向一一調用每個模塊的註冊頁面的函數。

註冊頁面的函數中可將模塊下面的所有頁面都註冊到註冊表中。

模塊的其它切面

有了模塊掛載樹,我們可以對模塊增加各種切面,默認情況會提供 模塊初始化切面,也可以增加 模塊異步初始化切面等。

如果你的編程語言限制泛型的實例化,比如 dart語言,你可能還需要增加 JSON解析器的註冊切面。

本質上是一些列的初始化行為,但通過切面的形式進行細分,職責更明確。

頁面的棧

前面章節中,我們把頁面的模塊化、頁面的註冊都做了說明,接下來的核心關注點就是頁面的棧,頁面棧上都是頁面的實例。

邏輯頁面棧

在頁面結構簡單的App上,一般只有一個頁面棧,但很多App其實不只是存在一個頁面棧,比如在iOS中,首頁底部有tab菜單的,每個tab都可能有一個自己的 UINavigationController。這個可以認為是頁面的物理結構上的頁面棧,因為我們無法抹平不同系統不同App頁面棧設計上的差異,所以我們不去關注頁面的物理頁面棧,頁面通常情況下以一個後進先出的方式出現在屏幕上(其他場景不在本文討論的頁面棧的範疇),所以我們這裡討論的頁面棧可以認為是一個邏輯頁面棧。

後續文中所說的頁面棧,都是指邏輯頁面棧。在圖表中用詞為 PageStack。

打開頁面

承接上面 頁面註冊表” 的圖,打開頁面的依賴關係圖如下:

每個模塊必然依賴於 Module 包,因為頁面都要註冊到註冊表中。幾乎每個模塊都需要打開頁面,依賴於 PageStack 包來獲得打開頁面的能力。

面向頁面的移動端架構設計 9

當一個頁面被打開多次的時候,在頁面棧上會有多個頁面的實例。

打開頁面的接口,push 將頁面推入頁面棧,需傳入頁面 URL,可傳入參數。

func push(String url, T params) -> (Bool) -> Void

func push(String url, T params, (U params){}) -> (Bool) -> Void

我們可以提供在頁面棧上插入一個頁面的接口,insert 將頁面插入某個頁面之上或之下,需傳入參照頁面的URL,上面還是下面的標誌位,但有的系統不支持插入一個頁面到頁面棧上。

func insertBefore(String url, String refUrl, T params) -> (Bool) -> Void

func insertAfter(String url, String refUrl, T params) -> (Bool) -> Void

關閉頁面

頁面關閉的邏輯架構圖如下,只需要依賴於 PageStack 就可以了。

面向頁面的移動端架構設計 10

頁面關閉的接口,pop, remove:

pop是關閉當前棧頂的頁面,無需傳入頁面 URL,可以傳入回調參數給打開被關閉頁面的 push 方法;remove是關閉一個特定頁面,需傳入該頁面的 URL。

func pop(T params) -> (Bool) -> Void

func remove(String url) -> (Bool) -> Void

以上需要傳入 URL 的場景,如果頁面棧中存在多個對應於 URL 的實例,則定為最頂部找到的頁面實例。

頁面間通信的方式

頁面間通信,實際上在很多路由庫上就存在,包括由編程框架本身提供的也存在頁面間通信。

我們把以下情形也當成頁面間通信:

push 的時候帶上參數給將被打開的頁面pop 的時候帶上參數回傳給打開這個頁面的 push 方法的回調

這兩種情形適用場景有限,能滿足打開與被打開頁面之間的通信,pop 回傳參數的優勢是不需要明確在哪個頁面被打開了。這裡需要解釋下,我們把頁面當成自治單元,那默認為我們的 push 也應該發生在某個頁面中。

我們還需要與任意一個頁面都能通信的能力,比較常見的場景是,一個列表上,點開一項的詳情頁,然後進入處理頁,處理完了,我需要告訴詳情頁和列表頁發生了什麼,是否需要刷新頁面上的數據。這裡實際上會需要同時與兩個頁面進行通信。

總的來說,我們還需要一個能發送參數給任意頁面的一個通訊方式,包括發送端和接收端,偽代碼如下:

發送端

func notify(String url, String name, T params) -> (Bool) -> Void

接收端,分不同的方式,如果是類似 Flutter 的實現方式,最好是提供一個 Widget 來接收,因為 Widget 可以插到任何一個 Widget 樹的節點上,有效控制頁面刷新範圍,偽代碼如下:

typedef WillNotify = T -> void;

class WillNotifyScope extends StatefulWidget {
const WillNotifyScope({
Key key,
@required this.name,
@required this.willNotify,
@required this.child,
});
final String name;
final WillNotify willNotify;
final Widget child;
}

如果是原生端,實現方式需要依賴於 UIViewController 和 Activity,通過協議的繼承,讓一個頁面決定自己是否要接收通知,如果接收就繼承協議。

interface OnNotifyListener {
func onNotify(String name, T params)
}

頁面棧的跨平台實現

大多數場景下,我們都是在一個平台內處理頁面棧,但如果我們引入了Flutter、H5的話,這一整套關於頁面棧的實現就會非常複雜了,具體的實現細節可能需要實踐之後才能完整闡述。

為了實現跨平台的頁面操作和頁面間通信,我們需要適配 Flutter 和 H5,並在三端提供統一的頁面操作和頁面間通信的API。

不遺餘力的要實現這樣一套框架,帶來的好處是值得的:

開發者不再需要處理跨平台的通信,H5不需要自己寫一堆JSBridge 方法,Flutter不需要寫各種Channel方法;能得到一個統一的頁面棧,為跟踪頁面棧提供支撐,甚至回放頁面;讓H5頁面的交互更像原生,讓Flutter頁面的交互跟原生一致。

統一H5的頁面棧

為 web 頁面註冊 URL 到 PageRegister 。

在iOS下面,通過 WKWebview攔截這些 URL,然後全部使用嵌套了 WkWebview的 UIViewController 來打開,體驗上基本跟原生一致,但這就要求 H5 放棄自己的那一套頁面間通信的方式。

iOS下面開多個 WkWebview 的代價已經不算太高了,這種方式是可行的。

Android下面比較複雜,考慮復用 webview 的方式,以截圖來展示不在屏幕上的 Activity 也是可以實現的。當然不通過這種方式實現也是可以的,我們的目的是記錄一個完整的路由棧,統一三端的頁面操作和頁面間通信的 API。只要能達到這個目的就可以了。

統一Flutter的頁面棧

關於 Flutter 的實現這裡有一個可參考的樣例 flutter_thrio“,不做過多展開。

頁面棧的結構圖

面向頁面的移動端架構設計 11

頁面棧的依賴圖

下圖是頁面棧的依賴圖,

PageRegister 全局唯一一個,所有頁面最終在原生端註冊;WebModule 是Module 在web 端的實現,依賴於PageRegister;WebPageStack 是PageStack相似API 在web 端的實現,依賴於PageStack;FlutterModule 是Module 在Flutter 端的實現,依賴於PageRegister ;FlutterPageStack 是PageStack 相似的API 在Flutter 端的實現,依賴於PageStack。

面向頁面的移動端架構設計 12

頁面棧的數據流圖

push&pop的數據流圖

面向頁面的移動端架構設計 13

面向頁面的移動端架構設計 14

面向頁面的移動端架構設計 15

notify的數據流圖

面向頁面的移動端架構設計 16

面向頁面的移動端架構設計 17

面向頁面的移動端架構設計 18

從數據流看,頁面間不管是 push, pop, notify,數據流是幾乎是一致的,這在實現上可以做到流程的複用。

頁面棧的副作用

定位發生崩潰的頁面

頁面棧的日誌,在提高移動端App的穩定性的作用是非常大的。這麼比喻吧,微服務的分佈式調用鏈的意義有多大,頁面棧的日誌層面的作用就有多大。沒有頁面棧的日誌,我們在面對很多崩潰問題時,如果堆棧信息不足的話,解決的難度是很大的。頁面棧的日誌可以幫助定位發生崩潰的頁面,就是這麼簡單直接。

重放的頁面跳轉行為

頁面棧的日誌,在設計良好的情況下,完全可以用來重放頁面的跳轉行為。結合網絡請求日誌,幾乎可以重放整個App的行為。這個能力可以用來 實現自動化測試,或者 重現用戶的行為。

分析用戶的行為

有了頁面棧,附加網絡請求日誌,等於擁有了用戶的所有行為。在大數據層面的意義比各種無痕埋點更具有針對性,又比手動買點更自動化,而這僅僅是頁面棧的一個副作用。

頁面的邏輯分層架構

我們這裡說的分層架構是邏輯上的分層架構,不是傳統的物理分層架構,比如經典三層架構,領域驅動的四層架構。

邏輯分層架構包括,MVC、MVP、MVVM等,這些邏輯分層架構在各個編程框架上是不一樣,比如iOS上的MVC,Android上比較靈活,MVC、MVP、MVVM都有。

在 POA 的架構設計下,這些邏輯分層架構用哪一種已經顯得不是那麼重要。因為頁面已經足夠簡單,MVC很多時候也能很好的解決,MVVM反而過於復雜了。下一章節的頁面的組件化”中還會講到如何對複雜頁面進行組件化。所以即使存在一個很複雜的頁面,我們依然可以通過組件化來分解成更小的組件。

在POA架構設計模式,一個頁面可以使用MVC的方式來開發,也可以使用MVP或者MVVM,都是可以兼容的,因為頁面本身就是最小的應用程序,開發者可以決定使用任何一種方式。所以我們不用關注在POA下使用哪一種的好與不好。

當然,像很多更新的移動端開發技術,比如 Flutter,我們在開發頁面的時候,甚至不用關注這些邏輯分層架構,所以POA在很大程度上可以兼容未來出現的更多邏輯分層架構技術。

頁面的組件化

組件化是一個前端領域的概念,可以參看wiki“。

在移動端,沒有嚴格意義上的組件化,因為 iOS 和 Android 系統的事實標準都在蘋果和谷歌公司手裡,不是一個開放的標準。另外系統本身的複雜性也很難像開放的前端一樣,可以有各種標準,也可以一個人開發一個編程框架。

雖然沒有嚴格意義上的組件化,但當UI 佈局越來越複雜之後,頁面的拆分是一個必須要進行的事情,組件化只是一個指導我們如何將這個拆分做得更好的一個理論支撐。

頁面最終是由編程框架提供的各種控件組成的,當然自定義的自繪控件也在控件的範疇之內。控件不是組件,組件是控件的組合。組件可以有自己的邏輯分層架構,這會讓組件更限制於目前業務場景。

頁面組件化的目的

如果我們不抽象組件的話,那麼實際上,頁面 = 控件的堆積,所有的頁面我們都需要堆積控件,即使很多時候頁面中存在很多相似的塊,但我們都需要從控件開始堆積頁面。另外,編程框架本身提供的控件實際上是很基礎的,粒度很小,這無形中給我們帶來了非常多的重複的工作量。

組件化的出發點就是為了復用組件,這裡我們說的複用,主要是指在當前的業務場景下的組件復用。通過組件化,我們在開發頁面的時候可以更高效,頁面 = 組件的組合。

頁面組件化的方式

理解了頁面組件化的目的,為了達成目的,我們需要有一套針對頁面組件化的方式來支撐。

組件復用次數的提高

一個組件被復用的次數越多,一定程度上說明組件的複用性越好。但這不是全部,因為即使一個組件只被復用兩次,我們也需要拆分組件,甚至只有一次,也需要拆分。唯一的能改變的是,如何拆分組件能提高組件被復用的次數。

但組件的可複用程度,很多時候依賴於頁面設計時對業務的抽象程度,依賴頁面設計者的個人認知能力,無法在此展開。但我們可以判斷組件封裝的合理性。

組件封裝的合理性

組件的封裝數,可以由組成組件的組件或控件個數來判斷。下圖中,組件左上角數字為封裝數,右上角數字為組件的不穩定度。

面向頁面的移動端架構設計 19

封裝數不是越高越好,看組件0,封裝數為8,很明顯這會是一個很複雜的組件,更像是一個頁面,封裝數越高,組件的不穩定度就越難以降低,如下圖:

x:被復用次數,y:封裝數,所以封裝數過高,組件的不穩定度會難以降低。但封裝度過低,組件封裝的意義可能就不存在了,比如組件6,封裝數為2,且有一個控件在裡面,兩者很可能應該封裝成一個組件就可以了。

合理的封裝數要在這個區間內 (1,8),結合復用次數,加快組件不穩定度的降低速率。

面向頁面的移動端架構設計 20

寫在最後

作者對移動端架構的設想過於理想化,現實的架構很難實現真正的POA,但理論總是要總結的,理論可以指導我們如何在混亂的移動端架構領域尋找最佳實踐。

剝去太多的虛無和神秘,架構需要庖丁解牛,我們需要關注模塊、頁面、組件,還有代碼包,需要關注如何模塊化、頁面棧的治理、組件化,在無法標準化的移動端架構領域尋找可以標準化的一點一滴。

參考

web components

service-oriented architecture

function as a service

bang – iOS 組件化方案探索

domain model

domain driven design

package principles

modular programming

separation of concerns

multitier architecture

Difference between MVC and 3 tier Architecture