日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

打通前后端逻辑,客户端Flutter代码一天上线

發布時間:2024/8/23 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 打通前后端逻辑,客户端Flutter代码一天上线 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、前沿

? 隨著閑魚的業務快速增長,運營類的需求也越來越多,其中不乏有很多界面修改或運營坑位的需求。閑魚的版本現在是每2周一個版本,如何快速迭代產品,跳過窗口期來滿足這些需求?另外,閑魚客戶端的包體也變的很大,企業包的大小,iOS已經到了94.3M,Android也到了53.5M。Android的包體大小,相比2016年,已經增長了近1倍,怎么能將包體大小降下來?首先想到的是如何動態化的解決此類問題。

? 對于原生的能力的動態化,Android平臺各公司都有很完善的動態化方案,甚至Google還提供了Android App Bundles讓開發者們更好地支持動態化。由于Apple官方擔憂動態化的風險,因此并不太支持動態化。因此動態化能力就會考慮跟Web結合,從一開始基于 WebView 的 Hybrid 方案 PhoneGap、Titanium,到現在與原生相結合的 React Native 、Weex。

? 但Native和JavaScript Context之間的通訊,頻繁的交互就成了程序的性能瓶頸。于此同時隨著閑魚Flutter技術的推廣,已經有10多個頁面用Flutter實現,上面提到的幾種方式都不適合Flutter場景,如何解決這個問題Flutter的動態化的問題?

二、動態方案

我們最初調研了Google的動態化方案CodePush。

2.1 CodePush

? CodePush是谷歌官方推出的動態化方案,目前只有在Android上面實現了。Dart VM在執行的時候,加載isolate_snapshot_data?和isolate_snapshot_instr?2個文件,通過動態更改這些文件,就達到動態更新的目的。官方的Flutter源碼當中,已經有相關的提交來做動態更新的內容,具體內容可以參考?ResourceExtractor.java。

? 根據官方給出的Guide,我們這邊也做了相關的測試,patch的包體大小會很大(939kb)。為了降低包體大小,還可以通過增量的修改snapshot文件的方式來更新。通過bsdiff生成的snapshot的差異文件,2個文件分別可以縮小到48kb和870kb。

? 目前看來,CodePush還不能做到很好的工程化。而且如何管理patch文件,需要制定baseline和patch文件的規則。

2.2 動態模板

? 動態模板,就是通過定義一套DSL,在端側解析動態的創建View來實現動態化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。這些方案都是創建的Native的View,如果想在Flutter里面實現,需要創建Texture來橋接;Native端渲染完成之后,再將紋理貼在Flutter的容器里面,實現成本很高,性能也有待商榷,不適合閑魚的場景。

? 所以我們提出了閑魚自己的Flutter動態化方案,前面已經有同事介紹過方案的原理:《做了2個多月的設計和編碼,我梳理了Flutter動態化的方案對比及最佳實現》,下面看下具體的實現細節。

三、模板編譯

自定義一套DSL,維護成本較高,怎么能不自定義DSL來實現模板下發?閑魚的方案就是直接將Dart文件轉化成模板,這樣模板文件也可以快速沉淀到端側。

3.1 模板規范

? 先來看下一個完整的模板文件,以新版我的頁面為例,這個是一個列表結構,每個區塊都是一個獨立的Widget,現在我們期望將“賣在閑魚”這個區塊動態渲染,對這個區塊拆分之后,需要3個子控件:頭部、菜單欄、提示欄;因為這3部分界面有些邏輯處理,所以先把他們的邏輯內置。

內置的子控件分別是MenuTitleWidget、MenuItemWidget和HintItemWidget,編寫的模板如下:

@override Widget build(BuildContext context) {return new Container(child: new Column(children: <Widget>[new MenuTitleWidget(data), // 頭部new Column( // 菜單欄children: <Widget>[new Row(children: <Widget>[new MenuItemWidget(data.menus[0]),new MenuItemWidget(data.menus[1]),new MenuItemWidget(data.menus[2]),],)],),new Container( // 提示欄child: new HintItemWidget(data.hints[0])),],),); }

中間省略了樣式描述,可以看到寫模板文件就跟普通的widget寫法一樣,但是有幾點要注意:

  • 每個Widget都需要用new或const來修飾
  • 數據訪問以data開頭,數組形式以[]訪問,字典形式以.訪問
  • ? 模板寫好之后,就要考慮怎么在端上渲染,早期版本是直接在端側解析文件,但是考慮到性能和穩定性,還是放在前期先編譯好,然后下發到端側。

    3.2 編譯流程

    ? 編譯模板就要用到Dart的Analyzer庫,通過parseCompilationUnit函數直接將Dart源碼解析成為以CompilationUnit為Root節點的AST樹中,它包含了Dart源文件的語法和語義信息。接下來的目標就是將CompilationUnit轉換成為一個JSON格式。

    ? 上面的模板解析出來build函數孩子節點是ReturnStatementImpl,它又包含了一個子節點InstanceCreationExpressionImpl,對應模板里面的new Container(…),它的孩子節點中,我們最關心的就是ConstructorNameImpl和ArgumentListImpl節點。ConstructorNameImpl標識創建節點的名稱,ArgumentListImpl標識創建參數,參數包含了參數列表和變量參數。

    定義如下結構體,來存儲這些信息:

    class ConstructorNode {// 創建節點的名稱String constructorName;// 參數列表List<dynamic> argumentsList = <dynamic>[];// 變量參數Map<String, dynamic> arguments = <String, dynamic>{}; }

    遞歸遍歷整棵樹,就可以得到一個ConstructorNode樹,以下代碼是解析單個Node的參數:

    ArgumentList argumentList = astNode;for (Expression exp in argumentList.arguments) {if (exp is NamedExpression) {NamedExpression namedExp = exp;final String name = ASTUtils.getNodeString(namedExp.name);if (name == 'children') {continue;}/// 是函數if (namedExp.expression is FunctionExpression) {currentNode.arguments[name] =FunctionExpressionParser.parse(namedExp.expression);} else {/// 不是函數currentNode.arguments[name] =ASTUtils.getNodeString(namedExp.expression);}} else if (exp is PropertyAccess) {PropertyAccess propertyAccess = exp;final String name = ASTUtils.getNodeString(propertyAccess);currentNode.argumentsList.add(name);} else if (exp is StringInterpolation) {StringInterpolation stringInterpolation = exp;final String name = ASTUtils.getNodeString(stringInterpolation);currentNode.argumentsList.add(name);} else if (exp is IntegerLiteral) {final IntegerLiteral integerLiteral = exp;currentNode.argumentsList.add(integerLiteral.value);} else {final String name = ASTUtils.getNodeString(exp);currentNode.argumentsList.add(name);} }

    端側拿到這個ConstructorNode節點樹之后,就可以根據Widget的名稱和參數,來生成一棵Widget樹。

    四、渲染引擎

    端側拿到編譯好的模板JSON后,就是解析模板并創建Widget。先看下,整個工程的框架和工作流:

    工作流程:

  • 開發人員編寫dart文件,編譯上傳到CDN
  • 端側拿到模板列表,并在端側存庫
  • 業務方直接下發對應的模板id和模板數據
  • Flutter側再通過橋接獲取到模板,并創建Widget樹
  • 對于Native測,主要負責模板的管理,通過橋接輸出到Flutter側。

    4.1 模板獲取

    模板獲取分為2部分,Native部分和Flutter部分;Native主要負責模板的管理,包括下載、降級、緩存等。

    程序啟動的時候,會先獲取模板列表,業務方需要自己實現,Native層獲取到模板列表會先存儲在本地數據庫中。Flutter側業務代碼用到模板的時候,再通過橋接獲取模板信息,就是我們前面提到的JSON格式的信息,Flutter也會有緩存,已減少Flutter和Native的交互。

    4.2 Widget創建

    Flutter側當拿到JSON格式的,先解析出ConstructorNode樹,然后遞歸創建Widget。

    創建每個Widget的過程,就是解析節點中的argumentsList和arguments?并做數據綁定。例如,創建HintItemWidget需要傳入提示的數據內容,new HintItemWidget(data.hints[0]),在解析argumentsList時,會通過key-path的方式從原始數據中解析出特定的值。

    解析出來的值都會存儲在WidgetCreateParam里面,當遞歸遍歷每個創建節點,每個widget都可以從WidgetCreateParam里面解析出需要的參數。

    /// 構建widget用的參數 class WidgetCreateParam {String constructorName; /// 構建的名稱dynamic context; /// 構建的上下文Map<String, dynamic> arguments = <String, dynamic>{}; /// 字典參數List<dynamic> argumentsList = <dynamic>[]; /// 列表參數dynamic data; /// 原始數據 }

    ? 通過以上的邏輯,就可以將ConstructorNode樹轉換為一棵Widget樹,再交給Flutter Framework去渲染。

    至此,我們已經能將模板解析出來,并渲染到界面上,交互事件應該怎么處理?

    4.3 事件處理

    在寫交互的時候,一般都會通過GestureDector、InkWell等來處理點擊事件。交互事件怎么做動態化?

    ? 以InkWell組件為例,定義它的onTap函數為openURL(data.hints[0].href, data.hints[0].params)。在創建InkWell時,會以OpenURL作為事件ID,查找對應的處理函數,當用戶點擊的時候,會解析出對應的參數列表并傳遞過去,代碼如下:

    ... final List<dynamic> tList = <dynamic>[]; // 解析出參數列表 exp.argumentsList.forEach((dynamic arg) {if (arg is String) {final dynamic value = valueFromPath(arg, param.data);if (value != null) {tList.add(value);} else {tList.add(arg);}} else {tList.add(arg);} });// 找到對應的處理函數 final dynamic handler =TeslaEventManager.sharedInstance().eventHandler(exp.actionName); if (handler != null) {handler(tList); } ...

    五、 效果

    新版我的頁面添加了動態化渲染能力之后,如果有需求新添加一種組件類型,就可以直接編譯發布模板,服務端下發新的數據內容,就可以渲染出來了;動態化能力有了,大家會關心渲染性能怎么樣。

    5.1 幀率

    在加了動態加載邏輯之后,已經開放了2個動態卡片,下圖是新版本我的頁面近半個月的的幀率數據:

    從上圖可以看到,幀率并沒有降低,基本保持在55-60幀左右,后續可以多添加動態的卡片,觀察下效果。

    注:因為我的頁面會有本地的一些業務判斷,從其他頁面回到我的tab,都會刷新界面,所以幀率會有損耗。

    ? 從實現上分析,因為每個卡片,都需要遍歷ConstructorNode樹來創建,而且每個構建都需要解析出里面的參數,這塊可以做一些優化,比如緩存相同的Widget,只需要映射出數據內容并做數據綁定。

    5.2 失敗率

    現在監控了渲染的邏輯,如果本地沒有對應的Widget創建函數,會主動拋Error。監控數據顯示,渲染的流程中,還沒有異常的情況,后續還需要對橋接層和native層加錯誤埋點。

    六、展望

    ? 基于Flutter動態模板,之前需要走發版的Flutter需求,都可以來動態化更改。而且以上邏輯都是基于Flutter原生的體系,學習和維護成本都很低,動態的代碼也可以快速的沉淀到端側。

    ? 另外,閑魚正在研究UI2Code的黑科技,不了解的老鐵,可以參考閑魚大神的這篇文章《重磅系列文章!UI2CODE智能生成Flutter代碼——整體設計篇》。可以設想下,如果有個需求,需要動態的顯示一個組件,UED出了視覺稿,通過UI2Code轉換成Dart文件,再通過這個系統轉換成動態模板,下發到端側就可以直接渲染出來,程序員都不需要寫代碼了,做到自動化運營,看來以后程序員失業也不是沒有可能了。

    ? 基于Flutter的Widget,還可以拓展更多個性化的組件,比如內置動畫組件,就可以動態化下發動畫了,更多好玩的東西等待大家來一起探索。


    原文鏈接
    本文為云棲社區原創內容,未經允許不得轉載。

    總結

    以上是生活随笔為你收集整理的打通前后端逻辑,客户端Flutter代码一天上线的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。