Flutter 2.0 提供了全平臺(tái)構(gòu)建應(yīng)用支持(iOS, Android, Windows, macOS, Linux, 嵌入式), 當(dāng)然也包括 Web 的穩(wěn)定版呀邢。
Flutter Web 整體的體驗(yàn)上還不錯(cuò)掷空,但依然還有很多需要完善的地方羡铲,在實(shí)際項(xiàng)目實(shí)踐中發(fā)現(xiàn)以下問題:
- SEO 不友好舷手。搜索引擎無(wú)法收錄眉尸,網(wǎng)頁(yè)沒辦法獲取自然流量秒紧。
- 包體積過大栗恩。main.dart.js 單個(gè) js 文件沒有拆包且整體包體積過大,導(dǎo)致網(wǎng)頁(yè)加載速度慢透乾。
- 頁(yè)面沒有緩存首次加載,canvaskit 下載時(shí)間過長(zhǎng)磕秤。如果使用 canvaskit 方式來(lái)渲染 web 需要額外加載 2.5MB 左右的 canvaskit.wasm乳乌。
- 白屏用戶體驗(yàn)差。由于網(wǎng)頁(yè)加載過慢市咆,官方尚未提供相應(yīng)加載指示器或 API 來(lái)控制頁(yè)面加載狀態(tài)汉操。
主要問題就是整體包體積過大導(dǎo)致首次無(wú)緩存情況下頁(yè)面加載很慢。整體的思路主要圍繞優(yōu)化體積蒙兰,代碼懶加載磷瘤,增加加載指示器等方面來(lái)提升頁(yè)面加載時(shí)的用戶體驗(yàn)。
優(yōu)化包體積
main.dart.js
默認(rèn)情況下 Flutter Web 會(huì)將所有 Dart
代碼全部轉(zhuǎn)譯為一個(gè) JS
文件即 main.dart.js
搜变。這樣會(huì)導(dǎo)致包體積很大采缚。我們可以使用延遲加載的方式進(jìn)行按需加載, 比如在跳轉(zhuǎn)某一個(gè)頁(yè)面的時(shí)候再對(duì)該頁(yè)面的代碼進(jìn)行加載。
Dart 當(dāng)中提供了 deferred
關(guān)鍵字來(lái)進(jìn)行延時(shí)加載痹雅。具體使用方式可以查看官方文檔仰担。
同時(shí) Gallery 項(xiàng)目中對(duì) deferred
進(jìn)行了封裝處理糊识,我對(duì)該 Widget 進(jìn)行簡(jiǎn)化可以直接拿來(lái)參考使用绩社。
import 'dart:async';
import 'package:ealing_widget/common/common_color.dart';
import 'package:flutter/material.dart';
typedef LibraryLoader = Future<void> Function();
typedef DeferredWidgetBuilder = Widget Function();
///延遲加載組件
class DeferredWidget extends StatefulWidget {
DeferredWidget(this.libraryLoader, this.createWidget,
{Key? key, Widget? placeholder})
: placeholder =
placeholder ?? Container(color: CommonColors.color_widget_background),
super(key: key);
final LibraryLoader libraryLoader;
final DeferredWidgetBuilder createWidget;
final Widget placeholder;
// 存儲(chǔ) libraryLoader 對(duì)應(yīng)的 future 數(shù)據(jù)
static final Map<LibraryLoader, Future<void>> _moduleLoaders = {};
// 存儲(chǔ)已經(jīng)預(yù)加載過了的 libraryLoader
static final Set<LibraryLoader> _loadedModules = {};
static Future<void>? preload(LibraryLoader loader) {
if (!_moduleLoaders.containsKey(loader)) {
_moduleLoaders[loader] = loader().then((dynamic _) {
_loadedModules.add(loader);
});
}
return _moduleLoaders[loader];
}
@override
_DeferredWidgetState createState() => _DeferredWidgetState();
}
class _DeferredWidgetState extends State<DeferredWidget> {
Widget? _loadedChild;
@override
void initState() {
if (DeferredWidget._loadedModules.contains(widget.libraryLoader)) {
_onLibraryLoaded();
} else {
DeferredWidget.preload(widget.libraryLoader)
?.then((dynamic _) => _onLibraryLoaded());
}
super.initState();
}
void _onLibraryLoaded() {
setState(() {
_loadedChild = widget.createWidget();
});
}
@override
Widget build(BuildContext context) {
return _loadedChild ?? widget.placeholder;
}
}
經(jīng)過 deferred
處理最后生成的 JS 文件就會(huì)被拆分很多個(gè)摔蓝,主體 main.dart.js
文件體積也會(huì)變小很多。
字體
如果項(xiàng)目當(dāng)中沒有自定義字體愉耙,F(xiàn)lutter 默認(rèn)會(huì)打包 MaterialIcons-Regular.otf
和 CupertinoIcons.ttf
這兩種字體庫(kù)贮尉。
這兩種字體是包含了一些預(yù)置的 Material 和 iOS 設(shè)計(jì)風(fēng)格 icon,所有體積較大朴沿,如果項(xiàng)目當(dāng)中沒有用到相關(guān)的 Icon猜谚,我們可以對(duì)這些字體庫(kù)進(jìn)行簡(jiǎn)化。
從 material-design-icons 中下載
MaterialIcons-Regular.ttf
字體庫(kù)赌渣。在 Web 產(chǎn)物中, 將
MaterialIcons-Regular.ttf
原有的./build/web/assets/fonts/MaterialIcons-Regular.otf
字體庫(kù)魏铅。可以將
build/web/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf
的字體庫(kù)刪除。修改
build/web/assets/FontManifest.json
中字體的相關(guān)配置坚芜。需要修改字體格式以及去除不用字體的引用路徑览芳。
以上操作可以通過編寫腳本方式在生成 Web 產(chǎn)物之后進(jìn)行修改。
canvaskit.wasm
canvaskit.wasm 問題在于要加載額外的 2.5 MB左右的庫(kù)鸿竖,拖慢了網(wǎng)頁(yè)的加載速度沧竟。目前有兩種解決方案:
將渲染方式改為 html。 但實(shí)際上使用 canvaskit 這種渲染方式是比較貼近原生的使用體驗(yàn)的缚忧。
更換 canvaskit.wasm CDN 地址悟泵。推薦一個(gè)對(duì)開源文件免費(fèi)的 CDN:jsdelivr 。接著在執(zhí)行 flutter build web 生成產(chǎn)物的時(shí)候需要增加參數(shù)
--dart-define=FLUTTER_WEB_CANVASKIT_URL
來(lái)修改 Flutter 預(yù)設(shè)的地址闪水。例如:
flutter build web --release --web-renderer canvaskit --dart-define=FLUTTER_WEB_CANVASKIT_URL=https://cdn.jsdelivr.net/npm/canvaskit-wasm@0.25.1/bin/
gzip
服務(wù)端開啟 gzip
之后糕非,拉取壓縮過的資源速度會(huì)更快一些。
Loading
在 index.html
里面放一張圖片并設(shè)置相應(yīng)的樣式球榆,等程序進(jìn)入主入口渲染第一幀或者在合適的時(shí)機(jī)把該元素移除即可峰弹。貼一些相關(guān)代碼供參考。
<!-- index.html -->
<div id="loading">
<style>
html, body {
width: 100%;
height: 100%;
}
#loading {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
background-color: white;
}
#loading img {
width: 150px;
height: 150px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -75px;
margin-top: -75px;
}
</style>
<img src="loading.gif">
</div>
在合適的時(shí)機(jī)移除該 <img> 元素芜果。
if(kIsWeb){
var loadingElement = html.document.getElementById("loading");
loadingElement?.remove();
}
最后
Flutter Web 穩(wěn)定版剛出來(lái)還需要一些時(shí)間來(lái)完善這些問題鞠呈,F(xiàn)lutter 團(tuán)隊(duì)也給出了后續(xù)需要著重要優(yōu)化的點(diǎn) Flutter: What’s next on the Web?,有興趣可以了解一下右钾。同時(shí)如果有疑問或者有更好的解決方案歡迎與我一起討論蚁吝。