Flutter-Web從0到部署上線(實(shí)踐+埋坑)


本文字?jǐn)?shù): 7743 字
預(yù)計(jì)閱讀時(shí)間: 60 分鐘
01
前言
首先說明一下,這篇文章是給 具備Flutter開發(fā)經(jīng)驗(yàn)的客戶端同學(xué) 看的。 Flutter 的誕生雖然來自 Google 的 Chrome 團(tuán)隊(duì),但大家都知道 Flutter 最先支持的平臺(tái)是 Android 和 iOS ,至今最核心的維護(hù)平臺(tái)依然是 Android 和 iOS 。由于 dart 語言的學(xué)習(xí)成本不高, Flutter 的響應(yīng)式UI與 ComposeUI 和 SwiftUI 都有極大的相似之處,整體的架構(gòu)思路也更偏向于客戶端的模式,再加上為了實(shí)現(xiàn)很多硬件或 Native 相關(guān)的基礎(chǔ)功能也需要專業(yè)的客戶端開發(fā)知識(shí),所以 Flutter 更多的是被客戶端開發(fā)同學(xué)認(rèn)可并使用(在我們的團(tuán)隊(duì)中, Flutter 已經(jīng)是客戶端開發(fā)同學(xué)的必備基本技能)。 在此背景下, Flutter 最初并不在 web 端上發(fā)力。不過由于 Flutter 本身就是攜帶了 web 的基因,在 Flutter2 發(fā)布的同時(shí)也發(fā)布了 web 的穩(wěn)定版。那么它有什么優(yōu)勢(shì)和劣勢(shì)呢?
-
優(yōu)勢(shì): 1. 零學(xué)習(xí)成本: 當(dāng)你已經(jīng)掌握了 Flutter 開發(fā)能力后,哪怕你對(duì) html , css , JavaScript 和主流的前端框架不那么了解,也不影響你開發(fā) web 應(yīng)用。 2. 跨端能力: 可將現(xiàn)有 Flutter 移動(dòng)應(yīng)用拓展到 web ,在多個(gè)平臺(tái)共享代碼,降低開發(fā)成本。
-
劣勢(shì): 1. 兼容性問題: 使用 html 模式來進(jìn)行渲染時(shí),應(yīng)用的大小相對(duì)較小但可能會(huì)出現(xiàn)兼容性問題。 2. 包體積增加: 使用 canvaskit 模式來進(jìn)行渲染時(shí),雖然性能較好,且可以降低不同瀏覽器渲染效果不一致的風(fēng)險(xiǎn),但會(huì)增加包體積。
分析了優(yōu)勢(shì)劣勢(shì)后,我們發(fā)現(xiàn)如果單純的做個(gè) web 端應(yīng)用, Flutter 并沒有優(yōu)勢(shì),前端開發(fā)同學(xué)大概也不會(huì)使用 Flutter 進(jìn)行 web 開發(fā)(確實(shí)沒必要,比如包體積增加或有一定的性能損失,還需要學(xué)習(xí)新語言與開發(fā)思路,原生開發(fā)不香么), Flutter Web 到底有什么用呢? 帶著這樣的想法,在使用 Flutter 后的很長(zhǎng)時(shí)間都不曾調(diào)研過 web 端的支持。但隨著業(yè)務(wù)和內(nèi)部需求的發(fā)展變化,我們有了使用 Flutter 進(jìn)行 web 開發(fā)的想法。下面我來說一下使用 Flutter Web 主要的三個(gè)場(chǎng)景。
02
Flutter Web的使用場(chǎng)景
1、客戶端團(tuán)隊(duì)內(nèi)部的web需求
在后疫情時(shí)代降本增效的大背景下,我們會(huì)更多的使用自研工具。自研工具的使用和結(jié)果展示的可視化通常以網(wǎng)頁的形式展現(xiàn)。客戶端同學(xué)使用 Flutter Web 進(jìn)行網(wǎng)頁開發(fā)學(xué)習(xí)成本低,完全可以快速的開發(fā)網(wǎng)頁(本人在使用 Vue 框架進(jìn)行 web 端開發(fā)時(shí)感受出客戶端和前端的 UI 布局思路還是有很大不同的, css 很靈活約束性低,這個(gè)與客戶端布局的強(qiáng)約束性差異很大,所以對(duì)于客戶端開發(fā)來說,使用 Flutter 開發(fā)網(wǎng)頁應(yīng)用時(shí)更順手。對(duì)于全員掌握 Flutter 技能的我們團(tuán)隊(duì)來說已經(jīng)是0成本了)。
2、簡(jiǎn)單的web端業(yè)務(wù)需求web 端承載了很多活動(dòng)需求,這些需求的特點(diǎn)是時(shí)效性強(qiáng),功能較簡(jiǎn)單,且不需長(zhǎng)期維護(hù)。但這些需求經(jīng)常是在某一時(shí)間段大量產(chǎn)生的(比如逢年過節(jié)的一些活動(dòng)或榜單),或突然產(chǎn)生的(比如蹭熱點(diǎn)的即時(shí)需求)。這些工作的插入有時(shí)會(huì)導(dǎo)致一些長(zhǎng)期迭代的 web 端需求需要延期,影響團(tuán)隊(duì)的整體排期。由于這些需求開發(fā)難度不大,性能要求不高,不需長(zhǎng)期維護(hù)(意味著即使團(tuán)隊(duì)里不再有人使用 Flutter 或 Flutter Web 有一天掛了也沒什么影響),那么就可以讓 Flutter 開發(fā)同學(xué)加入進(jìn)來,平攤了一部分工作,以此來提升整個(gè)團(tuán)隊(duì)的效率。
3、客戶端與web端的跨端隨著 Flutter Web 趨于穩(wěn)定,用 Flutter 實(shí)現(xiàn)的 App 可以低成本的被打包成 web 版了,畢竟對(duì)于用戶來說使用瀏覽器打開個(gè)網(wǎng)頁比下載個(gè) App 成本低多了。這種情況下我們就可以利用 Flutter 的跨端優(yōu)勢(shì),節(jié)約很多人力資源,避免去重新開發(fā)一套 web 端了。
好的既然有了使用場(chǎng)景,我們就好好來走一下 Flutter Web 是怎么開發(fā)部署上線的流程。
03
Flutter Web工程的創(chuàng)建和業(yè)務(wù)實(shí)現(xiàn)
我們使用 Android Studio 作為IDE,以 Flutter 3.10.5 版本為基礎(chǔ)創(chuàng)建一個(gè) Flutter Web 工程。 創(chuàng)建一個(gè) New Flutter Project ,在選擇 Platforms 的時(shí)候只勾選 Web ,然后直接 Create 。
然后我們發(fā)現(xiàn)在工程目錄里多了個(gè) web 的文件夾:
如果你是為現(xiàn)有的 Flutter 工程添加 Web 的支持,只需在項(xiàng)目根目錄運(yùn)行如下命令即可:
flutter create --platforms=web .
項(xiàng)目創(chuàng)建好了,如果想要 run 起來只需選擇 chrome 瀏覽器,點(diǎn)擊 run 就行了:
然后我們就可以在瀏覽器看到運(yùn)行結(jié)果了,當(dāng)然我們也可以打開開發(fā)者模式方便查看與調(diào)試:
這部分跑通后,非常恭喜你可以愉快的用 Flutter 開發(fā)網(wǎng)頁了,接下來我們實(shí)現(xiàn)一個(gè)業(yè)務(wù)需求:做一個(gè)網(wǎng)頁搜索功能。
業(yè)務(wù)功能上的開發(fā)實(shí)現(xiàn)我就不做贅述了,可以告訴做過 Flutter 開發(fā)的同學(xué),沒什么不同,基礎(chǔ)配置/網(wǎng)絡(luò)模塊/數(shù)據(jù)共享/路由等該怎么封裝就怎么封裝,我也不過是直接拿了之前客戶端 Flutter 工程相應(yīng)模塊的代碼,稍作修改而已。 UI 上的開發(fā)也是該怎么布局怎么布局,業(yè)務(wù)的開發(fā)體驗(yàn)上和客戶端使用 Flutter 沒什么不同。
2、window在 web 端開發(fā)的時(shí)候我們通常會(huì)使用 window 對(duì)象進(jìn)行一些操作。 window 對(duì)象代表一個(gè)瀏覽器窗口或一個(gè)框架。常用的 event 監(jiān)聽,打開網(wǎng)頁等操作都需要 window 對(duì)象。 Flutter 自帶的 dart:html 封裝了 window ,我們可以通過它來實(shí)現(xiàn)獲取 window 的屬性或?qū)?nbsp; window 進(jìn)行操作,比如:
//打開網(wǎng)頁
window.open("http://www.baidu.com","");
//監(jiān)聽event
window.addEventListener("mousedown", (event) => {
//do something
});
另外 window 也可以幫助我們區(qū)分運(yùn)行環(huán)境。
3、瀏覽器運(yùn)行環(huán)境區(qū)分 客戶端通常需要區(qū)分的是 Android 和 iOS 這兩個(gè)不同的運(yùn)行環(huán)境,而 web 端是需要通過 UA 來區(qū)分不同的瀏覽器環(huán)境的,不同環(huán)境下的UI/邏輯等會(huì)有差別。在國內(nèi),我們最常需要區(qū)分 PC 端/移動(dòng)端/ Android 端/ iOS 端/微信網(wǎng)頁/微信小程序這幾個(gè)。那么我們可以定義一個(gè)類,利用 window.navigator.userAgent 去區(qū)分這些環(huán)境:
import 'dart:html';
class DeviceUtil {
static final DeviceUtil _instance = DeviceUtil._private();
static DeviceUtil get() => _instance;
factory DeviceUtil() => _instance;
late String ua;
DeviceUtil._private() {
ua = window.navigator.userAgent;
}
//移動(dòng)端
isMobile() {
return RegExp(
r'phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone')
.hasMatch(ua);
}
//iOS端
isIos() {
return RegExp(r'\(i[^;]+;( U;)? CPU.+Mac OS X').hasMatch(ua);
}
//Android端
isAndroid() {
var isAndroid = ua.contains("Android") || ua.contains("Adr");
return isAndroid;
}
//微信環(huán)境
isWechat() {
return ua.contains("MicroMessenger");
}
//微信小程序環(huán)境
isMiniprogram() {
if (ua.contains("micromessenger")) {
//微信環(huán)境下
if (ua.contains("miniprogram")) {
//小程序;
return true;
}
}
return false;
}
}
4、開發(fā)/測(cè)試/生產(chǎn)環(huán)境區(qū)分
同客戶端一樣,web 端也需要區(qū)分開發(fā)/測(cè)試/生產(chǎn)環(huán)境。同客戶端的方式一樣,我們還是可以通過配置不同的入口文件來實(shí)現(xiàn)環(huán)境的區(qū)分。如:
-
main_dev.dart
void main() {
AppConfig.init(ConfigType.dev);
root_main.main();
}
-
main_test.dart
void main() {
AppConfig.init(ConfigType.test);
root_main.main();
}
-
main_online.dart
void main() {
AppConfig.init(ConfigType.online);
root_main.main();
}
在 AppConfig.init() 就可以根據(jù)不同的環(huán)境做不同的配置了。
5、其他常用庫或插件關(guān)于數(shù)據(jù)共享/網(wǎng)絡(luò)/ UI /動(dòng)畫等庫就不做介紹了,因?yàn)檫@些庫和平臺(tái)不相關(guān),用各自熟悉的就好,下面是來介紹一下為了實(shí)現(xiàn)一些瀏覽器相關(guān)功能需要用到的插件。
-
shared_preferences 在客戶端開發(fā)的時(shí)候,我們知道如果需要對(duì)一些數(shù)據(jù)實(shí)現(xiàn)輕量級(jí)的本地序列化可以使用 shared_preferences ,其實(shí)現(xiàn)對(duì)應(yīng) Android 的 SharedPreferences 和 iOS 的 NSUserDefaults 。而在進(jìn)行 web 開發(fā)的時(shí)候,我們知道如需在本地序列化一些數(shù)據(jù)的話,可以使用 LocalStorage 。其實(shí) Flutter 的 shared_preferences 插件也是支持 web 的,其實(shí)現(xiàn)也正是封裝了 LocalStorage 。關(guān)于 shared_preferences 的使用也不做贅述了,已經(jīng)非常熟悉了。
-
image_picker_for_web 來自于我們熟悉的 image_picker 插件。根據(jù)瀏覽器的不同,支持或部分支持拍照/拍視頻/讀取圖片/讀取視頻等。
-
js 這個(gè)插件是用來使用注解的方式幫助你用 Dart 調(diào)用 JavaScript API 或用 JavaScript 調(diào)用 Dart API 的。
好了,到此為止,我覺著使用 Flutter 開發(fā)一個(gè)常規(guī)的 web 業(yè)務(wù)已經(jīng)不成問題了。接下來我們探討一下如何調(diào)試呢?
04
調(diào)試
跑通后應(yīng)該如何調(diào)試呢?我們先來說明一下 PC 端的調(diào)試方式。
1、PC端調(diào)試如果熟悉瀏覽器開發(fā)者模式,可直接使用瀏覽器進(jìn)行調(diào)試,打 log 或 debug 都是沒問題的,也可以看到源碼,可以抓包:
當(dāng)然客戶端同學(xué)可能不熟悉瀏覽器開發(fā)者模式,也沒關(guān)系,利用 Android Studio ,之前在客戶端寫 Flutter 怎么調(diào)試,現(xiàn)在寫 web 端依舊可以怎么調(diào)試。 介紹完 PC 端的調(diào)試,那么在移動(dòng)端應(yīng)該如何調(diào)試呢?
2、移動(dòng)端調(diào)試
我們依舊可以用 PC 上的瀏覽器,紅色箭頭指向的位置可以切換至移動(dòng)端模擬器設(shè)備,可以選擇機(jī)型。但更多的時(shí)候,我們希望可以真機(jī)調(diào)試。熟悉 vue 框架的同學(xué)都知道,在本地調(diào)試的時(shí)候,會(huì)給出兩個(gè)地址,如下圖所示:
我們可以在手機(jī)瀏覽器上輸入 Network 顯示的 ip 地址進(jìn)行調(diào)試。在 Flutter 環(huán)境上并沒有提供相應(yīng)的 ip 地址,我們可以通過 flutter 的本地打包命令指定一個(gè)地址,如下所示:
flutter run -d chrome --web-hostname 10.2.136.130 -t lib/main_test.dart --web-port 8080
指定本機(jī)的 ip 地址和端口號(hào),然后在手機(jī)瀏覽器上輸入:
10.2.136.130:8080
之后我們?nèi)绾慰吹秸{(diào)試信息呢?由于使用 Chrome 瀏覽器需要科學(xué)上網(wǎng),在此我們以 iPhone 的 Safari 瀏覽器+ PC 端的 Safari 瀏覽器為例:
-
1.首先我們需要用數(shù)據(jù)線將手機(jī)和電腦連接起來。
-
2.找到 Safari 的 開發(fā) 菜單,找到你手機(jī)的名稱,然后選擇相應(yīng)的地址,如下圖所示:

-
3.然后我們就可以看到網(wǎng)頁檢查器進(jìn)行調(diào)試了,如下圖所示:

如何進(jìn)行調(diào)試我們已經(jīng)清楚了,假設(shè)我們已經(jīng)開發(fā)完成了,如何打包部署上線呢?
05
打包部署上線
Flutter Web 的打包非常簡(jiǎn)單,運(yùn)行:
flutter build web
即可。但這樣顯然是不夠的,因?yàn)槲覀冃枰獏^(qū)分環(huán)境來打不通的包。 在上一章節(jié)我們配置了不同的入口文件,我們以 dev 環(huán)境為例,其入口文件是 main_dev ,那么我們的打包命令就變成了:
flutter build web -t lib/main_dev.dart
這行命令執(zhí)行完成后,報(bào)錯(cuò)了,報(bào)錯(cuò)信息如下:
這是個(gè)圖標(biāo)數(shù)據(jù)加載問題,我們加上 --no-tree-shake-icons 即可。執(zhí)行命令如下:
flutter build web -t lib/main_dev.dart --no-tree-shake-icons
然后我們就會(huì)在項(xiàng)目根目錄的 build 文件夾下找到 web 這個(gè)文件夾,對(duì)應(yīng)的就是 web 前端打出來的 dist 文件夾。包含了以下文件:
編譯產(chǎn)物有了,那么如何部署呢?
2、部署官方給了如下的部署方式:
https://flutter.cn/docs/deployment/web#deploying-to-the-web
看了官方文檔后我發(fā)現(xiàn),這三種部署方式并不適用于我們的項(xiàng)目。由于 CDN 具有提高網(wǎng)站性能和用戶體驗(yàn),減輕原始服務(wù)器的負(fù)載等優(yōu)勢(shì),目前我們團(tuán)隊(duì)已經(jīng)搭建了 CDN 部署平臺(tái)。既然如此,我們的部署方案也需要往這方面靠。 CDN 部署配置主要要解決的問題就是各種資源的路徑問題。
(1)修改index.html的CDN資源路徑
我先來簡(jiǎn)單說明一下 FlutterWeb 編譯產(chǎn)物,如下圖所示:
assets 包含了我們所有的靜態(tài)資源文件:包括圖片,字體文件等。 最重要是 flutter.js 和 main.dart.js 這兩個(gè)文件。其中 flutter.js 為入口的 js 文件,我們可以打開 web 目錄下 index.html :
<!DOCTYPE html>
<html>
<head>
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_web">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<link rel="icon" type="image/png" href="favicon.png"/>
<title>flutter_web</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
</script>-->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine({
}).then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
</body>
</html>
看到 <script src="flutter.js" defer></script> 這行。而 main.dart.js 是我們的 dart 業(yè)務(wù)代碼被編譯成的 js 文件。 flutter.js 會(huì)加載 main.dart.js 和其它文件。默認(rèn)情況下, flutter.js 會(huì)加載各個(gè)文件,包括資源文件( assets )都使用的是相對(duì)路徑。首先就是通過 loadEntrypoint () 方法加載 main.dart.js 這個(gè)文件:
//flutter.js
async loadEntrypoint(options) {
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } =
options || {};
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
}
但我們發(fā)現(xiàn)貌似 entrypointUrl 是可以自己傳遞的,于是我們從官網(wǎng)文檔里找到了 自定義web應(yīng)用初始化 的鏈接: https://flutter.cn/docs/platform-integration/web/initialization 有如下的參數(shù)可傳:
其中 loadEntrypoint() 方法可以傳遞 entrypointUrl 參數(shù)來指定 main.dart.js 的路徑。而 initializeEngine() 方法可以通過傳遞 assetBase 參數(shù)來指定 CDN 資源路徑。這么看來我們完全可以通過將這兩個(gè)參數(shù)設(shè)置為絕對(duì)路徑來解決 main.dart.js 的加載與 CDN 資源路徑的問題。需要注意的是 initializeEngine() 方法是 Flutter3.7.0 開始才支持的。 我們改一下 index.html :
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
entrypointUrl: "YOUR_CDN_ABSOLUTE_PATH/main.dart.js",
onEntrypointLoaded: function(engineInitializer) {
engineInitializer.initializeEngine({
assetBase: "YOUR_CDN_ABSOLUTE_PATH"
}).then(function(appRunner) {
appRunner.runApp();
});
}
});
});
我們?cè)俅騻€(gè)包,還是會(huì)報(bào)錯(cuò),找不到 flutter.js ,還是因?yàn)槁窂絾栴}。處理方式更簡(jiǎn)單了,直接在 index.html 里配置成絕對(duì)路徑即可。另外我們發(fā)現(xiàn) Icon-192.png , favicon.png , manifest.json 這幾個(gè)文件也是相對(duì)路徑,那么我們一次性都改成絕對(duì)路徑:
<head>
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_web">
<link rel="apple-touch-icon" href="YOUR_CDN_ABSOLUTE_PATH/icons/Icon-192.png">
<link rel="icon" type="image/png" href="YOUR_CDN_ABSOLUTE_PATH/favicon.png"/>
<title>flutter_web</title>
<link rel="manifest" href="YOUR_CDN_ABSOLUTE_PATH/manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<script src="YOUR_CDN_ABSOLUTE_PATH/flutter.js" defer></script>
</head>
再打個(gè)包上傳到 CDN ,嗯一切都正常了~ 到這里看上去都完美了,但突然想起來不對(duì)啊,我們是區(qū)分開發(fā)/測(cè)試/生產(chǎn)環(huán)境的,相應(yīng)的 CDN 路徑也是不同的。修改 index.html 的方式指定的都是絕對(duì)路徑,不符合我們的需求啊。既然如此我們?cè)俑母摹?/span>
(2)區(qū)分不同環(huán)境配置CDN路徑正常情況下,我們開發(fā)/測(cè)試/生產(chǎn)環(huán)境的 host 會(huì)映射到不同的 CDN 地址上。另外我們?cè)诒镜卣{(diào)試的時(shí)候用的是本地資源,不需要配置 CDN 地址。那么我們的 index.html 修改如下:
<!DOCTYPE html>
<html>
<head>
<base id="href">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="摸魚kik.">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="moyu">
<link id="apple-touch-icon" rel="apple-touch-icon" href="icons/Icon-192.png">
<link id="icon" rel="icon" type="image/png" href="favicon.png" />
<title>moyu</title>
<link id="manifest" rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<script id="flutter_js" defer></script>
</head>
<body>
<script>
var YOUR_CDN_HOST = ""; //默認(rèn)是本地調(diào)試,不需要配置cdn地址
if (document.location.origin == YOUR_DEV_HOST) {
YOUR_CDN_HOST = YOUR_DEV_CDN_HOST;
} else if (document.location.origin == YOUR_TEST_HOST) {
YOUR_CDN_HOST = YOUR_TEST_CDN_HOST;
} else if (document.location.origin == YOUR_PRODUCT_HOST) {
YOUR_CDN_HOST = YOUR_PRODUCT_CDN_HOST;
}
//需要相應(yīng)的element并配置其絕對(duì)路徑
document.getElementById("flutter_js").setAttribute("src", `${YOUR_CDN_HOST}flutter.js`);
document.getElementById("manifest").href = `${YOUR_CDN_HOST}manifest.json`;
document.getElementById("icon").href = `${YOUR_CDN_HOST}favicon.png`;
document.getElementById("apple-touch-icon").href = `${YOUR_CDN_HOST}icons/Icon-192.png`;
window.addEventListener('load', function (ev) {
// Download main.dart.js
if (YOUR_CDN_HOST == "") {
//本地調(diào)試
_flutter.loader.loadEntrypoint().then(function (engineInitializer) {
return engineInitializer.initializeEngine();
}).then(function (appRunner) {
return appRunner.runApp();
});
} else {
//部署后
_flutter.loader.loadEntrypoint({
entrypointUrl: `${YOUR_CDN_HOST}main.dart.js`,
}).then(function (engineInitializer) {
return engineInitializer.initializeEngine({
assetBase: `${YOUR_CDN_HOST}`
});
}).then(function (appRunner) {
return appRunner.runApp();
});
}
});
</script>
</body>
</html>
-
1.首先根據(jù)當(dāng)前域名 document.location.origin 的不同,區(qū)分不同環(huán)境下的 CDN 地址: YOUR_CDN_HOST 。默認(rèn)是是空,即本地調(diào)試情況,不需要配置 CDN 地址。
-
2.為 flutter.js , icons/Icon-192.png , favicon.png , manifest.json 指定 id ,并通過 document.getElementById() 方法找到相應(yīng)元素,為他們配置 CDN 的絕對(duì)路徑。
-
3.如上一章節(jié)所示,配置 entrypointUrl 與 assetBase 。
一切真正的完美了~到此為止,如果打包部署我們就講完了。下一章節(jié)我要說明一下在開發(fā)過程中,遇到的一些意想不到的坑與相應(yīng)的處理方式。
06
Flutter Web避坑指南
由于在實(shí)際項(xiàng)目中,我們是將一個(gè)現(xiàn)成的 Flutter 應(yīng)用打包成 web 版。原先的 App 已經(jīng)支持了 Android , iOS , Mac , Windows 這四個(gè)平臺(tái)。這一章節(jié)將針對(duì)實(shí)際項(xiàng)目中遇到的一些問題進(jìn)行說明。包含如下幾個(gè)問題:
-
1. Dart 中 int 和 JS 中 Number 的轉(zhuǎn)換問題。
-
2.導(dǎo)入特定平臺(tái)依賴項(xiàng)。
-
3.路由問題。
-
4. iPhone 手機(jī) Safari 瀏覽器的側(cè)滑返回問題。
-
5. lottie 問題。
-
6.跨域問題。
接下來我會(huì)針對(duì)這幾個(gè)問題一一進(jìn)行說明。
1、Dart中int和JS中Number的轉(zhuǎn)換由于我們的項(xiàng)目是將一個(gè)線上的 Flutter 的 App 項(xiàng)目直接打包成 web 版,在運(yùn)行的時(shí)候發(fā)現(xiàn),我們發(fā)送的請(qǐng)求時(shí)常返回錯(cuò)誤的數(shù)據(jù),比如說:
我們請(qǐng)求了一個(gè) feed 列表,然后點(diǎn)擊某一個(gè) item 進(jìn)入詳情頁。
這時(shí)候列表都能正常的展示,但進(jìn)入詳情頁服務(wù)端會(huì)報(bào)錯(cuò):
不存在這個(gè) feed。
通過跟服務(wù)端同學(xué)的溝通發(fā)現(xiàn),出錯(cuò)的原因是在進(jìn)入詳情頁請(qǐng)求 feed 詳情時(shí)帶的 id 錯(cuò)了。 這怎么會(huì)??? id 都是列表接口給的, web 端也不會(huì)做任何處理進(jìn)詳情頁直接帶過去,而且線上 App 都是好好的也沒有 bug 啊。 經(jīng)過排查發(fā)現(xiàn), id 定義的是 int 類型,在 Dart 中,只有 int 和 double 這兩種表達(dá)數(shù)字的數(shù)據(jù)類型,其中 int 的取值范圍是 -2^63 ~ 2^63 - 1 ,可以同等于 Java 中的 Long 。 在打包成 web 版式, Dart 中的 int 會(huì)被編譯成 JS 中的 Number ,問題就出在這兒了。 Number 的取值范圍是 -2^53 ~ 2^53 - 1 。很不幸,我們模型中一些的 id 的取值范圍大于 2^53 - 1 ,從而轉(zhuǎn)換成 JS 的 Number 后出錯(cuò)了。 原因找出來了,解決方法也顯而易見了: 這種可能會(huì)超出 JS 取值范圍的字段,需要改成 String 類型 。 修改完后,這個(gè)問題順利解決。
2、導(dǎo)入特定平臺(tái)依賴項(xiàng)在使用 Flutter 進(jìn)行 web 端開發(fā)的時(shí)候,我們會(huì)經(jīng)常使用 dart:html 這個(gè)庫來實(shí)現(xiàn)一些功能。在僅僅打包 web 端時(shí)沒問題,但由于我們的項(xiàng)目是跨平臺(tái)的,打包 App 時(shí)就會(huì)出現(xiàn)以下問題:
是因?yàn)?nbsp; dart:html 這個(gè)庫只在 web 環(huán)境下能找得到,而編譯 App 時(shí)并沒有這個(gè)包,那也就意味著我們只能在 web 打包時(shí)使用 dart:html 這個(gè)庫。解決方法如下:
import 'dart:html' if (dart.library.io) 'io_platform.dart' as platform;
在 import 的時(shí)候需要區(qū)分平臺(tái), dart.library.io 意味著是在非 web 環(huán)境下( dart:io 不支持 web )。所以在非 web 環(huán)境下我們 import 的是 io_platform.dart 這個(gè)文件。這時(shí)候我們有個(gè)疑問,非 web 環(huán)境下不引入 dart:html 不就好了么?為什么要引入另一個(gè)文件呢?原因是因?yàn)榫幾g的時(shí)候還是會(huì)找相應(yīng)的方法,我們沒有引入任何庫,導(dǎo)致相應(yīng)的代碼編譯不過,所以我們自己創(chuàng)建了一個(gè) io_platform.dart 文件,去實(shí)現(xiàn)相應(yīng)的接口。當(dāng)然由于這些方法不會(huì)被調(diào)用到,其實(shí)只是個(gè)空實(shí)現(xiàn)。 比方說我們現(xiàn)在用到了 dart:html 以下的方法和變量:
platform.window.navigator.userAgent; //navigator.userAgent
platform.window.location.origin; //location.origin
platform.window.location.href; //location.href
platform.window.open(url, ""); //open(String, String)
于是我們的 io_platform.dart 是這么實(shí)現(xiàn)的:
IoPlatformWindow get window => IoPlatformWindow();
class IoPlatformWindow {
IoNavigator navigator = IoNavigator();
IoLocation location = IoLocation();
open(String url, String name) {}
}
class IoNavigator {
String userAgent = "";
}
class IoLocation {
String origin = "";
String href = "";
}
實(shí)際上只是為了解決編譯的問題。如果大家有更好的方式解決這個(gè)問題請(qǐng)給我留言哈。接下來我們?cè)賮砜绰酚蓡栴}。
3、路由問題我們知道常規(guī) web 端開發(fā)時(shí),進(jìn)行頁面跳轉(zhuǎn)傳參是靠在 url 上拼參數(shù),如:
YOUR_HOST_NAME/PATH?feedId=123
但顯然 Flutter 并不是這么傳參的。比方說我們進(jìn)入一個(gè)詳情頁,那么它的路由就是: YOUR_HOST_NAME/#detailPage ,而參數(shù)并不可見。這樣的話在我們刷新頁面的時(shí)候,也拿不到參數(shù)自然會(huì)出現(xiàn)問題。 解決方法呢,比如說可以在 LocalStorage 里記錄參數(shù)信息,然后做一個(gè)工具類去記錄路由棧。但這也有問題,因?yàn)槲覀兛梢詮?fù)制任意鏈接分享給別人,那么別人打開的時(shí)候本地沒有記錄自然也就無法正常打開頁面。這種情況下甚至無法引導(dǎo)用戶去首頁。既然如此,那我們干脆處理成用戶在刷新的時(shí)候,重新將網(wǎng)頁指定到首頁 url 。
void register() {
if (platform.window.location.href !=
platform.window.location.origin + "/" &&
platform.window.location.href !=
platform.window.location.origin + "/#/") {
platform.window.location.href = platform.window.location.origin + "/";
}
}
在發(fā)現(xiàn)網(wǎng)頁 url 不是首頁的情況下,強(qiáng)制將 href 處理到首頁。 然后在 runApp(const MyApp()); 的 MyApp 控件的 initState() 方法中調(diào)用 register() 。 到這呢我們起碼解決了分享出去一個(gè)鏈接,完全打不開頁面的尷尬,好歹讓用戶看到首頁了。接著我們想想辦法帶點(diǎn)兒參數(shù)進(jìn)去。 在此呢我們可以用 window.history.replaceState() 為我們的 url 添加參數(shù),且不會(huì)留下歷史記錄。這正是我們想要的,代碼如下:
platform.window.history.replaceState({}, "", newUrl);
那么接下來我們應(yīng)該為 url 添加什么參數(shù)呢?由于 web 版是 App 代碼直接改造的,在首頁會(huì)有很多初始化的處理,直接跳轉(zhuǎn)至某些路由頁面,即使帶了參數(shù)頁面也無法正常展示。這時(shí)候我想到了我們?cè)?nbsp; App 開發(fā)的時(shí)候常用的跳轉(zhuǎn)協(xié)議:
在進(jìn)行 App 開發(fā)的時(shí)候,我們會(huì)用去 scheme 處理一些的 Push 跳轉(zhuǎn)或網(wǎng)頁的跳轉(zhuǎn),封裝成跳轉(zhuǎn)協(xié)議。
而在 web 我們可以添加跳轉(zhuǎn)協(xié)議需要的參數(shù),經(jīng)過解析后封裝成我們既有的跳轉(zhuǎn)協(xié)議,低成本的完成頁面跳轉(zhuǎn)和加載仿佛是可行的。我們的跳轉(zhuǎn)協(xié)議結(jié)構(gòu)如下:
OUR_SCHEME/PATH?param1=1¶m2=2
這么看就更簡(jiǎn)單了,我們將 url 拼上 ?param1=1¶m2=2 ,在處理的時(shí)候,將 ? 前的內(nèi)容替換為 OUR_SCHEME/PATH 就直接將 url 替換成我們的跳轉(zhuǎn)協(xié)議了。然后再調(diào)我們統(tǒng)一的協(xié)議處理方法即可。經(jīng)過驗(yàn)證,效果如我們所替代的,完美的實(shí)現(xiàn)了刷新/分享鏈接的處理。
4、iPhone手機(jī)Safari瀏覽器的側(cè)滑返回問題在使用 iPhone 真機(jī)進(jìn)行調(diào)試的時(shí)候,我們發(fā)現(xiàn)手勢(shì)在真機(jī)設(shè)備的邊緣進(jìn)行側(cè)滑返回的時(shí)候,會(huì)導(dǎo)致棧底的根頁面也返回,并且導(dǎo)致整個(gè) Flutter 應(yīng)用重新加載,體驗(yàn)非常不好,如下圖所示:
目前這個(gè)問題官方?jīng)]有很好的解決方法,我們只能通過對(duì) flt-glass-pane 標(biāo)簽( Flutter 根布局對(duì)應(yīng)的標(biāo)簽)增加 touchstart 監(jiān)聽,對(duì)邊緣處手勢(shì)進(jìn)行忽略。在 index.html 中增加如下代碼:
_flutter.loader.loadEntrypoint({
entrypointUrl: `${MOYU_HOST}main.dart.js`,
}).then(function (engineInitializer) {
return engineInitializer.initializeEngine({
assetBase: `${MOYU_HOST}`
});
}).then(function (appRunner) {
return appRunner.runApp();
}).then(function (_) {
boundaryCheck();
});
function boundaryCheck() {
const flutterRoot = document
.getElementsByTagName("flt-glass-pane")
.item(0);
flutterRoot.addEventListener("touchstart", (e) => {
var pageX = e.targetTouches[0].pageX;
if (pageX > 24 && pageX < window.innerWidth - 24) return;
e.preventDefault();
});
}
在 main.js.dart 加載, Flutter 引擎初始化完成后,調(diào)用 boundaryCheck() 方法進(jìn)行手勢(shì)位置邊緣檢測(cè),如果在邊緣處則調(diào)用 preventDefault() 方法,避免根部頁面返回并重新加載。
5、lottie問題由于我們的業(yè)務(wù)中使用了大量的 lottie 動(dòng)畫,在各端,包括 PC 端的瀏覽器上運(yùn)行都沒有問題。但在移動(dòng)端真機(jī)上,部分 lottie 動(dòng)畫會(huì)導(dǎo)致崩潰。查其原因是因?yàn)樵谝苿?dòng)端真機(jī)上不支持 BlendMode.clear 模式,部分 lottie 動(dòng)畫由于支持了 BlendMode.clear 模式,導(dǎo)致出現(xiàn)問題。這個(gè)需要和 UI 同學(xué)進(jìn)行溝通,更新/替換動(dòng)畫等。
6、跨域問題跨域問題需要和服務(wù)端同學(xué)共同解決,都是現(xiàn)成的方案。當(dāng)然如果是在本地調(diào)試階段(也僅限于本地調(diào)試的情況),你也可以通過以下步驟解決跨域問題:
-
1.前往 flutter\bin\cache 文件夾,刪除 flutter_tools.stamp 文件。
-
2.前往 flutter\packages\flutter_tools\lib\src\web ,打開 chrome.dart 文件。
-
3.找到 '--disable-extensions' 這部分,在最下面添加 '--disable-web-security' ,重新 build 即可。
07
總結(jié)
我們利用 Flutter 完成了一個(gè) web 項(xiàng)目的開發(fā),打包部署到 CDN 上,并最終上線。 FlutterWeb 雖然已經(jīng)穩(wěn)定了一段時(shí)間了,但是除非是有明確的跨端需求,并不推薦大家將它用在需要長(zhǎng)期迭代,大而重的項(xiàng)目中。不過對(duì)于我們客戶端開發(fā)來說,在擁有了 Flutter 的技能后,除去我們所熟悉的 Android 和 iOS 跨端開發(fā),完全可以拓展自己的業(yè)務(wù)范疇,分?jǐn)傄恍┖线m的 web 端項(xiàng)目進(jìn)行開發(fā),為自己的團(tuán)隊(duì)增加更多的業(yè)務(wù)可能。 另外雖然 Flutter Web 確實(shí)還沒那么完美,之前很多文章分享的延遲組件分包以減小 main.dart.js 大小的方式貌似也不可用了(官網(wǎng)明確說明是給 Android 的 AAB 來使用的)。但有總比沒有強(qiáng),將一個(gè)現(xiàn)成的 App 打包成 web 版成本很低。畢竟重新開發(fā)一個(gè) web 版的 App 功能工作量也是巨大的。目前繼續(xù)等著 Flutter 的更新,看看未來會(huì)不會(huì)有更好的支持。
