<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 29886字,需瀏覽 60分鐘

           ·

          2024-04-12 02:58

          182d1b8cea8b0f1341e8107543dbc438.webp

          e9306f41e63266a8d5d2e1e65af04542.webp

          本文字?jǐn)?shù): 7743

          預(yù)計(jì)閱讀時(shí)間: 60 分鐘

          e179ebfcfd932ea2610bc80de4c92d7e.webp

          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)

          1、創(chuàng)建與運(yùn)行

          我們使用 Android Studio 作為IDE,以 Flutter 3.10.5 版本為基礎(chǔ)創(chuàng)建一個(gè) Flutter Web 工程。 創(chuàng)建一個(gè) New Flutter Project ,在選擇 Platforms 的時(shí)候只勾選 Web ,然后直接 Create

          bb9de9312e3c6177e35beec6686c9064.webp

          然后我們發(fā)現(xiàn)在工程目錄里多了個(gè)  web   的文件夾:

          51a7f1ad06ed3be724362a8ef03f506b.webp

          如果你是為現(xiàn)有的  Flutter 工程添加  Web 的支持,只需在項(xiàng)目根目錄運(yùn)行如下命令即可:

          flutter create --platforms=web .

          項(xiàng)目創(chuàng)建好了,如果想要  run   起來只需選擇  chrome   瀏覽器,點(diǎn)擊  run   就行了:

          89555f57415de49cada880afacd0d8c1.webp

          然后我們就可以在瀏覽器看到運(yùn)行結(jié)果了,當(dāng)然我們也可以打開開發(fā)者模式方便查看與調(diào)試:

          510bd2c34d2b637b22a37e0d1517ee40.webp

          這部分跑通后,非常恭喜你可以愉快的用  Flutter   開發(fā)網(wǎng)頁了,接下來我們實(shí)現(xiàn)一個(gè)業(yè)務(wù)需求:做一個(gè)網(wǎng)頁搜索功能。

          52f485008cc5059e6b5a1b6fbd806613.webp

          業(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   都是沒問題的,也可以看到源碼,可以抓包:

          8f8cdc722b72bca4c3258f361d745660.webp ba32bc1daa47844faaa7483aa274e844.webp 1a744df97b52a82b7b5f2373b544114f.webp

          當(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)試
          918bb692d4f28939756420858ecfd3c8.webp

          我們依舊可以用  PC   上的瀏覽器,紅色箭頭指向的位置可以切換至移動(dòng)端模擬器設(shè)備,可以選擇機(jī)型。但更多的時(shí)候,我們希望可以真機(jī)調(diào)試。熟悉  vue   框架的同學(xué)都知道,在本地調(diào)試的時(shí)候,會(huì)給出兩個(gè)地址,如下圖所示:

          5a508247219ca32cb75c867437259b89.webp

          我們可以在手機(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)的地址,如下圖所示:

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

            351b27c086978af9f6a2ea0ed6001f74.webp

          如何進(jìn)行調(diào)試我們已經(jīng)清楚了,假設(shè)我們已經(jīng)開發(fā)完成了,如何打包部署上線呢?

          05

          打包部署上線

          1、打包

          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ò)信息如下:

          3a5867295d83d1ecc45cdc871245161c.webp

          這是個(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   文件夾。包含了以下文件:

          f8805cf28f084fe202b432a37a376ac7.webp

          編譯產(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)物,如下圖所示:

          3df107cf1b56b1cbbb9cd3fa0fe354a7.webp

          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ù)可傳:

          88055fc09f3bd219fe023c471a2d10f5.webp 4556e67231ce5f3e34cea5b2431edb4f.webp


          其中  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)以下問題:

          cdc6147f18cb3891263bf3afde167c99.webp

          是因?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&param2=2

          這么看就更簡(jiǎn)單了,我們將  url   拼上  ?param1=1&param2=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)非常不好,如下圖所示:

          77da971108a7b650de29642c48a17500.webp

          目前這個(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ì)有更好的支持。

          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91探花秘 在线播放 | 日本亚洲免费视频 | 夜夜骚av一区二区三区 | 亚洲豆花视频 | 操逼内射 |