原文地址
題記:天貓App長大了,已經長成了流量以千萬計規模的App,當下至少有10個團隊在直接維護天貓App。在App長大,團隊擴充的過程中解耦是一個永恒的話題,而界面解耦又是App架構的重中之重。
統跳協議是天貓App統一跳轉協議,主要負責天貓App界面之間的串聯,也就是界面跳轉服務。Rewrite
引擎是與之配合的一套URL重寫引擎,可以通過配置實現重寫規則動態化。
歷史上的今天
統跳協議的前身是一套叫做internal的協議,internal要重點解決的問題是在WebView和推送通知中如何跳轉到指定的界面,進一步在任何動態場景下如何跳轉到指定界面。在這樣的思路下,internal中定義了多種協議格式,如:
tmall://tmallclient/?{"action":""}
internal:url=
link:url=
tmall://mobile.tmall.com/page/
幾乎每一種場景都有一種格式的協議與之對應。在具體操作過程中這些協議都以URL表現出來。不難看出,這套協議最大的問題在于協議格式異構化嚴重,且不符合W3C的URL標準。隨著App規模的擴大,場景日趨復雜,界面越來越多,這套協議的弊端也日益顯露。
而在天貓App開始從百萬級沖擊千萬級的時候,我們認識到一套格式統一,符合標準,規則簡潔的協議非常必要。這套協議的任務也絕不是解決固定場景跳轉,而是完全托管整個App的跳轉工作,從而實現全App界面解耦和跳轉動態化。因此,我們重新設計了界面協議,形成了當下這套規范——統跳協議。配合統跳協議,為了解決更多細節問題和跨平臺問題,我們還設計了Rewrite引擎與之配合。
統跳協議
統跳協議設計之初就保留了很強的可擴展性,為接下來更豐富的場景預留了能力。上文講到了統跳協議在界面跳轉中作用,而事實上界面跳轉僅僅是這套方案的一個典型場景,一個最佳實踐。界面跳轉在統跳協議的框架中被認為成一個服務,而跳轉到哪一個界面則是由服務內部實現決定的。
注冊一個服務
服務通過聲明URL的方式注冊到統跳協議中,這個聲明發生在服務所屬模塊內部的一個配置文件中,而這個配置文件被注冊到統跳協議里。也就是說,整個App中的每一個模塊都要注冊一個配置文件到統跳協議,統跳協議在初始化過程中會遍歷配置文件列表,逐一加載這些模塊配置,根據配置信息把一個一個的模塊服務注冊到協議中。
統跳協議要求調用服務的URL必須是符合W3C URL標準的,服務注冊使用的URL只能包括host和path兩部分,其中host是必須的,path則可選。當統跳協議接收一個跳轉請求的URL后,先根據該URL的host和path兩部分作為條件查找已注冊的服務,再初始化對應服務,把URL交給服務實例執行后續操作。
如何實現服務
統跳協議聲明了一個服務接口,這個接口中只有一個方法,服務必須由該接口實現而來。每一個服務可以通過實現接口中聲明的方法,使用參數中傳遞來的完整URL,參數列表和調用發起者指針,執行具體業務邏輯。
例如分享服務,以iOS為例:實現了TMShareUrlHandler
服務。
@interface TMShareUrlHandler : NSObject<AliAppURLHandler>
@end
@implementation TMShareUrlHandler
#pragma mark - URL調用分享組件
- (id)handleUrl:(NSURL *)url withTarget:(id)target withParams:(id)params {
// 省略代碼詳情
return nil;
}
@end
在分享模塊的配置文件中聲明該服務的URL為sharekit.tm/doShare
。
這份配置文件在分享模塊里:
分享模塊的配置文件sharekit_bundle.plist
也注冊到統跳協議中。
這份配置文件在統跳協議模塊里:
統跳協議如何處理界面跳轉
界面跳轉是統跳協議的初衷,也是統跳協議最重要的任務。因此在統跳協議服務注冊機制中,為界面服務注冊做了更精細的定制開發。
上文提到跳轉服務是一個單一服務,而界面則成百上千,所以在界面注冊和服務注冊中出現了沖突。本著降低開發成本的原則,我們又希望把同一個模塊中界面注冊和服務注冊放在一起。所以在統跳協議中做了如下訂制:
- 默認注冊跳轉服務
跳轉服務是默認存在的,在統跳協議初始化過程中這個服務就已經初始化了。
- 給界面注冊提供特殊的標記
上文中可以看到在注冊分享服務的配置中object
字段是服務的類名,若界面注冊也按照這個規則,那么界面的類就會被認為成一個服務,在調用過程中必然會出現錯誤。因此我們約定,界面注冊需要在類名前加#
標示。
如此一來,在統跳協議初始化過程中,默認加載跳轉服務。當調用發生,解析URL查找到的對應對象帶有#
,則認為這是一個界面,則初始化這個對象,但不對其調用處理URL的方法,而是托管給已注冊的跳轉服務。跳轉服務則根據URL和初始化的界面對象執行跳轉服務。
Rewrite
Rewrite引擎的思路來源于Web容器中的Rewrite機制,主要解決天貓App中URL平臺展現一致性的問題。
天貓App中所有界面都是通過URL來標示的,然而標示Native界面的URL全部都建立在Native規范下,無法和其他平臺對應起來,而Rewrite引擎通過重寫URL來實現平臺一致性。
例如:商品詳情頁面在PC Web的URL是https://detail.tmall.com/item.htm
,在Mobile Web則是https://detail.m.tmall.com/item.htm
,在Native聲明的是tmall://page.tm/itemDetail
。三者各不相同。PC Web和Mobile Web可以通過判斷瀏覽器的UA識別環境,從而通過跳轉實現一致性,也就是說在手機瀏覽器訪問PC Web的URL,會通過一次302轉到Mobile Web的URL。而Native App的環境具有一定的特殊性,Native界面則無法通過類似302這樣的跳轉來實現無感知切換,而Rewrite引擎就是來解決這個問題的。首先,無論是Native還是Web,在天貓App中他們兩兩之間的跳轉都被統跳協議托管,而統跳協議在執行跳轉操作之前會把原始URL放入Rewrite引擎中做一次Rewrite操作。這樣一來,Rewrite引擎就根據配置規則,把原始URL轉換成適用于天貓App的目標URL,實現了URL表現平臺一致性。
原理
Rewrite引擎的原理非常簡單,模擬Web容器(Apache/Nginx等)的Rewrite配置,根據配置把傳入的原始URL進行重寫,返回重寫后的目標URL,交給統跳協議處理。
配置是通過正則表達式描述的Rewrite規則列表,這份列表通過貓客的配置中心實現動態更新。
Rewrite規則
- 每條Rewrite規則中有三個字段:模式串,轉換串和標記位
- 模式串:即正則表達式,用于匹配原始URL
- 轉換串:即需要被轉換成目標URL的描述
- 標記位:以西文逗號分隔的
標記位
,包括表示匹配則終止的l
,需要進行店鋪域名查詢的s
等
- Rewrite規則按行整理,并自上而下按順序逐行匹配
轉換模板中的保留字
-
變量
變量由變量標示符和變量名組合而成,如:
$0
,$#1
,query
,$#fragment
等。變量被用在轉換串中描述轉換后的目標URL中的值。- 變量標示符:
$
,$$
和$#
-
$
原變量的值 -
$$
對原變量做URL Encode
-
$#
自動識別編碼,對原變量做URL Decode
-
$$$
自動識別編碼,對原變量做URL Decode
,再以UTF8
做URL Encode
-
- 變量名:數字(從
0
開始),枚舉(scheme
,host
,port
,path
,query
,fragment
,shopid
)- 數字:
0 -
表示整個URL
,1~n -
表示正則中使用圓括號取出的參數 - 枚舉:
scheme
,host
,port
,path
,query
,fragment
表示標準URL中的相應部分;shopid
表示對個性店鋪域名查詢后得到的shopid
- 數字:
- 變量標示符:
-
標記位
即上述規則中的標記位
Rewrite引擎查詢流程
- 取出規則列表中的首條規則
- 以模式串為模板對原始URL做匹配,并得到模式串定義的參數表
- 若匹配成功則繼續進行,否則進入下一條規則,從2開始進行下一輪匹配
- 查看該條規則是否包含
s
標記位,若包含,則使用原始串做一次個性域名的查詢 - 使用1的結果和重寫串對原始URL進行重寫操作,得到目標URL
- 查看該條規則是否包含
l
標記位:- 若包含,則結束匹配,返回目標URL
- 若不包含,則把目標URL賦值給原始URL,并進入下一條規則,從2開始下一輪匹配
- 直到最后一條規則結束,返回目標URL
舉例
上述提到過商品詳情頁的例子,在Rewrite配置中就體現為:
模式串 | 轉換串 | 標記位 |
---|---|---|
^(?:https?:)?\/\/detail(?:.m)?.tmall.com\/?item.htm\?(.*) |
tmall://page.tm/itemDetail?$1 |
l |
在這條規則的保護下,PC Web和Mobile Web下的商品詳情URL在天貓App中都會被攔截到Native商品詳情頁面,可以帶來最好的用戶體驗。也就是說,在日常的運營工作中,不需要關注一個商品在某個平臺內部需要以什么樣的URL來投放,只需要投放一個主要的URL格式。這個URL在天貓App內部會被Rewrite引擎重寫為Native界面聲明的URL,進行展示。