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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

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

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

一、前沿

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

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

? 但Native和JavaScript Context之間的通訊,頻繁的交互就成了程序的性能瓶頸。于此同時(shí)隨著閑魚Flutter技術(shù)的推廣,已經(jīng)有10多個(gè)頁(yè)面用Flutter實(shí)現(xiàn),上面提到的幾種方式都不適合Flutter場(chǎng)景,如何解決這個(gè)問(wèn)題Flutter的動(dòng)態(tài)化的問(wèn)題?

二、動(dòng)態(tài)方案

我們最初調(diào)研了Google的動(dòng)態(tài)化方案CodePush。

2.1 CodePush

? CodePush是谷歌官方推出的動(dòng)態(tài)化方案,目前只有在Android上面實(shí)現(xiàn)了。Dart VM在執(zhí)行的時(shí)候,加載isolate_snapshot_data?和isolate_snapshot_instr?2個(gè)文件,通過(guò)動(dòng)態(tài)更改這些文件,就達(dá)到動(dòng)態(tài)更新的目的。官方的Flutter源碼當(dāng)中,已經(jīng)有相關(guān)的提交來(lái)做動(dòng)態(tài)更新的內(nèi)容,具體內(nèi)容可以參考?ResourceExtractor.java。

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

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

2.2 動(dòng)態(tài)模板

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

? 所以我們提出了閑魚自己的Flutter動(dòng)態(tài)化方案,前面已經(jīng)有同事介紹過(guò)方案的原理:《做了2個(gè)多月的設(shè)計(jì)和編碼,我梳理了Flutter動(dòng)態(tài)化的方案對(duì)比及最佳實(shí)現(xiàn)》,下面看下具體的實(shí)現(xiàn)細(xì)節(jié)。

三、模板編譯

自定義一套DSL,維護(hù)成本較高,怎么能不自定義DSL來(lái)實(shí)現(xiàn)模板下發(fā)?閑魚的方案就是直接將Dart文件轉(zhuǎn)化成模板,這樣模板文件也可以快速沉淀到端側(cè)。

3.1 模板規(guī)范

? 先來(lái)看下一個(gè)完整的模板文件,以新版我的頁(yè)面為例,這個(gè)是一個(gè)列表結(jié)構(gòu),每個(gè)區(qū)塊都是一個(gè)獨(dú)立的Widget,現(xiàn)在我們期望將“賣在閑魚”這個(gè)區(qū)塊動(dòng)態(tài)渲染,對(duì)這個(gè)區(qū)塊拆分之后,需要3個(gè)子控件:頭部、菜單欄、提示欄;因?yàn)檫@3部分界面有些邏輯處理,所以先把他們的邏輯內(nèi)置。

內(nèi)置的子控件分別是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寫法一樣,但是有幾點(diǎn)要注意:

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

    3.2 編譯流程

    ? 編譯模板就要用到Dart的Analyzer庫(kù),通過(guò)parseCompilationUnit函數(shù)直接將Dart源碼解析成為以CompilationUnit為Root節(jié)點(diǎn)的AST樹中,它包含了Dart源文件的語(yǔ)法和語(yǔ)義信息。接下來(lái)的目標(biāo)就是將CompilationUnit轉(zhuǎn)換成為一個(gè)JSON格式。

    ? 上面的模板解析出來(lái)build函數(shù)孩子節(jié)點(diǎn)是ReturnStatementImpl,它又包含了一個(gè)子節(jié)點(diǎn)InstanceCreationExpressionImpl,對(duì)應(yīng)模板里面的new Container(…),它的孩子節(jié)點(diǎn)中,我們最關(guān)心的就是ConstructorNameImpl和ArgumentListImpl節(jié)點(diǎn)。ConstructorNameImpl標(biāo)識(shí)創(chuàng)建節(jié)點(diǎn)的名稱,ArgumentListImpl標(biāo)識(shí)創(chuàng)建參數(shù),參數(shù)包含了參數(shù)列表和變量參數(shù)。

    定義如下結(jié)構(gòu)體,來(lái)存儲(chǔ)這些信息:

    class ConstructorNode {// 創(chuàng)建節(jié)點(diǎn)的名稱String constructorName;// 參數(shù)列表List<dynamic> argumentsList = <dynamic>[];// 變量參數(shù)Map<String, dynamic> arguments = <String, dynamic>{}; }

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

    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;}/// 是函數(shù)if (namedExp.expression is FunctionExpression) {currentNode.arguments[name] =FunctionExpressionParser.parse(namedExp.expression);} else {/// 不是函數(shù)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);} }

    端側(cè)拿到這個(gè)ConstructorNode節(jié)點(diǎn)樹之后,就可以根據(jù)Widget的名稱和參數(shù),來(lái)生成一棵Widget樹。

    四、渲染引擎

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

    工作流程:

  • 開發(fā)人員編寫dart文件,編譯上傳到CDN
  • 端側(cè)拿到模板列表,并在端側(cè)存庫(kù)
  • 業(yè)務(wù)方直接下發(fā)對(duì)應(yīng)的模板id和模板數(shù)據(jù)
  • Flutter側(cè)再通過(guò)橋接獲取到模板,并創(chuàng)建Widget樹
  • 對(duì)于Native測(cè),主要負(fù)責(zé)模板的管理,通過(guò)橋接輸出到Flutter側(cè)。

    4.1 模板獲取

    模板獲取分為2部分,Native部分和Flutter部分;Native主要負(fù)責(zé)模板的管理,包括下載、降級(jí)、緩存等。

    程序啟動(dòng)的時(shí)候,會(huì)先獲取模板列表,業(yè)務(wù)方需要自己實(shí)現(xiàn),Native層獲取到模板列表會(huì)先存儲(chǔ)在本地?cái)?shù)據(jù)庫(kù)中。Flutter側(cè)業(yè)務(wù)代碼用到模板的時(shí)候,再通過(guò)橋接獲取模板信息,就是我們前面提到的JSON格式的信息,Flutter也會(huì)有緩存,已減少Flutter和Native的交互。

    4.2 Widget創(chuàng)建

    Flutter側(cè)當(dāng)拿到JSON格式的,先解析出ConstructorNode樹,然后遞歸創(chuàng)建Widget。

    創(chuàng)建每個(gè)Widget的過(guò)程,就是解析節(jié)點(diǎn)中的argumentsList和arguments?并做數(shù)據(jù)綁定。例如,創(chuàng)建HintItemWidget需要傳入提示的數(shù)據(jù)內(nèi)容,new HintItemWidget(data.hints[0]),在解析argumentsList時(shí),會(huì)通過(guò)key-path的方式從原始數(shù)據(jù)中解析出特定的值。

    解析出來(lái)的值都會(huì)存儲(chǔ)在WidgetCreateParam里面,當(dāng)遞歸遍歷每個(gè)創(chuàng)建節(jié)點(diǎn),每個(gè)widget都可以從WidgetCreateParam里面解析出需要的參數(shù)。

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

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

    至此,我們已經(jīng)能將模板解析出來(lái),并渲染到界面上,交互事件應(yīng)該怎么處理?

    4.3 事件處理

    在寫交互的時(shí)候,一般都會(huì)通過(guò)GestureDector、InkWell等來(lái)處理點(diǎn)擊事件。交互事件怎么做動(dòng)態(tài)化?

    ? 以InkWell組件為例,定義它的onTap函數(shù)為openURL(data.hints[0].href, data.hints[0].params)。在創(chuàng)建InkWell時(shí),會(huì)以O(shè)penURL作為事件ID,查找對(duì)應(yīng)的處理函數(shù),當(dāng)用戶點(diǎn)擊的時(shí)候,會(huì)解析出對(duì)應(yīng)的參數(shù)列表并傳遞過(guò)去,代碼如下:

    ... final List<dynamic> tList = <dynamic>[]; // 解析出參數(shù)列表 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);} });// 找到對(duì)應(yīng)的處理函數(shù) final dynamic handler =TeslaEventManager.sharedInstance().eventHandler(exp.actionName); if (handler != null) {handler(tList); } ...

    五、 效果

    新版我的頁(yè)面添加了動(dòng)態(tài)化渲染能力之后,如果有需求新添加一種組件類型,就可以直接編譯發(fā)布模板,服務(wù)端下發(fā)新的數(shù)據(jù)內(nèi)容,就可以渲染出來(lái)了;動(dòng)態(tài)化能力有了,大家會(huì)關(guān)心渲染性能怎么樣。

    5.1 幀率

    在加了動(dòng)態(tài)加載邏輯之后,已經(jīng)開放了2個(gè)動(dòng)態(tài)卡片,下圖是新版本我的頁(yè)面近半個(gè)月的的幀率數(shù)據(jù):

    從上圖可以看到,幀率并沒(méi)有降低,基本保持在55-60幀左右,后續(xù)可以多添加動(dòng)態(tài)的卡片,觀察下效果。

    注:因?yàn)槲业捻?yè)面會(huì)有本地的一些業(yè)務(wù)判斷,從其他頁(yè)面回到我的tab,都會(huì)刷新界面,所以幀率會(huì)有損耗。

    ? 從實(shí)現(xiàn)上分析,因?yàn)槊總€(gè)卡片,都需要遍歷ConstructorNode樹來(lái)創(chuàng)建,而且每個(gè)構(gòu)建都需要解析出里面的參數(shù),這塊可以做一些優(yōu)化,比如緩存相同的Widget,只需要映射出數(shù)據(jù)內(nèi)容并做數(shù)據(jù)綁定。

    5.2 失敗率

    現(xiàn)在監(jiān)控了渲染的邏輯,如果本地沒(méi)有對(duì)應(yīng)的Widget創(chuàng)建函數(shù),會(huì)主動(dòng)拋Error。監(jiān)控?cái)?shù)據(jù)顯示,渲染的流程中,還沒(méi)有異常的情況,后續(xù)還需要對(duì)橋接層和native層加錯(cuò)誤埋點(diǎn)。

    六、展望

    ? 基于Flutter動(dòng)態(tài)模板,之前需要走發(fā)版的Flutter需求,都可以來(lái)動(dòng)態(tài)化更改。而且以上邏輯都是基于Flutter原生的體系,學(xué)習(xí)和維護(hù)成本都很低,動(dòng)態(tài)的代碼也可以快速的沉淀到端側(cè)。

    ? 另外,閑魚正在研究UI2Code的黑科技,不了解的老鐵,可以參考閑魚大神的這篇文章《重磅系列文章!UI2CODE智能生成Flutter代碼——整體設(shè)計(jì)篇》。可以設(shè)想下,如果有個(gè)需求,需要?jiǎng)討B(tài)的顯示一個(gè)組件,UED出了視覺(jué)稿,通過(guò)UI2Code轉(zhuǎn)換成Dart文件,再通過(guò)這個(gè)系統(tǒng)轉(zhuǎn)換成動(dòng)態(tài)模板,下發(fā)到端側(cè)就可以直接渲染出來(lái),程序員都不需要寫代碼了,做到自動(dòng)化運(yùn)營(yíng),看來(lái)以后程序員失業(yè)也不是沒(méi)有可能了。

    ? 基于Flutter的Widget,還可以拓展更多個(gè)性化的組件,比如內(nèi)置動(dòng)畫組件,就可以動(dòng)態(tài)化下發(fā)動(dòng)畫了,更多好玩的東西等待大家來(lái)一起探索。


    原文鏈接
    本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。

    總結(jié)

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

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。