1.前言
首先說明一下,這篇文章是給具備Flutter開發經驗的客戶端同學看的。Flutter
的誕生雖然來自Google
的Chrome
團隊,但大家都知道Flutter
最先支持的平臺是Android
和iOS
,至今最核心的維護平臺依然是Android
和iOS
。由于dart
語言的學習成本不高,Flutter
的響應式UI與ComposeUI
和SwiftUI
都有極大的相似之處,整體的架構思路也更偏向于客戶端的模式,再加上為了實現很多硬件或Native
相關的基礎功能也需要專業的客戶端開發知識,所以Flutter
更多的是被客戶端開發同學認可并使用(在我們的團隊中,Flutter
已經是客戶端開發同學的必備基本技能)。
在此背景下,Flutter
最初并不在web
端上發力。不過由于Flutter
本身就是攜帶了web
的基因,在Flutter2
發布的同時也發布了web
的穩定版。那么它有什么優勢和劣勢呢?
-
優勢:
1. 零學習成本:當你已經掌握了Flutter
開發能力后,哪怕你對html
,css
,JavaScript
和主流的前端框架不那么了解,也不影響你開發web
應用。
2. 跨端能力:可將現有Flutter
移動應用拓展到web
,在多個平臺共享代碼,降低開發成本。 -
劣勢:
1. 兼容性問題:使用html
模式來進行渲染時,應用的大小相對較小但可能會出現兼容性問題。
2. 包體積增加:使用canvaskit
模式來進行渲染時,雖然性能較好,且可以降低不同瀏覽器渲染效果不一致的風險,但會增加包體積。
分析了優勢劣勢后,我們發現如果單純的做個web
端應用,Flutter
并沒有優勢,前端開發同學大概也不會使用Flutter
進行web
開發(確實沒必要,比如包體積增加或有一定的性能損失,還需要學習新語言與開發思路,原生開發不香么),Flutter Web
到底有什么用呢?
帶著這樣的想法,在使用Flutter
后的很長時間都不曾調研過web
端的支持。但隨著業務和內部需求的發展變化,我們有了使用Flutter
進行web
開發的想法。下面我來說一下使用Flutter Web
主要的三個場景。
2.Flutter Web的使用場景
- 1.客戶端團隊內部的web需求:在后疫情時代降本增效的大背景下,我們會更多的使用自研工具。自研工具的使用和結果展示的可視化通常以網頁的形式展現。客戶端同學使用
Flutter Web
進行網頁開發學習成本低,完全可以快速的開發網頁(本人在使用Vue
框架進行web
端開發時感受出客戶端和前端的UI布局思路還是有很大不同的,css
很靈活約束性低,這個與客戶端布局的強約束性差異很大,所以對于客戶端開發來說,使用Flutter
開發網頁應用時更順手。對于全員掌握Flutter
技能的我們團隊來說已經是0成本了)。 - 2.簡單的web端業務需求:
web
端承載了很多活動需求,這些需求的特點是時效性強,功能較簡單,且不需長期維護。但這些需求經常是在某一時間段大量產生的(比如逢年過節的一些活動或榜單),或突然產生的(比如蹭熱點的即時需求)。這些工作的插入有時會導致一些長期迭代的web
端需求需要延期,影響團隊的整體排期。由于這些需求開發難度不大,性能要求不高,不需長期維護(意味著即使團隊里不再有人使用Flutter
或Flutter Web
有一天掛了也沒什么影響),那么就可以讓Flutter
開發同學加入進來,平攤了一部分工作,以此來提升整個團隊的效率。 - 3.客戶端與web端的跨端:隨著
Flutter Web
趨于穩定,用Flutter
實現的App
可以低成本的被打包成web
版了,畢竟對于用戶來說使用瀏覽器打開個網頁比下載個App
成本低多了。這種情況下我們就可以利用Flutter
的跨端優勢,節約很多人力資源,避免去重新開發一套web
端了。
好的既然有了使用場景,我們就好好來走一下Flutter Web
是怎么開發部署上線的流程。
3.Flutter Web工程的創建和業務實現
3.1.創建與運行
我們使用Android Studio
作為IDE,以Flutter 3.10.5
版本為基礎創建一個Flutter Web
工程。
創建一個New Flutter Project
,在選擇Platforms
的時候只勾選Web
,然后直接Create
。
然后我們發現在工程目錄里多了個
web
的文件夾:如果想要
run
起來只需選擇chrome
瀏覽器,點擊run
就行了:然后我們就可以在瀏覽器看到運行結果了,當然我們也可以打開開發者模式方便查看與調試:
這部分跑通后,非常恭喜你可以愉快的用
Flutter
開發網頁了,接下來我們實現一個業務需求:做一個網頁搜索功能。業務功能上的開發實現我就不做贅述了,可以告訴做過
Flutter
開發的同學,沒什么不同,基礎配置/網絡模塊/數據共享/路由等該怎么封裝就怎么封裝,我也不過是直接拿了之前客戶端Flutter
工程相應模塊的代碼,稍作修改而已。UI
上的開發也是該怎么布局怎么布局,業務的開發體驗上和客戶端使用Flutter
沒什么不同。
3.2.調試
跑通后應該如何調試呢?
如果熟悉瀏覽器開發者模式,可直接使用瀏覽器進行調試,打log
或debug
都是沒問題的,也可以看到源碼,可以抓包:
當然客戶端同學可能不熟悉瀏覽器開發者模式,也沒關系,利用
Android Studio
,之前在客戶端寫Flutter
怎么調試,現在寫web
端依舊可以怎么調試。
3.3.window
在web
端開發的時候我們通常會使用window
對象進行一些操作。window
對象代表一個瀏覽器窗口或一個框架。常用的event
監聽,打開網頁等操作都需要window
對象。
Flutter
自帶的dart:html
封裝了window
,我們可以通過它來實現獲取window
的屬性或對window
進行操作,比如:
//打開網頁
window.open("http://www.baidu.com","");
//監聽event
window.addEventListener("mousedown", (event) => {
//do something
});
另外window
也可以幫助我們區分運行環境。
3.4.瀏覽器運行環境區分
客戶端通常需要區分的是Android
和iOS
這兩個不同的運行環境,而web
端是需要通過UA
來區分不同的瀏覽器環境的,不同環境下的UI/邏輯等會有差別。在國內,我們最常需要區分PC
端/移動端/Android
端/iOS
端/微信網頁/微信小程序這幾個。那么我們可以定義一個類,利用window.navigator.userAgent
去區分這些環境:
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;
}
//移動端
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;
}
//微信環境
isWechat() {
return ua.contains("MicroMessenger");
}
//微信小程序環境
isMiniprogram() {
if (ua.contains("micromessenger")) {
//微信環境下
if (ua.contains("miniprogram")) {
//小程序;
return true;
}
}
return false;
}
}
3.5.開發/測試/生產環境區分
同客戶端一樣,web
端也需要區分開發/測試/生產環境。同客戶端的方式一樣,我們還是可以通過配置不同的入口文件來實現環境的區分。如:
- 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()
就可以根據不同的環境做不同的配置了。
3.6.其他常用庫或插件
關于數據共享/網絡/UI/動畫等庫就不做介紹了,因為這些庫和平臺不相關,用各自熟悉的就好,下面是來介紹一下為了實現一些瀏覽器相關功能需要用到的插件。
-
shared_preferences
在客戶端開發的時候,我們知道如果需要對一些數據實現輕量級的本地序列化可以使用shared_preferences
,其實現對應Android
的SharedPreferences
和iOS
的NSUserDefaults
。而在進行web
開發的時候,我們知道如需在本地序列化一些數據的話,可以使用LocalStorage
。其實Flutter
的shared_preferences
插件也是支持web
的,其實現也正是封裝了LocalStorage
。關于shared_preferences
的使用也不做贅述了,已經非常熟悉了。 -
image_picker_for_web
來自于我們熟悉的image_picker
插件。根據瀏覽器的不同,支持或部分支持拍照/拍視頻/讀取圖片/讀取視頻等。 -
js
這個插件是用來使用注解的方式幫助你用Dart
調用JavaScript API
或用JavaScript
調用Dart API
的。
好了,到此為止,我覺著使用Flutter
開發一個常規的web
業務已經不成問題了。接下來我們探討一下如何打包部署上線呢?
4.打包部署上線
4.1.打包
Flutter Web
的打包非常簡單,運行:
flutter build web
即可。但這樣顯然是不夠的,因為我們需要區分環境來打不通的包。
在上一章節我們配置了不同的入口文件,我們以dev
環境為例,其入口文件是main_dev
,那么我們的打包命令就變成了:
flutter build web -t lib/main_dev.dart
這行命令執行完成后,報錯了,報錯信息如下:
這是個圖標數據加載問題,我們加上--no-tree-shake-icons即可。執行命令如下:
flutter build web -t lib/main_dev.dart --no-tree-shake-icons
然后我們就會在項目根目錄的build
文件夾下找到web
這個文件夾,對應的就是web
前端打出來的dist
文件夾。包含了以下文件:
編譯產物有了,那么如何部署呢?
4.2.部署
官方給了如下的部署方式:
https://flutter.cn/docs/deployment/web#deploying-to-the-web
看了官方文檔后我發現,這三種部署方式并不適用于我們的項目。由于CDN
具有提高網站性能和用戶體驗,減輕原始服務器的負載等優勢,目前我們團隊已經搭建了CDN
部署平臺。既然如此,我們的部署方案也需要往這方面靠。
4.2.1.方案1——修改index.html
我先來簡單說明一下FlutterWeb
編譯產物,重點有兩個:flutter.js
和main.dart.js
。其中flutter.js
為入口的js
文件,我們可以打開web
目錄下index.html
:
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<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.">
<!-- iOS meta tags & icons -->
<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">
<!-- Favicon -->
<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>
<!-- This script adds the flutter initialization JS code --></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
業務代碼被編譯成的js
文件。flutter.js
會加載main.dart.js
和其它文件。默認情況下,flutter.js
會加載各個文件,包括資源文件(assets
)都使用的是相對路徑。首先就是通過loadEntrypoint ()
方法加載main.dart.js
這個文件:
//flutter.js
async loadEntrypoint(options) {
const { entrypointUrl = `${baseUri}main.dart.js`, onEntrypointLoaded } =
options || {};
return this._loadEntrypoint(entrypointUrl, onEntrypointLoaded);
}
但我們發現貌似entrypointUrl
是可以自己傳遞的,于是我們從官網文檔里找到了自定義web應用初始化
的鏈接:
https://flutter.cn/docs/platform-integration/web/initialization
有如下的參數可傳:
其中
loadEntrypoint()
方法可以傳遞entrypointUrl
參數來指定main.dart.js
的路徑。而initializeEngine()
方法可以通過傳遞assetBase
參數來指定CDN
資源路徑。這么看來我們完全可以通過將這兩個參數設置為絕對路徑來解決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();
});
}
});
});
我們再打個包,還是會報錯,找不到flutter.js
,還是因為路徑問題。處理方式更簡單了,直接在index.html
里配置成絕對路徑即可。另外我們發現Icon-192.png
,favicon.png
,manifest.json
這幾個文件也是相對路徑,那么我們一次性都改成絕對路徑:
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<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.">
<!-- iOS meta tags & icons -->
<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">
<!-- Favicon -->
<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>
<!-- This script adds the flutter initialization JS code -->
<script src="YOUR_CDN_ABSOLUTE_PATH/flutter.js" defer></script>
</head>
再打個包上傳到CDN
,嗯一切都正常了~
到這里看上去都完美了,但突然想起來不對啊,我們是區分開發/測試/生產環境的,相應的CDN
路徑也是不同的。修改index.html
的方式指定的都是絕對路徑,不符合我們的需求啊。經過調研,找到了另一種方式。
4.2.2方案2——--base-href
重新看index.html
的代碼,發現最上面注釋:
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
大概意思是,我們可以在使用flutter build
打包的使用通過--base-href
參數指定base href
的值。趕緊查看了一下base href
的相關說明:
base標記是一個基鏈接標記,是一個單標記。用以改變文件中所有連結標記的參數內定值。它只能應用于標記<head>與</head>之間。
你網頁上的所有[相對路徑]在鏈接時都將在前面加上基鏈接指向的地址。
既然如此,我們就試試吧~
打包命令更新如下:
flutter build web -t lib/main_dev.dart --base-href YOUER_CDN_PATH --no-tree-shake-icons
需要注意的是YOUER_CDN_PATH
并非絕對路徑,而是去掉host
的路徑。比方你的絕對路徑是:
那么你的YOUER_CDN_PATH
應為:
/your/business/path/dev/
再打個包上傳到CDN
上,一切真正的完美了~
5.總結
我們利用Flutter
完成了一個web
項目的開發,并且部署到CDN
上。另外在web
端還有一些常見的問題,比方說跨域問題,這些需要和服務端同學共同解決,都是現成的方案。當然如果是在本地調試階段(也僅限于本地調試的情況),你也可以通過以下步驟解決跨域問題:
- 1.前往
flutter\bin\cache
文件夾,刪除flutter_tools.stamp
文件。 - 2.前往
flutter\packages\flutter_tools\lib\src\web
,打開chrome.dart
文件。 - 3.找到
'--disable-extensions'
這部分,在最下面添加'--disable-web-security'
,重新build
即可。
FlutterWeb
雖然已經穩定了一段時間了,但是除非是有明確的跨端需求,并不推薦大家將它用在需要長期迭代,大而重的項目中。不過對于我們客戶端開發來說,在擁有了Flutter
的技能后,除去我們所熟悉的Android
和iOS
跨端開發,完全可以拓展自己的業務范疇,分攤一些合適的web
端項目進行開發,為自己的團隊增加更多的業務可能。