整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          在閑魚,我們如何用Dart做高效后端開發?

          在閑魚,我們如何用Dart做高效后端開發?

          像阿里其他技術團隊以及業界的做法一樣,閑魚的大多數后端應用都是全部使用java來實現的。java易用、豐富的庫、結構容易設計的特性決定了它是進行業務開發的最好語言之一。后端應用中數據的存儲、訪問、轉換、輸出雖然都屬于后端的范疇,但是其中變更的頻率是不同的。通常領域對象確定之后,它的變化是很少的,但是客戶端展示的變化很多,導致接口層(或者叫粘連前臺和后臺的膠水層)的變化非常快。大多數web應用采用統一的技術棧來實現后端,膠水層跟領域層使用統一技術,這樣的做法仍然有可以優化的地方:

          • 在預發環境中驗證調試比較困難:一方面,每次提交代碼、構建、部署、驗證的總時間相對較長;另一方面,多人共用一個部署環境,相互干擾(代碼沖突和部署沖突),增加了成本。后端開發人員都渴望有一個獨立、高效的開發環境,就像開發一個前端頁面那樣
          • 前臺(java、object-c,javascript)和后臺(java)的技術不同,導致前臺同學很難開發后端程序,閑魚技術團隊為了追求更高的開發效率,希望能夠跨越服務端開發與客戶端、前端的界限,讓前臺開發人員也能夠寫后端代碼
          • 膠水層通常依賴很多后端服務,計算比較簡單,是IO密集型的任務。我們理想中的編程框架是能夠像寫同步代碼一樣簡單,但是享受異步的好處。目前的方案還無法完全做到這一點。

          為什么選擇dart

          閑魚技術團隊選擇使用dart作為膠水層的實現語言。

          • dart是一種靜態類型語言,在編譯器就能完全確定變量的類型。它是支持泛型的面向對象語言,任何變量都是對象,不存在java中的原始類型。跟javascript類似,它是一種單線程語言,對異步的支持非常好(async/await)。dart的語法與主流開發語言(java,python,c/c++,javascript)很類似, 在主流的語言語法基礎上,dart增加了很多語法結構,getter/setter、方法級聯、函數式編程、閉包,這些語法讓允許開發人員更加容易地寫出簡潔的代碼;全面易用的類庫也是dart能夠作為flutter開發語言的重要原因。
          • flutter證明了dart在客戶端開發上的成功,閑魚不僅走在flutter開發的前列,也正在嘗試使用dart開發后端應用;語法跟javascript,java相近,有人形容這門語言是傻瓜式的簡單(stupid-simple to learn),無論是java后端開發人員,還是客戶端開發同學,亦或是前端開發同學,都能夠快速上手寫出生產級的代碼。所有技術同學都能夠開發后端接口在閑魚是可以做到的。
          • dart對異步化的良好支持對業務開發是強大助力。后端應用膠水層代碼大多數IO密集型的任務,使用異步化技術可以把多個IO請求的總RT,從所有請求RT之和,降低為所有請求中最高RT。dart對異步有良好的支持,開發同學使用dart可以以近乎同步的代碼風格取得異步的性能。我們以閑魚寶貝詳情頁的代碼舉例,對比不同的編碼方式。
          // java同步代碼
          ItemDetailDO queryItemDetail(Long itemId) {
           ItemDetailDO data=new ItemDetailDO();
           data.setBrowseCount(IdleItemBrowseService.count(itemId));// 多少人看過
           data.setFavorCount(IdleItemFavorService.count(itemId));// 多少人點贊
           return data;
          }
          // dart異步代碼
          ItemDetailDO queryItemDetail(int itemId) async {
           var data=new ItemDetailDO();
           await Future.wait([
           IdleItemBrowseService.count(itemId).then((count)=> data.browseCount=count)
           .catchError((exp, stackTrace)=> logError('$exp $stackTrace')),
           IdleItemFavorService.count(itemId).then((count)=> data.favorCount=count)
           .catchError((exp, stackTrace)=> logError('$exp $stackTrace'))
           ]);
           return data;
          }
          // rxjava異步代碼
          ItemDetailDO queryItemDetail(Long itemId) {
           ItemDetailDO data=new ItemDetailDO();
           Flowable<Long> browseCountFlow=Flowable.fromCallable(
           ()=> IdleItemBrowseService.count(itemId)
           ).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
           Flowable<Long> favorCountFlow=Flowable.fromCallable(
           ()=> IdleItemFavorService.count(itemId)
           ).onErrorReturn(t -> 0).subscribeOn(Schedulers.io());
           Flowable.zip(browseCountFlow, favorCountFlow, (browseCount, favorCount) -> {
           data.setBrowseCount(browseCount);
           data.setFavorCount(favorCount);
           }).blockingFirst();
          }
          

          在java中我們也廣泛使用RxJava這種強大的響應式擴展實施異步操作:RxJava作為java的響應式編程擴展,功能非常強大全面,它使用流的概念封裝所有的異步操作。需要注意的是這里的兩個服務調用都被放到一個IO線程池中運行, 這個線程池是無界的,容易消耗線程這種系統稀缺的資源。這意味著當流量非常大的時候,系統的線程池很容易被打滿,需要設置合理的背壓策略。

          從上面的代碼中可以看到“數據獲取”,“數據組裝”的邏輯非常清晰,不像同步代碼分散在各處;相比于同步操作,dart的異步操作允許我們同時等待多個IO事件,降低總的響應時間。dart的異步代碼擁有同步代碼的簡潔容易理解的優點,又具有異步編程的性能優勢。

          dart異步的原理也是容易理解的。作為單線程語言,dart依靠事件循環運行代碼。dart從main函數開始執行,我們在main函數里面創建Future,相當于在一個dart內部維護的事件隊列(event queue)中添加計劃任務(添加的任務并不會立即執行)。main中的代碼執行完之后,dart事件循環開始從事件隊列中依次獲取任務執行。async/await是dart的語法糖,它允許開發人員能夠以書寫同步代碼的方式來實施異步編程(在C#、javascript中也有類似實現)。被async修飾的方法返回一個Future,調用這樣的方法,相當于創建一個Future。await一個Future,相當于把await之后的代碼打包放在Future.then()的代碼塊里,這樣就保證之后的代碼在Future之后執行。由于任務存儲于事件隊列,dart在流量大的時候,內存消耗較大,也需要我們前期合理評估需求和分配系統資源。

          dart后端開發實戰

          為了提高開發效率,我們利用dart的特性構建了一套高效的隔離開發環境。在業務開發實踐中,我們總結出基本的開發架構和代碼模式。在這些技術基礎上,開發了閑魚寶貝詳情頁的主干業務。下面逐一介紹。

          高效的隔離開發環境

          我們以往的開發場景是:提交代碼 -> 代碼沖突(多人共用一個部署環境) -> 構建/部署 -> 通過接口驗證 -> 提交fix -> 構建/部署 -> 驗證 的迭代。在這個過程中,開發人員有可能需要親自解決代碼沖突,或者依賴別人解決代碼沖突,需要等待構建/部署的時間(少則5分鐘,多則十幾分鐘)。而且這個過程可能需要迭代多次,時間成本很高,如果因為其他開發人員的代碼分支的問題導致部署失敗,那么等待驗證的時間成倍增加。這樣的開發效率顯然不是特別理想。

          在閑魚的dart應用中,這種問題會得到緩解。每個開發人員使用自己獨立的開發環境,開發環境使用每個人的工號唯一識別。在不需要提交代碼的情況下,開發人員把代碼部署到遠程預發環境中,并在本地調用預發服務,查看服務的輸出,做到本地驗證調試的效果,極大地提高了開發效率。因為只會有開發自己單一分支的代碼部署,不會牽扯到代碼沖突。整個過程,部署、服務調用過程十分快速,可以在10秒內完成。驗證和調試的效率非常高。

          每個開發人員的獨立開發環境對應預發機器上的一個isolate。dart的isolate相當于一個線程,但是不會和其他isolate共享內存,isolate之間的通信通過發送、接收消息完成。閑魚技術團隊使用每個開發人員的代碼創建一個isolate,使用工號作為標識,代碼可以全量替換掉運行中的isolate,也可以使用熱部署增量替換掉isolate中更改的功能。整個過程非常快。在早期使用dart原生的編譯器,發現速度較慢(10多秒)后,我們對dart編譯器做了裁剪和優化,把編譯時間從10多秒降低到幾百毫秒(簡單來說就是,把dart原生的編譯器的附加功能,重新封裝,然后通過JIT/AOT生成新的編譯工具)。經過我們對dart開發環境的增強,現在開發dart膠水層接口,只需要點擊開發工具上的一個按鈕,就可以把修改的代碼,在幾秒內部署到遠程的預發環境,并調用當前的開發接口,在本地查看輸出。獲得和在預發環境上驗證一樣的效果,但是體驗就像在開發一個完全不依賴外部的本地應用程序。

          業務開發架構

          業務開發中最重要的部分是分離出變化和不變的部分,變化的部分用最靈活、快捷的方式實現(變的最多的地方當然用最快的方式處理),不變的部分使用穩定、高效的方式實現。我們已經把dart建設成為一種能夠高效開發,并且適合客戶端、前端、后端技術人員共同使用的技術。這種技術最適合應用于發生快速變化的接口層,也就是客戶端和后端交互的地方,業務需求的變化導致這里的數據結構快速變化,也稱之為膠水層。對于相對穩定的數據服務,我們使用java實現為領域服務。

          上圖是服務之間的交互圖,實現方式如下圖所示:

          膠水層dart應用以HTTP協議方式作為MTOP接口提供給客戶端調用,往下使用HSF從Java應用中獲取數據。

          通常先定義并開發好領域服務,然后再與客戶端對接開發出接口,領域服務提供的接口,包含了獲取基礎數據的所有方法,開發好之后,很少發生變化;膠水層獲取領域服務提供的數據,對數據進行加工、裁剪、組裝,輸出為客戶端能夠解析的視圖數據,客戶端解析、渲染、展示為頁面。膠水層的代碼大致可以分為:獲取數據,然后數據處理和組裝。抽象出代碼模式如下所示:

          // 數據處理和組裝
          void putTiger(Zoo zoo, Tiger tiger)=> zoo.tiger=tiger;
          void putDophin(Zoo zoo, Dophin dophin)=> zoo.dophin=dophin;
          void putRatel(Zoo zoo, Ratel ratel)=> zoo.ratel=ratel;
          // 發起多個異步請求,所有請求完成后返回所有數據
          Future<T> catchError<T>(Future<T> future) {
           return future.catchError((e, st)=> LogUtils.error('$e $st'));
          }
          Future<List<T>> waitFutures<T>(List<Future<T>> futures) {
           Future<List<T>> future=Future.wait(futures.map(catchError));
           return catchError(future);
          }
          // 服務接口
          Future<Zoo> process(Parameter param) async {
           var zoo=new Zoo();
           // 數據獲取
           await waitFutures(
           Service1.invoke(param).then((animal) -> putTiger(zoo, animal)),
           Service2.invoke(param).then((animal) -> putLion(zoo, dophin)),
           Service3.invoke(param).then((animal) -> putRatel(zoo, animal))
           );
           return finalData;
          }
          

          為了使用java的領域服務,我們首先解決了dart和java之間數據交互問題,主要是通過序列化對java類文件和dart類文件進行合理的轉換,保證dart能夠透明、簡潔地使用java的數據結構,調用java的遠程服務;在調用鏈路上設置全局唯一的上下文id,跨越dart和java調用棧,支持全鏈路排查;對所有的服務的成功率,rt和額外業務參數有詳細的日志,可以配置以日志為數據源的監控告警等等(后續的文章將詳細介紹我們對這些問題的詳細解決方案,請持續關注哦)。

          服務化詳情頁主干開發

          閑魚寶貝詳情頁是我們使用dart開發的一個重要項目。最早的閑魚寶貝詳情頁把各個業務的代碼邏輯耦合在一起,導致維護和變更困難,穩定性也難以保證。我們設計的swak框架(更多細節請查看文章swak框架),能夠分離垂直業務的共性和差異性,把閑魚寶貝詳情頁的實現分割成主干實現和垂直業務實現兩塊。我們使用自己開發的dart后端開發框架,對swak框架做了最小實現。項目完成了詳情頁主干的完整功能和基礎優化:

          • 垂直業務路由:我們使用dart中的zone存儲每個閑魚商品的業務標識,代碼生成的靜態代理類依據業務標識調用相應的服務,在主干數據里填充各個業務的獨有數據。zone是dart異步代碼的執行環境,能夠緩存一些可重用數據(業務代碼里除非非此不可,盡量不要多用)
          • 作為遠程服務的提供方:在hsfcpp對hessian協議的實現基礎上做開發,dart也能成為遠程服務的提供方
          • 服務調用的優化: 對java遠程服務的代理做了優化,隔離業務層面對框架層的感知,做到透明調用
          • 解決緩存調用的差異性:我們依賴緩存的c++接口訪問緩存,但是仍然需要處理java/c++緩存讀寫不兼容問題完成dart和java對同一緩存的同時讀寫
          • 項目流程圖可見下圖:
           ![dart-detail-flow.png](http://gw.alicdn.com/mt/TB1Pyv6V9zqK1RjSZFjXXblCFXa-558-561.png)
          

          實際效果

          目前該項目已經上線超過6周,qps最高可達400,成功率在99.5%以上。整個調用鏈路的RT與同樣功能的java應用持平。由于前期的精心設計,領域服務很少改動,大部分變更發生在dart膠水層。從上線后經歷的若干次變更來看,dart膠水層從修改代碼結束到提供給客戶端使用總耗時不超過2分鐘,而相同功能的java應用需要10分鐘以上。

          總結

          dart是一門簡潔、容易上手、對異步支持良好的編程語言,在flutter的開發中大放異彩。在我們的努力下,dart用于后端開發的支持逐漸完善,前臺開發同學和后端開發人員快速高效地開發膠水層接口。我們在很多生產項目中使用了dart用于后端開發,性能、穩定性良好,開發效率大大提高。未來我們會著力于進一步改善dart開發體驗、與java項目的兼容性、提升dart遠程服務的性能,挖掘dart在后端開發中更大的潛力。

          作者:閑魚技術-臨耕



          面主要講了Dart的一些基礎用法,今天主要講解Dart的函數部分。

          一、Dart函數概述

          1、Dart是一門面向對象的語言,而且是完全面向對象的。所謂的完全面向對象就是函數也是對象,函數可以被聲明成變量,函數也可以作為另外一個函數的參數使用,同時也可以像調用函數一樣調用類的實例變量,函數這些特性和JavaScript中函數第一等公民很像。

          2、和其他語言不太一樣的是,Dart所有的函數都有返回值,如果沒有指明返回值,函數返回null;會默認的拼接到函數體。

          3、Dart中的函數如果只有一行表達式的,可以使用尖頭語法簡寫

          4、函數有兩種參數類型:規定參數和可選參數。先列出規定參數,可選參數跟隨其后。命名成可選的參數也可以被標記為規定參數。



          二、函數的種類和定義

          1、系統內置函數

          比如print()'

          2、自定義函數

          自定義函數的基本格式:

          返回類型 函數名稱(函數參數1,函數參數2,...){

          函數體

          return 返回值;

          }

          void printData(){
            print('我是一個自定義函數');
          }
          printData();

          3、可選參數的函數

          在函數參數中用[]符號包裹可選的參數

          String printUserInfo(String username,[String sex='男',int age]){  //形式參數
            if(age!=null){
              return "姓名:$username---性別:$sex--年齡:$age";
            }
            return "姓名:$username---性別:$sex--年齡保密";
          }
          
          print(printUserInfo('張三'));
          print(printUserInfo('小李','女'));
          print(printUserInfo('小李','女',30));

          4、帶默認參數的函數

          可以使用=來指明參數的默認值。默認值必須是編譯時常量。如果沒有默認值,默認值就是null。

          String printUserInfo(String username,{int age,String sex='男'}){  //形式參數
              if(age!=null){
                 return "姓名:$username---性別:$sex--年齡:$age";
               }
              return "姓名:$username---性別:$sex--年齡保密";
          }
          
          print(printUserInfo('張三',age:20,sex:'未知'));

          5、命名參數的函數

          定義函數的時候使用{param1, param2, …}來明確參數

          //函數1
          func1(){
            print('func1');
           }
          
          //函數2
          func2(func){
            func();
          }
          
          //調用func2這個函數 把func1這個函數當做參數傳入
          func2(func1);
          
          

          6、函數作為參數的函數

          var fn=(){
            print('我是一個匿名函數');
          };      
          fn();
          



          7、匿名函數

          Dart也可以創建匿名函數,可以給一個變量賦值一個匿名函數。匿名函數看起來像一個有名稱的函數零個或多個參數在圓括號中用逗號或中括號分隔,代碼塊在函數體后邊。

          list.forEach((value){
              print(value);
          });
          
          list.forEach((value)=>print(value));
          
          list.forEach((value)=>{
            print(value)
          });

          8、箭頭函數

          ((int n){
             print(n);
             print('我是自執行方法');
          })(12);

          9、自執行函數

          var sum=1;			
          func(n){
            sum*=n;
            if(n==1){
              return ;
            }         
            func(n-1);
          }
          
          func(5);      
          print(sum);

          10、遞歸函數

          var sum=1;			
          func(n){
            sum*=n;
            if(n==1){
              return ;
            }         
            func(n-1);
          }
          
          func(5);      
          print(sum);

          11、main函數

          每個app必須有一個頂級的main()函數,提供程序的入口。main()函數返回void類型并且有一個list<String>類型的可選參數。

          void main() {
          
          }



          三、全局變量、局部變量和閉包

          Dart是一個靜態作用域的語言,意味著變量的作用域是在寫代碼的時候就提前定義好的。可以看一個函數是否在花括號里邊來看它的作用域,比如全局變量或者局部變量。

          1、全局變量

          特點: 全局變量常駐內存、全局變量污染全局

          var data=123;
          
          void main(){
            print(a);
          
            func(){
              data++;
              print(data);
            }
            func();
          }

          2、局部變量

          特點:不常駐內存會被垃圾機制回收、不會污染全局

          var data=123;
          
          void main(){
            print(a);
          
            func(){
              data++;
              print(data);
            }
            func();
            func();
            
             printData(){
                var myData=123;
                myData++;
                print(myData);
             }
             printData();
             printData();
          }

          3、閉包

          背景:

          • 常駐內存
          • 不污染全局

          為了實現這個需求,產生了閉包。

          閉包: 函數嵌套函數, 內部函數會調用外部函數的變量或參數, 變量或參數不會被系統回收(不會釋放內存)

          閉包的寫法: 函數嵌套函數,并return 里面的函數,這樣就形成了閉包。

          Google(谷歌)公司開發的Dart語言迎來了2.5版本的更新。本次更新提供了ML Complete(由機器學習驅動的代碼補全功能)和dart:ffi 外部函數接口(用來直接從 Dart 調用 C 語言代碼)。

          Dart 2.5

          類型化編程語言的核心優勢之一,就是在類型中附帶的信息使得 IDE / 編輯器能夠在鍵入代碼時提供強大的代碼補全功能,從而幫助開發者提高效率。通過代碼補全,開發者只需要輸入代碼的開頭部分即可從提供的選項中進行選擇,從而避免拼寫錯誤,也便于探索各種 API。

          但隨著 API 數量的增長,探索 API 也變得愈發困難,因為補全功能提供的列表太長,開發者無法按照字母順序去逐一瀏覽。在過去的一年里,我們一直在努力讓機器學習來解決這個問題。簡單地講,我們通過分析 GitHub 上大量開源的 Dart 代碼來訓練一個模型,用以分析特定上下文時不同代碼成員的出現模式。這個基于 TensorFlow Lite 打造的模型在被訓練成型后,可以在開發者編寫代碼時預測接下來需要用到的代碼內容。這個新功能我們稱之為 ML Complete。以下是使用 Flutter 框架開發新的 MyHome widget 的示例:

          使用 ML Complete 開發 Flutter widget 時的示例

          • 用于分析的大量 GitHub 開源 Dart 代碼
          • https://console.cloud.google.com/marketplace/details/github/github-repos
          • TensorFlow Lite
          • https://www.tensorflow.org/lite

          讓我們來深入了解一下它的運行機制。假設您正在編寫一個小程序來計算從當前時間開始一天后的時間。使用 ML Complete,您將獲得下圖這樣迅捷的開發體驗。

          使用 ML Complete 編寫代碼的體驗

          不使用 ML Complete 編寫同樣代碼的體驗

          首先,請注意 ML Complete 會根據開發者輸入的變量名稱 now 自動給出 DateTime.now() 的建議。當第一行輸入完成后,請注意我們在開發者輸入第二個變量名時,也給出了 tomorrow 這個變量名建議。最后,基于 now 這個變量給出了第二個補全建議 add(…)。而在上圖的非 ML Complete 體驗中,我們必須手動鍵入 DateTime,而且在鍵入 tomorrow 變量名時沒有補全提示,另外 now 的 add(…) 方法在推薦列表更下面的位置才出現。

          許多開發者要求我們為從 Dart 調用 C 代碼提供更好的支持。一個非常明確的信號,是在 Flutter 問題反饋專區里 C 語言互操作是呼聲最高的功能請求,得票數超過 600。這些功能請求背后有許多有趣的用例,包括調用低級平臺 API (如 stdlib.h 或 Win32),調用現有的跨平臺庫以及用 C 語言編寫的實用程序 (如 TensorFlow、Realm 和 SQLite) 等。

          • Flutter 功能請求列表
          • https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc
          • stdlib.h
          • https://pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html
          • Win32
          • https://en.wikipedia.org/wiki/Windows_API

          目前,直接從 Dart 調用 C 的支持僅限于使用原生擴展與 Dart VM 進行深度集成。或者,Flutter 應用可以通過平臺通道調用 host,并從那里來間接調用 C。這種兩層的間接調用表現并不理想,我們希望提供一種新的機制,能夠提供出色的性能,易于使用,并可以在許多支持 Dart 的平臺和編譯器上運行。

          • 原生擴展
          • https://dart.dev/server/c-interop-native-extensions
          • 平臺通道
          • https://flutter.dev/docs/development/platform-integration/platform-channels
          • 支持 Dart 的平臺和編譯器
          • https://dart.dev/platforms

          Dart-C 互操作支持兩種主要場景:

          • 在操作系統 (OS) 上調用基于 C 的系統 API
          • 調用基于 C 的代碼庫,該代碼庫可以基于單個操作系統,也可以是跨平臺的

          我們來看看第一個互操作場景。我們將調用 Linux 命令 system,它可以執行任何系統命令; 傳遞給它的參數實際上是傳遞給了 shell/terminal,并在那里運行。這個指令的 C 語言頭部如下所示:

          // C header: int system(const char *command) in stdlib.h
          

          任何互操作機制的核心挑戰都是處理兩種語言的語義差異。在 dart:ffi 這里,Dart 代碼需要處理好兩件事:

          1. C 語言函數及其參數的類型,以及返回類型
          2. 與之對應的 Dart 函數及其類型

          我們通過定義兩個 typedef 來做到這一點:

          // C header typedef:
          typedef SystemC=ffi.Int32 Function(ffi.Pointer<Utf8> command);
          // Dart header typedef:
          typedef SystemDart=int Function(ffi.Pointer<Utf8> command);
          

          下面我們需要加載代碼庫,并查找我們要調用的函數。具體做法取決于操作系統,在下面這個例子中,我們使用的是 macOS。

          // Load `stdlib`. On MacOS this is in libSystem.dylib.
          final dylib=ffi.DynamicLibrary.open('/usr/lib/libSystem.dylib');
          // Look up the system function.
          final systemP=dylib.lookupFunction<SystemC, SystemDart>('system');
          

          您可以在 GitHub 上找到可供所有三種操作系統 (macOS、Windows、Linux) 執行的完整示例。

          • macOS 示例
          • https://github.com/dart-lang/samples/blob/master/ffi/system-command/macos.dart
          • Windows 示例
          • https://github.com/dart-lang/samples/blob/master/ffi/system-command/windows.dart
          • Linux 示例
          • https://github.com/dart-lang/samples/blob/master/ffi/system-command/linux.dart

          接下來,我們使用與特定操作系統相關的編碼對字符串參數進行編碼,調用該函數,并再次釋放參數內存:

          // Allocate a pointer to a Utf8 array containing our command.
          final cmdP=Utf8.toUtf8('open http://dart.dev');
          // Invoke the command.
          systemP(cmdP);
          // Free the pointer.
          cmdP.free();
          

          這段代碼會執行系統命令,使用系統默認瀏覽器打開 dart.dev 網頁:

          通過 dart:ffi 使用系統 API 打開默認瀏覽器。

          關注【GeekYawei】,獲取更多開發相關資訊。


          主站蜘蛛池模板: 久热国产精品视频一区二区三区| 日本精品啪啪一区二区三区| 国产伦精品一区二区三区免费迷| 亚洲一区无码精品色| av一区二区三区人妻少妇| 麻豆一区二区在我观看| 亚洲AV成人一区二区三区AV| 中文字幕在线一区| 精品一区二区三区四区电影| 国产福利日本一区二区三区| 国产主播在线一区| 蜜芽亚洲av无码一区二区三区| 国产精品福利一区二区久久| 亚洲一区动漫卡通在线播放| 色欲AV蜜臀一区二区三区 | 日韩成人无码一区二区三区| 久久免费区一区二区三波多野| 中文字幕无码一区二区免费| 中文字幕Av一区乱码| 少妇无码AV无码一区| 好看的电影网站亚洲一区| 日本一区二区三区高清| 国产一区二区三区在线2021| 国产一区二区三区在线2021| 亚洲av午夜福利精品一区| 无码av免费一区二区三区试看 | 天堂Av无码Av一区二区三区| 午夜福利无码一区二区| 国产精品一区二区无线| 三级韩国一区久久二区综合| 香蕉免费看一区二区三区| 一区二区三区无码视频免费福利 | 国产精品亚洲一区二区三区久久| 国产乱码精品一区二区三区| 国产在线一区视频| 波多野结衣一区二区三区aV高清| 亚洲国产精品一区二区第一页| 亚洲美女一区二区三区| 欧洲精品一区二区三区| 国产天堂一区二区综合| 精品一区二区三区自拍图片区|