Flutter路由管理程式碼這麼長長長長長,阿里工程師怎麼高效解決?(實用)
作者:閒魚技術-興往
背景:
在flutter的業務開發過程中,flutter側會逐漸豐富自己的路由管理。一個輕量的路由管理本質上是頁面標識(或頁面路徑)與頁面例項的對映。本文基於dart註解提供了一個輕量路由管理方案。
不論是在native與flutter的混合工程,還是純flutter開發的工程,當我們實現一個輕量路由的時候一般會有以下幾種方法:
- 較差的實現,if-else的邏輯堆疊:
做對映時較差的實現是通過if-else的邏輯判斷把url對映到對應的widget例項上,
class Router { Widget route(String url, Map params) { if(url == 'myapp://apage') { return PageA(url); } else if(url == 'myapp://bpage') { return PageB(url, params); } } }
這樣做的弊端比較明顯:
1)每個對映的維護影響全域性對映配置的穩定性,每次維護對映管理時需要腦補所有的邏輯分支.
2)無法做到頁面的統一抽象,頁面的構造器和構造邏輯被開發者自定義.
3)對映配置無法與頁面聯動,把頁面級的配置進行中心化的維護,導致維護責任人缺失.
- 一般的實現,手動維護的對映表:
稍微好一點的是將對映關係通過一個配置資訊和一個工廠方法來表現
class Router { Map<String, dynamic> mypages = <String, dynamic> { 'myapp://apage': 'pagea', 'myapp://bpage': 'pageb' } Widget route(String url, Map params) { String pageId = mypages[url]; return getPageFromPageId(pageId); } Widget getPageFromPageId(String pageId) { switch(pageId) { case 'pagea': return PageA(); case 'pageb': return PageB(); } return null; }
在flutter側這種做法仍然比較麻煩,首先是問題3仍然存在,其次是由於flutter目前不支援反射,必須有一個類似工廠方法的方式來建立頁面例項。
為了解決以上的問題,我們需要一套能在頁面級使用、自動維護對映的方案,註解就是一個值得嘗試的方向。我們的路由註解方案annotation_route(github地址:https://github.com/alibaba-flutter/annotation_route) 應運而生,整個註解方案的執行系統如圖所示:
讓我們從dart註解開始,瞭解這套系統的運作。
dart註解
註解,實際上是程式碼級的一段配置,它可以作用於編譯時或是執行時,由於目前flutter不支援執行時的反射功能,我們需要在編譯期就能獲取到註解的相關資訊,通過這些資訊來生成一個自動維護的對映表。那我們要做的,就是在編譯時通過分析dart檔案的語法結構,找到檔案內的註解塊和註解的相關內容,對註解內容進行收集,最後生成我們想要的對映表,這套方案的構想如圖示:
在調研中發現,dart的部分內建庫加速了這套方案的落地。
source_gen
dart提供了build、analyser、source_gen這三個庫,其中source_gen利用build庫和analyser庫,給到了一層比較好的註解攔截的封裝。從註解功能的角度來看,這三個庫分別給到了如下的功能:
- build庫:整套資原始檔的處理
- analyser庫:對dart檔案生成完備的語法結構
- source_gen庫:提供註解元素的攔截
這裡簡要介紹下source_gen和它的上下游,先看看我們捋出來的它註解相關的類圖:
source_gen的源頭是build庫提供的Builder基類,該類的作用是讓使用者自定義正在處理的資原始檔,它負責提供資原始檔資訊,同時提供生成新資原始檔的方法。source_gen從build庫提供的Builder類中派生出了一個自己的builder,同時自定義了一套生成器Generator的抽象,派生出來的builder接受Generator類的集合,然後收集Generator的產出,最後生成一份檔案,不同的派生builder對generator的處理各異。這樣source_gen就把一個檔案的構造過程交給了自己定義的多個Generator,同時提供了相對build庫而言比較友好的封裝。
在抽象的生成器Generator基礎上,source_gen提供了註解相關的生成器GeneratorForAnnotation,一個註解生成器例項會接受一個指定的註解型別,由於analyser提供了語法節點的抽象元素Element和其metadata欄位,即註解的語法抽象元素ElementAnnotation,註解生成器即可通過檢查每個元素的metadata型別是否匹配宣告的註解型別,從而篩選出被註解的元素及元素所在上下文的資訊,然後將這些資訊包裝給使用者,我們就可以利用這些資訊來完成路由註解。
annotation_route
在瞭解了source_gen之後,我們開始著手自己的註解解析方案annotation_route
剛開始介入時,我們遇到了幾個問題:
- 只需要生成一個檔案:由於一個輸入檔案對應了一個生成檔案字尾,我們需要避免多餘的檔案生成
- 需要知道在什麼時候生成檔案:我們需要在所有的備選檔案掃描收集完成後再能進行對映表的生成
- source_gen對一個類只支援了一個註解,但存在多個url對映到一個頁面
在一番思索後我們有了如下產出
首先將註解分成兩類,一類用於註解頁面@ARoute,另一類用於註解使用者自己的[email protected]。routeBuilder擁有RouteGenerator例項,RouteGenerator例項,負責@ARoute註解;routeWriteBuilder擁有RouteWriterGenerator例項,負責@ARouteRoot註解。通過build庫支援的配置檔案build.yaml,控制兩類builder的構造順序,在routeBuilder執行完成後去執行routeWriteBuilder,這樣我們就能準確的在所有頁面註解掃描完成後開始生成自己的配置檔案。
在註解解析工程中,對於@ARoute註解的頁面,通過RouteGenerator將其配置資訊交給擁有靜態儲存空間的Collector處理,同時將其輸出內容設為null,即不會生成對應的檔案。在@ARoute註解的所有頁面掃描完成後,RouteWriteGenerator則會呼叫Writer,它從Collector中提取資訊,並生成最後的配置檔案。對於使用者,我們提供了一層友好的封裝,在使用annotation_route配置到工程後,我們的路由程式碼發生了這樣的變化:
使用前:
class Router {
Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) {
if(urlString == 'myapp://testa') {
return TestA(urlString, query);
} else if(urlString == 'myapp://testb') {
String absoluteUrl = Util.join(urlString, query);
return TestB(url: absoluteUrl);
} else if(urlString == 'myapp://testc') {
String absoluteUrl = Util.join(urlString, query);
return TestC(config: absoluteUrl);
} else if(urlString == 'myapp://testd') {
return TestD(PageDOption(urlString, query));
} else if(urlString == 'myapp://teste') {
return TestE(PageDOption(urlString, query));
} else if(urlString == 'myapp://testf') {
return TestF(PageDOption(urlString, query));
} else if(urlString == 'myapp://testg') {
return TestG(PageDOption(urlString, query));
} else if(urlString == 'myapp://testh') {
return TestH(PageDOption(urlString, query));
} else if(urlString == 'myapp://testi') {
return TestI(PageDOption(urlString, query));
}
return DefaultWidget;
}
}
使用後:
import 'package:annotation_route/route.dart';
class MyPageOption {
String url;
Map<String, dynamic> query;
MyPageOption(this.url, this.query);
}
class Router {
ARouteInternal internal = ARouteInternalImpl();
Widget pageFromUrlAndQuery(String urlString, Map<String, dynamic> query) {
ARouteResult routeResult = internal.findPage(ARouteOption(url: urlString, params: query), MyPageOption(urlString, query));
if(routeResult.state == ARouteResultState.FOUND) {
return routeResult.widget;
}
return DefaultWidget;
}
}
目前該方案已在閒魚app內穩定執行,我們提供了基礎的路由引數,隨著flutter業務場景越來越複雜,我們也會在註解的自由度上進行更深的探索。關於annotation_route更加詳細的安裝和使用說明參見github地址:https://github.com/alibaba-flutter/annotation_route ,在使用中遇到任何問題,歡迎向我們反饋。