前言
如何優雅的將專案中的程式碼,亦或是你的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');
}
}
- 使用效果:flutter_smart_dialog
配置程式碼檔案
因為原理是遍歷資原始檔,所以必須將需要展示的程式碼檔案或者其資料夾路徑,定義在assets下,這步操作,為大家提供了一個自動化的外掛解決
強烈建議需要展示到介面的程式碼,都放在統一的資料夾裡管理
- 展示介面的程式碼需要在pugspec.yaml中的assets定義
如果程式碼預覽的資料夾,分級複雜,每次都需要定義路徑實在麻煩
提供一個外掛:Flutter Code Helper
- 安裝:Plugins中搜尋
Flutter Code Helper
- pugspec.yaml中定義下需要自動生成資料夾的路徑,資料夾隨便套娃,會自動幫你遞迴在assets下生成
- 不需要自動生成,可:不寫該配置,或者配置空陣列(auto_folder: [])
code_helper:
auto_folder: [ "assets/", "lib/widgets/" ]
說明下:上面的外掛是基於RayC的FlutterAssetsGenerator外掛專案改的
- 看了下RayC的外掛程式碼和相關功能,和我預想的上面功能實現有一定出入,改動起來變動較大
- 想試下外掛專案的各種新配置,直接拉到最新
- 後期如果想到需要什麼功能,方便隨時新增
所以沒向其外掛裡面提pr,就單獨新開了個外掛專案
高階使用
主題
提供倆種程式碼樣式主題
- 日間模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.light);
- 夜間模式
CodePreview.config = CodePreviewConfig(codeTheme: CodeTheme.dark);
註釋解析
- 你可以使用如下的格式,在類上新增註釋
- key的前面必須加
@
,舉例(@title,@xxx) - key與value的之間,必須使用
分號
分割,舉例(@xxx: xxx) - value如果需要換行,換行的文案前必須加
中劃線
- key的前面必須加
/// @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的輔助工具,會去掃描
- 對映表,宿主可以透過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.yaml
的assets
裡面直接寫萬用字元定義全路徑,然後悲劇了,它不支援這種寫法
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平臺,可以方便的體驗各種元件的效果,結合文章中的程式碼預覽方案,就可以更加快速的上手各種元件用法了~
好了,下次再見了,靚仔們!