打通前後端邏輯,客戶端Flutter程式碼一天上線
隨著閒魚的業務快速增長,運營類的需求也越來越多,其中不乏有很多介面修改或運營坑位的需求。閒魚的版本現在是每2週一個版本,如何快速迭代產品,跳過視窗期來滿足這些需求?另外,閒魚客戶端的包體也變的很大,Android的包體大小,相比2016年,已經增長了近1倍,怎麼能將包體大小降下來?首先想到的是動態化的解決此類問題。
對於原生的能力的動態化,Android平臺各公司都有很完善的動態化方案,甚至Google還提供了Android App Bundles讓開發者們更好地支援動態化。由於Apple官方擔憂動態化的風險,因此並不太支援動態化。因此動態化能力就會考慮跟Web結合,從一開始基於 WebView 的 Hybrid 方案,到現在與原生相結合的 React Native 、Weex。
與此同時,隨著閒魚Flutter技術的推廣,已經有10多個頁面用Flutter實現,上面提到的幾種方式都不適合Flutter場景,如何解決這個問題Flutter的動態化的問題?
動態方案
我們最初調研了Google的動態化方案CodePush。
01
CodePush是谷歌官方的動態化方案,Dart VM在執行的時候,載入 isolate_snapshot_data
和 isolate_snapshot_instr
2個檔案,透過動態更改這些檔案,就達到動態更新的目的。官方的Flutter原始碼當中,已經有相關的提交來做動態更新的內容,具體可以參考 ResourceExtractor.java。目前,此功能還在開發中,期待ing。
02
動態模板,就是透過定義一套DSL,在端側解析動態的建立View來實現動態化,比如LuaViewSDK、Tangram-iOS和Tangram-Android。這些方案都是建立的Native的View,如果想在Flutter裡面實現,需要建立Texture來橋接;Native端渲染完成之後,再將紋理貼在Flutter的容器裡面,實現成本很高,效能也有待商榷,不適合閒魚的場景。
所以我們提出了閒魚自己的Flutter動態化方案,前面已經有同事介紹過方案的原理:《做了2個多月的設計和編碼,我梳理了Flutter動態化的方案對比及最佳實現》,下面看下具體的實現細節。
模版編譯
自定義一套DSL,維護成本較高,怎麼能不自定義DSL來實現模板下發?閒魚的方案就是直接將Dart檔案轉化成模板,這樣模板檔案也可以快速沉澱到端側。
01
先來看下一個完整的模板檔案,以新版我的頁面為例,這個是一個列表結構,每個區塊都是一個獨立的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
開頭,陣列形式以[]
訪問,字典形式以.
訪問
模板寫好之後,就要考慮怎麼在端上渲染,早期版本是直接在端側解析檔案,但是考慮到效能和穩定性,還是放在前期先編譯好,然後下發到端側。
02
編譯模板就要用到Dart的 Analyzer
庫,透過 parseCompilationUnit
函式直接將Dart原始碼解析成為以 CompilationUnit
為Root節點的AST樹中,它包含了Dart原始檔的語法和語義資訊。接下來的目標就是將 CompilationUnit
轉換成為一個JSON格式。
上面的模板解析出來build函式孩子節點是 ReturnStatementImpl
,它又包含了一個子節點 InstanceCreationExpressionImpl
,對應模板裡面的 newContainer(…)
,它的孩子節點中,我們最關心的就是 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側。
01
模板獲取分為2部分,Native部分和Flutter部分;Native主要負責模板的管理,包括下載、降級、快取等。
程式啟動的時候,會先獲取模板列表,業務方需要自己實現,Native層獲取到模板列表會先儲存在本地資料庫中。Flutter側業務程式碼用到模板的時候,再透過橋接獲取模板資訊,就是我們前面提到的JSON格式的資訊,Flutter也會有快取,已減少Flutter和Native的互動。
02
Flutter側當拿到JSON格式的,先解析出 ConstructorNode
樹,然後遞迴建立Widget。
建立每個Widget的過程,就是解析節點中的 argumentsList
和 arguments
並做資料繫結。例如,建立 HintItemWidget
需要傳入提示的資料內容, newHintItemWidget(data.hints[0])
,在解析 argumentsList
時,會透過key-path的方式從原始資料中解析出特定的值。
解析出來的值都會儲存在 WidgetCreateParam
裡面,當遞迴遍歷每個建立節點,每個widget都可以從 WidgetCreateParam
裡面解析出需要的引數。
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
樹,再交給Flutter Framework去渲染。
至此,我們已經能將模板解析出來,並渲染到介面上,互動事件應該怎麼處理?
03
在寫互動的時候,一般都會透過 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);
}
...
效果
新版我的頁面新增了動態化渲染能力之後,如果有需求新新增一種元件型別,就可以直接編譯釋出模板,服務端下發新的資料內容,就可以渲染出來了;動態化能力有了,大家會關心渲染效能怎麼樣。
01
在加了動態載入邏輯之後,已經開放了2個動態卡片,下圖是新版本我的頁面近半個月的的幀率資料:
從上圖可以看到,幀率並沒有降低,基本保持在55-60幀左右,後續可以多新增動態的卡片,觀察下效果。
注:因為我的頁面會有本地的一些業務判斷,從其他頁面回到我的tab,都會重新整理介面,所以幀率會有損耗。
從實現上分析,因為每個卡片,都需要遍歷 ConstructorNode
樹來建立,而且每個構建都需要解析出裡面的引數,這塊可以做一些最佳化,比如快取相同的Widget,只需要對映出資料內容並做資料繫結。
02
現在監控了渲染的邏輯,如果本地沒有對應的Widget建立函式,會主動拋Error。監控資料顯示,渲染的流程中,還沒有異常的情況,後續還需要對橋接層和native層加錯誤埋點。
後續計劃
基於Flutter動態模板,之前需要走發版的Flutter需求,都可以來動態化更改。而且以上邏輯都是基於Flutter原生的體系,學習和維護成本都很低,動態的程式碼也可以快速的沉澱到端側。
另外,閒魚正在研究UI2Code的黑科技,不瞭解的老鐵,可以參考閒魚大神的這篇文章《重磅系列文章!UI2CODE智慧生成Flutter程式碼——整體設計篇》。可以設想下,如果有個需求,需要動態的顯示一個元件,UED出了視覺稿,透過UI2Code轉換成Dart檔案,再透過這個系統轉換成動態模板,下發到端側就可以直接渲染出來,程式設計師都不需要寫程式碼了,做到自動化運營,看來以後程式設計師失業也不是沒有可能了。
基於Flutter的Widget,還可以擴充更多個性化的元件,比如內建動畫元件,就可以動態化下發動畫了,更多好玩的東西等待大家來一起探索。
參考文獻
https://mp.weixin.qq.com/s/4s6MaiuW4VoHr7f0SvuQ
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900359/viewspace-2639466/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 服務端渲染vs客戶端渲染到前後端同構服務端客戶端後端
- 生成萬能二維碼 前後端邏輯 完整流程後端
- 「iOS」行車服務app 「客戶端、後端思路+程式碼」iOSAPP客戶端後端
- 傳奇原始碼分析-客戶端(遊戲邏輯處理源分析二)原始碼客戶端遊戲
- impala客戶端連線客戶端
- Redis客戶端連線Redis客戶端
- .net客戶端呼叫activeMQ程式碼客戶端MQ
- 仿WanAndroid客戶端Flutter版NaNAndroid客戶端Flutter
- 機器學習模型部署--打通前後端任督二脈機器學習模型後端
- 一個高顏值Flutter版WanAndroid客戶端FlutterNaNAndroid客戶端
- 寫一個Flutter彩票客戶端--開獎列表Flutter客戶端
- Winform客戶端引用WCF客戶端後,部分類無法正常使用ORM客戶端
- plsql 客戶端亂碼SQL客戶端
- flutter版本的玩Android客戶端FlutterAndroid客戶端
- Flutter寫的部落格園客戶端Flutter客戶端
- 從邏輯解偶到物理解耦再到前後端分離解耦後端
- 打通前後端流程,案例解讀華為雲開源低程式碼引擎解決方案後端
- FTP客戶端c程式碼功能實現FTP客戶端C程式
- Neditor 2.0.0 釋出,移除後端程式碼,重寫上傳邏輯(Ajax)後端
- mysql、redis 客戶端連線池MySqlRedis客戶端
- vncserver建立與客戶端連線VNCServer客戶端
- 客戶端,服務端客戶端服務端
- 服務端,客戶端服務端客戶端
- Flutter 開發一個 GitHub 客戶端 | 掘金技術徵文FlutterGithub客戶端
- Flutter混合開發玩Android客戶端FlutterAndroid客戶端
- 胖客戶端程式總結客戶端
- C++客戶端程式(socket)C++客戶端
- 小程式iOS客戶端框架——控制元件事件邏輯框架與控制元件原生化iOS客戶端框架控制元件事件
- 小程式iOS客戶端框架—控制元件事件邏輯框架與控制元件原生化iOS客戶端框架控制元件事件
- 面試官:說一說前端路由,後端路由客戶端渲染與服務端渲染面試前端路由後端客戶端服務端
- TCP通訊客戶端和服務端簡單程式碼實現TCP客戶端服務端
- Laravel後臺作為客戶端,socket.io作為服務端,App或其他作為另一個客戶端Laravel客戶端服務端APP
- Java OAuth 2.0 客戶端程式設計(二): 客戶端憑據授權JavaOAuth客戶端程式設計
- golang實現tcp客戶端服務端程式GolangTCP客戶端服務端
- MQTT客戶端JAVA程式碼----fusesource mqtt-clientMQQT客戶端Javaclient
- dubbo客戶端客戶端
- Pulsar客戶端客戶端
- mqtt 客戶端MQQT客戶端