Flutter路由管理代碼這麼長長長長長,阿里工程師怎麼高效解決?(實用)

背景:

在flutter的業務開發過程當中,flutter側會逐漸豐富本身的路由管理。一個輕量的路由管理本質上是頁面標識(或頁面路徑)與頁面實例的映射。本文基於dart註解提供了一個輕量路由管理方案。 
不管是在native與flutter的混合工程,仍是純flutter開發的工程,當咱們實現一個輕量路由的時候通常會有如下幾種方法:git

  1. 較差的實現,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)映射配置沒法與頁面聯動,把頁面級的配置進行中心化的維護,致使維護責任人缺失.github

  1. 通常的實現,手動維護的映射表: 
    稍微好一點的是將映射關係經過一個配置信息和一個工廠方法來表現
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) 應運而生,整個註解方案的運行系統如圖所示: app

讓咱們從dart註解開始,瞭解這套系統的運做。ui

dart註解

註解,其實是代碼級的一段配置,它能夠做用於編譯時或是運行時,因爲目前flutter不支持運行時的反射功能,咱們須要在編譯期就能獲取到註解的相關信息,經過這些信息來生成一個自動維護的映射表。那咱們要作的,就是在編譯時經過分析dart文件的語法結構,找到文件內的註解塊和註解的相關內容,對註解內容進行收集,最後生成咱們想要的映射表,這套方案的構想如圖示: this

在調研中發現,dart的部份內置庫加速了這套方案的落地。url

source_gen

dart提供了build、analyser、source_gen這三個庫,其中source_gen利用build庫和analyser庫,給到了一層比較好的註解攔截的封裝。從註解功能的角度來看,這三個庫分別給到了以下的功能:spa

  • 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類型是否匹配聲明的註解類型,從而篩選出被註解的元素及元素所在上下文的信息,而後將這些信息包裝給使用者,咱們就能夠利用這些信息來完成路由註解。3d

annotation_route

在瞭解了source_gen以後,咱們開始着手本身的註解解析方案annotation_route 
剛開始介入時,咱們遇到了幾個問題:code

  1. 只須要生成一個文件:因爲一個輸入文件對應了一個生成文件後綴,咱們須要避免多餘的文件生成
  2. 須要知道在何時生成文件:咱們須要在全部的備選文件掃描收集完成後再能進行映射表的生成
  3. source_gen對一個類只支持了一個註解,但存在多個url映射到一個頁面 
    在一番思索後咱們有了以下產出

首先將註解分紅兩類,一類用於註解頁面@ARoute,另外一類用於註解使用者本身的router@ARouteRoot。routeBuilder擁有RouteGenerator實例,RouteGenerator實例,負責@ARoute註解;routeWriteBuilder擁有RouteWriterGenerator實例,負責@ARouteRoot註解。經過build庫支持的配置文件build.yaml,控制兩類builder的構造順序,在routeBuilder執行完成後去執行routeWriteBuilder,這樣咱們就能準確的在全部頁面註解掃描完成後開始生成本身的配置文件。 
在註解解析工程中,對於@ARoute註解的頁面,經過RouteGenerator將其配置信息交給擁有靜態存儲空間的Collector處理,同時將其輸出內容設爲null,即不會生成對應的文件。在@ARoute註解的全部頁面掃描完成後,RouteWriteGenerator則會調用Writer,它從Collector中提取信息,並生成最後的配置文件。對於使用者,咱們提供了一層友好的封裝,在使用annotation_route配置到工程後,咱們的路由代碼發生了這樣的變化: 
使用前:router

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 ,在使用中遇到任何問題,歡迎向咱們反饋。

原文連接

相關文章
相關標籤/搜索