Flutter 如何將程式碼顯示到介面上

2023-05-05 09:00:17

前言

如何優雅的將專案中的程式碼,亦或是你的demo程式碼展示到介面上?本文對使用簡單、便於維護且通用的解決方案,進行相關的對比和探究

為了節省大家的時間,把最終解決方案的相關接入和用法寫在前面

預覽程式碼

快速開始

dependencies:
  code_preview: ^0.1.5
  • 用法:CodePreview,提供需要預覽的className,可自動匹配該類對應的程式碼檔案
    • 本來想把寫法簡化成傳入物件,但是因為一些原因無奈放棄,改成了className
    • 具體可以參考下面Flutter Web中的問題模組的說明
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';

class Test extends StatelessWidget {
  const Test({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const CodePreview(className: 'Test');
  }
}

設定程式碼檔案

因為原理是遍歷資原始檔,所以必須將需要展示的程式碼檔案或者其資料夾路徑,定義在assets下,這步操作,為大家提供了一個自動化的外掛解決

強烈建議需要展示到介面的程式碼,都放在統一的資料夾裡管理

  • 展示介面的程式碼需要在pugspec.yaml中的assets定義

如果程式碼預覽的資料夾,分級複雜,每次都需要定義路徑實在麻煩

提供一個外掛:Flutter Code Helper

  • 安裝:Plugins中搜尋Flutter Code Helper

  • pugspec.yaml中定義下需要自動生成資料夾的路徑,資料夾隨便套娃,會自動幫你遞迴在assets下生成
    • 不需要自動生成,可:不寫該設定,或者設定空陣列(auto_folder: [])
code_helper:
  auto_folder: [ "assets/", "lib/widgets/" ]

說明下:上面的外掛是基於RayCFlutterAssetsGenerator外掛專案改的

  • 看了下RayC的外掛程式碼和相關功能,和我預想的上面功能實現有一定出入,改動起來變動較大
  • 想試下外掛專案的各種新設定,直接拉到最新
  • 後期如果想到需要什麼功能,方便隨時新增

所以沒向其外掛裡面提pr,就單獨新開了個外掛專案

高階使用

主題

提供倆種程式碼樣式主題

  • 日間模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.light);

  • 夜間模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.dark);

註釋解析

  • 你可以使用如下的格式,在類上新增註釋
    • key的前面必須加@,舉例(@title,@xxx)
    • key與value的之間,必須使用分號分割,舉例(@xxx: xxx)
    • value如果需要換行,換行的文案前必須加中劃線
/// @title:
///  - test title one
///  - test title two
/// @content: test content
/// @description: test description
class OneWidget extends StatelessWidget {
  const OneWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}
  • 然後可以從customBuilder的回撥獲取param引數,param中擁有parseParam引數
    • 獲取取得上面註釋的資料:param.parseParam['title']或者param.parseParam['***']
    • 獲取的value的型別是List,可相容多行value的型別
  • customBuilder的用法
    • codeWidget內建的程式碼預覽佈局,如果你想定義自己預覽程式碼的佈局,那就可以不使用codeWidget
    • 一般來說,可以根據註釋獲取的資料,結合codeWidget巢狀來自定義符合要求的佈局
    • param中含有多個有用內容,可自行檢視
import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';

class Test extends StatelessWidget {
  const Test({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CodePreview(
      className: 'OneWidget',
      customBuilder: (Widget codeWidget, CustomParam? param) {
        debugPrint(param?.parseParam['title'].toString());
        debugPrint(param?.parseParam['content'].toString());
        debugPrint(param?.parseParam['description'].toString());
        return codeWidget;
      },
    );
  }
}
  • 目前內部預覽的佈局,會自動去掉類上的註釋,如果想保留註釋,可自行匹配下
 CodePreview.config = CodePreviewConfig(removeParseComment: false);

幾種程式碼預覽方案

FlutterUnit方案

FlutterUnit專案也是自帶程式碼預覽方案,這套方案是比較特殊方案

  • 大概看了下,整個FlutterUnit的資料都是基於flutter.db,該檔案裡面就有相關demo的文字資訊
  • 所有的demo也是單獨存在一個叫widgets的專案中
  • 所以大概可以猜測出
    • 應該會有個db的輔助工具,會去掃描widgets的專案中的demo程式碼
    • 將他們的文字資訊都掃描出來,然後解析上面的註釋等相關資訊,分類儲存到資料庫中,最後生成db檔案

  • 對映表,宿主可以通過db中的元件類名,從這裡拿到demo效果範例

總結

整套流程看下來,實現起來的工作量還是有點大的

  • db輔助工具的編寫
  • 文字註釋相關解析規則
  • 如何便捷的維護db檔案(輔助工具是否支援,生成後自動覆蓋宿主db檔案)
  • 不同平臺db檔案的讀取和相關適配

優點

  • 因為掃描工具不依賴Flutter相關庫,預覽方案可以快速的移植到其它程式語言(compose,SwiftUI等)
  • 具備高度自定義,因為是完全獨立的第三方掃描工具,可以隨性所欲的客製化化

缺點

  • 最明顯的缺點,應該就是稍微改下demo程式碼,就需要三方工具重新生成db檔案(如果三方工具實現的是cli工具,可以將掃描生成命令和push等命令整合一起,應該可以比較好的避免該問題)

build_runner方案

build_runner是個強大程式碼自動生成工具,根據ast語法樹+自定義註解資訊,可以生成很多強大的附屬程式碼資訊,例如 json_serializable等庫

所以,也能利用這點自定義類註解,獲取到對應的整個類的程式碼資訊,在對應附屬的xx.g.dart檔案中,將獲取的程式碼內容轉換成字串,然後直接將xx.g.dart檔案的程式碼字串資訊,展示到介面就行了

優點

  • 可以通過生成命令,全自動的生成程式碼,甚至將整個預覽demo的對映表都可以自動設定完成
  • 可以規範的通過註解設定多個引數

缺點

  • 因為build_runner需要解析整個ast語法樹,一旦專案很大之後,解析生成的時間會非常非常的長!
  • 因為現在很多的這類庫都是依賴build_runner,所以跑自動生成命令,會導致巨多xx.g.dart檔案被改動,極大的增加cr工作量

資原始檔方案

這應該最常用的一種方案

  • pubspec.yaml中的assets中定義下我們程式碼檔案路徑
flutter:
  assets:
    - lib/widgets/show/
  • 然後用loadString獲取檔案內容
final code = await rootBundle.loadString('lib/widgets/show/custome_dialog_animation.dart');

優點

  • 侵入性非常低,不會像build_runnner方案那樣影響到其它模組
  • 便於維護,如果demo預覽程式碼被改變了,打包的時候,資原始檔也會生成對應改變後的程式碼檔案

缺點

  • 使用麻煩,使用的時候需要傳入具體的檔案路徑,才能找到想要的程式碼資原始檔
  • 需要反覆的在pubspec.yaml中的assets裡面定義檔案路徑

資原始檔方案優化

上面的三種方案各有優缺點,明確當前的訴求

  • 目前是想寫個簡單的,通用的,僅在Flutter中實現程式碼預覽方案

  • 要求使用簡單,高效

  • 維護簡單,多人開發的時候不會有很大成本

FlutterUnit方案:實現起來成本較大,且多人開發對單個db檔案的維護很可能會有點問題,例如:更新程式碼的時候,db檔案忘記更新

build_runner方案:生成時間是個問題,還有很對其他型別xx.g.dart檔案產生影響也比較麻煩

資原始檔方案:整體是符合預期的,但是使用時候,需要傳入路徑和pubspec.yaml中反覆定義檔案路徑,這是倆個很大痛點

結合實現成本和訴求,選擇資原始檔方案,下面對其痛點進行優化

使用優化

Flutter的編譯產物中,有個相當有用的檔案:AssetManifest.json

AssetManifest.json檔案裡面,有所有的資原始檔的路徑,然後就簡單了,我們只需要讀取該檔案內容

final manifestContent = await rootBundle.loadString('AssetManifest.json');

獲取到所有的路徑之後,再結合傳入的類名,讀取所有路徑的檔案內容,然後和傳入的類名做正則匹配就行了

稍微優化

  • 將傳入的類名,轉換為下劃線名稱和所有路徑名稱做匹配,如果能匹配上,再進行內容匹配,匹配成功後就返回該檔案的程式碼內容
  • 如果上述匹配失敗,就進行兜底的全量匹配

優化前

import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';

class Test extends StatelessWidget {
  const Test({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const CodePreview(path: 'lib/widgets/show/custome_dialog_animation.dart');
  }
}

優化後

import 'package:code_preview/code_preview.dart';
import 'package:flutter/material.dart';

class Test extends StatelessWidget {
  const Test({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const CodePreview(className: 'CustomDialogAnimation');
  }
}
  • 一般來說,我是統一設定預覽demo和className,這樣比較好對照

路徑定義優化

本來是想在pubspec.yamlassets裡面直接寫萬用字元定義全路徑,然後悲劇了,它不支援這種寫法

flutter:
  assets:
    - lib/widgets/**/*.dart

GG,只能想其他辦法了,想了很多方法都不行,只能從外部入手,用idea外掛的形式,實現自動化掃描生成路徑

  • 安裝:Plugins中搜尋Flutter Code Helper

  • pugspec.yaml中定義下需要自動生成資料夾的目錄,資料夾隨便套娃,會自動幫你遞迴在assets下生成
    • 不需要自動生成,可:不寫該設定,或者設定空陣列(auto_folder: [])
code_helper:
  auto_folder: [ "assets/", "lib/widgets/" ]

Flutter Web中的問題

魔幻的runtimeType

flutter web的release模式中

  • dart2js 會壓縮 JS,這樣會使得型別名被改變
  • 例如:dart中的TestWidgetFunction類的runtimeType,可能會變成minified:Ah,而不是TestWidgetFunction

為啥需要壓縮呢?壓縮名稱可以使得編譯器將 JavaScript體積縮小 3 倍+;精確等效語意和效能/程式碼大小之間的權衡,Dart明顯是選擇了後者

這種情況只會在Flutter Web的release模式下發生,其他平臺和Flutter web的Debug | Profile模式都不會有這種問題;所以說Xxx.runtimeType.toString,並不一定會得到預期內的資料。。。

具體討論可參考

解決思路

  • 將壓縮型別minified:Ah 恢復成 Test
  • 將獲取的Test字串使用相同演演算法壓縮成minified:Ah

如有知道如何實現的,務必告訴鄙人

下面從壓縮級別調整的角度,探究是否可解決該問題

dart2js壓縮說明

注:flutter build web預設的是O4優化級別

  • O0: 禁用許多優化。
  • O1: 啟用預設優化(僅是dart2js該命令的預設級別)
  • O2: 在O1優化基礎上,尊重語言語意且對所有程式安全的其他優化(例如縮小)
    • 備註:使用-O2,使用開發JavaScript編譯器編譯時,型別的字串表示不再與Dart VM中的字串表示相同
  • O3: 在O2優化基礎上,並省略隱式型別檢查。
    • 注意:省略型別檢查可能會導致應用程式因型別錯誤而崩潰
  • O4: 在O3優化基礎上,啟用更積極的優化
    • 注意:O4優化容易受到輸入資料變化的影響,在依賴O4之前,需測試使用者輸入中的邊緣情況

下面是flutter新建專案,未做任何改動,不同壓縮級別的js產物體積

# main.dart.js: 7.379MB
flutter build web --dart2js-optimization O0 
# main.dart.js: 5.073MB
flutter build web --dart2js-optimization O1
# main.dart.js: 1.776MB
flutter build web --dart2js-optimization O2
# main.dart.js: 1.716MB
flutter build web --dart2js-optimization O3
# main.dart.js: 1.687MB
flutter build web --dart2js-optimization O4

總結

  • 預期用法
    • 為什麼想使用物件?因為當物件名稱改變時,對應使用的地方,可以便捷觀察到需要改變
    • 可以使用傳入的物件範例,在內部使用runtimeType獲取型別名,再進行相關匹配
CodePreview(code: Test());

但是

綜上可知,使用flutter build web --dart2js-optimization O1編譯的flutter web release產物,能夠使得runtimeType的語意和Dart VM中字串保持一致

但是該壓縮級別下的,js體積過於誇張,務必會對載入速度產生極大影響,可想而知,在複雜專案中的體積增漲肯定更加離譜

對於想要用法更加簡單,使用低階別壓縮命令打包的想法需要捨棄

  • 用法不得已做妥協
CodePreview(className: "Test");

這是個讓我非常糾結的思路歷程

最後

到這裡也結束了,自我感覺,對大家應該能有一些幫助

一般來說,大部分團隊,都會有個自己的內部元件庫,因為Flutter強大的跨平臺特性,所以就能很輕鬆的釋出到web平臺,可以方便的體驗各種元件的效果,結合文章中的程式碼預覽方案,就可以更加快速的上手各種元件用法了~

好了,下次再見了,靚仔們!