初識(shí)Flutter web

級(jí)別:★☆☆☆☆
標(biāo)簽:「Flutter web」「Dart Server」「blocked by CORS Policy」「跨域」
作者: WYW
審校: QiShare團(tuán)隊(duì)

前言 筆者最近了解了Flutter web相關(guān)的內(nèi)容,本文會(huì)分享創(chuàng)建Flutter web項(xiàng)目、Flutter web項(xiàng)目預(yù)覽,F(xiàn)lutter web項(xiàng)目和Flutter mobile(Flutter Android/iOS)項(xiàng)目的差別、搭建簡(jiǎn)易Dart服務(wù)器(解決跨域問(wèn)題)、上線Flutter web項(xiàng)目相關(guān)內(nèi)容。

一、創(chuàng)建Flutter web 項(xiàng)目

準(zhǔn)備Flutter web 環(huán)境

更新本地環(huán)境為 beta channel最新版。(dev channel 也可以)

flutter channel beta

flutter upgrade

Flutter有如下4個(gè)channel:

flutter channel
Flutter channels:
  beta
* dev
  master
  stable

Flutter 官方建議使用 stable 的channel。

master 是當(dāng)前最新的channel;

dev 是當(dāng)前最新的充分測(cè)試后的channel;

beta是每個(gè)月Flutter官方調(diào)整選出來(lái)的最好的dev的channel,并提升為beta channel;

stable是Flutter 認(rèn)為是當(dāng)前最穩(wěn)定的channel。

穩(wěn)定性而言:master < dev < beta < stable 。更多內(nèi)容可查看:Flutter build release channels

開(kāi)啟項(xiàng)目支持Flutter web

flutter config --enable-web

如果想在當(dāng)前已有項(xiàng)目Flutter mobile項(xiàng)目的基礎(chǔ)上,添加Flutter web支持,可 cd 到 Flutter mobile 項(xiàng)目目錄下,添加Flutter web支持。

新建Flutter web項(xiàng)目

如果之前沒(méi)有創(chuàng)建過(guò)Flutter 項(xiàng)目,新建一個(gè)Flutter web項(xiàng)目可以使用如下命令。

flutter create 項(xiàng)目名(小寫(xiě)) 如:flutter create qi_flutter_web_demo

現(xiàn)有項(xiàng)目生成Flutter web 相關(guān)文件

如果之前創(chuàng)建過(guò)Flutter 項(xiàng)目,想現(xiàn)有項(xiàng)目生成web文件夾及index.html等文件可使用如下命令。

flutter create .

Flutter web 新增index.html 等

運(yùn)行項(xiàng)目命令:flutter run -d chrome

遇到問(wèn)題:運(yùn)行失敗

運(yùn)行失敗報(bào)錯(cuò)如下:

wangyongwangdeiMac:qi_flutter_page wangyongwang$ flutter run -d chrome

Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust

this source!

Downloading Web SDK... 1.1s

Launching lib/main.dart on Chrome in debug mode...

Error compiling dartdevc module:qi_flutter_page|lib/main_web_entrypoint.ddc.js

packages/qi_flutter_page/main_web_entrypoint.dart:9:18: Error: Too few positional arguments:

1 required, 0 given.

entrypoint.main();

       ^                                                             

AssetNotFoundException: qi_flutter_page|lib/main_web_entrypoint.ddc.js

Failed after 23.3s

Building application for the web... 33.5s

Failed to build application for the Web.

猜測(cè)原因:訪問(wèn)網(wǎng)址https://storage.flutter-io.cn.不可達(dá)

起初,筆者猜測(cè)原因是這個(gè)網(wǎng)址https://storage.flutter-io.cn.訪問(wèn)不可達(dá);不過(guò)試過(guò)運(yùn)行新創(chuàng)建的Flutter web項(xiàng)目,發(fā)現(xiàn)新建的Flutter web項(xiàng)目可以正常運(yùn)行,可以排除問(wèn)題不在于網(wǎng)址https://storage.flutter-io.cn.訪問(wèn)不可達(dá)。

Document not found

繼續(xù)看這段報(bào)錯(cuò),可以發(fā)現(xiàn)Flutter web 項(xiàng)目的main 方法中不能有參數(shù)。

packages/qi_flutter_page/main_web_entrypoint.dart:9:18: Error: Too few
positional arguments: 1 required, 0 given.
entrypoint.main();
^

AssetNotFoundException: qi_flutter_page|lib/main_web_entrypoint.ddc.js
Failed after 22.9s

問(wèn)題在于main方法中參數(shù)

運(yùn)行Flutter web 項(xiàng)目的時(shí)候,main方法中不能有參數(shù)。

void main(List<String> args) {

}

// 刪除main方法名中的參數(shù)后,可以正常運(yùn)行。

void main() {

}

筆者以之前寫(xiě)的項(xiàng)目qi_flutter_page為例:運(yùn)行起來(lái)的效果如下:

Flutter web 項(xiàng)目預(yù)覽
Flutter web 項(xiàng)目預(yù)覽

上周和同事CH聊天學(xué)到的內(nèi)容:Flutter web項(xiàng)目顯示的網(wǎng)頁(yè)的特點(diǎn):

顯示網(wǎng)頁(yè)源代碼的時(shí)候,可以網(wǎng)頁(yè)發(fā)現(xiàn)顯示的內(nèi)容是html的body 中嵌套的main.dart.js。

顯示頁(yè)面源文件
頁(yè)面源文件

二、Flutter web 項(xiàng)目預(yù)覽

運(yùn)行Flutter web項(xiàng)目 默認(rèn)會(huì)在Chrome瀏覽器中顯示,不過(guò)在本機(jī)的Safari 瀏覽器中及模擬器中的瀏覽器中輸入相應(yīng)的網(wǎng)址,也可以顯示相應(yīng)的視圖。

Flutter web 項(xiàng)目預(yù)覽

三、Flutter web項(xiàng)目 與 Flutter mobile 項(xiàng)目的不同

筆者在把現(xiàn)有Flutter mobile項(xiàng)目,直接支持Flutter web 的過(guò)程中遇到了網(wǎng)絡(luò)請(qǐng)求報(bào)異常的問(wèn)題,另外簡(jiǎn)單測(cè)試了2個(gè)三方庫(kù)的在Flutter web項(xiàng)目中的體現(xiàn)。

HttpClient() 不能用于Flutter web 項(xiàng)目

try {
  HttpClient client = HttpClient();
} catch (e) {
  print('捕獲異常:$e');
}

捕獲異常:NoSuchMethodError: invalid member on null: 'indexOf'

Flutter web 項(xiàng)目的網(wǎng)絡(luò)請(qǐng)求可以使用html.httpRequest。

import 'dart:html' as html;

html.HttpRequest.request(url).then((responseValue) {
     
 });

三方庫(kù)支持情況

筆者這里舉2個(gè)自己使用過(guò)的2個(gè)三方庫(kù),shared_preferences、url_launcher均支持 Flutter web 項(xiàng)目。

下列代碼對(duì)于Flutter web 項(xiàng)目中仍然支持打開(kāi)加載url的窗口。

String soUrl = 'https://www.so.com';
if (await canLaunch(soUrl)) {
  await launch(soUrl);
}

由如下代碼及相應(yīng)結(jié)果可知,shared_preferences 也支持 Flutter web 項(xiàng)目。

void _incrementCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int counter = (prefs.getInt('counter') ?? 0) + 1;
    print('Pressed $counter times.');
    await prefs.setInt('counter', counter);
  }

ListTile2
Pressed 1 times.
ListTile2
Pressed 2 times.
ListTile2
Pressed 3 times.
ListTile2
Pressed 4 times.
ListTile2
Pressed 5 times.

三方庫(kù)一般會(huì)注明支持的平臺(tái)(Android、iOS或Web)。
url_launcher 5.4.1支持 Flutter web 項(xiàng)目。
shared_preferences 支持 Flutter web 項(xiàng)目。
sqflite自己注明了支持Android和iOS,是否支持Flutter web沒(méi)有做說(shuō)明。

如下圖所示:

url_launcher
shared_preferences
sqflite

四、簡(jiǎn)易Dart服務(wù)器

使用如下代碼,可以本地啟動(dòng)一個(gè)Dart服務(wù)。

main() async {
  var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 9988);
  await for (var request in server) {
    request.response
      ..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
      ..write('Hello Dart! 你好Dart')
      ..close();
  }
 }

瀏覽器中直接請(qǐng)求http://127.0.0.1:9988 示意圖如下:

簡(jiǎn)易Dart 服務(wù)器示意

筆者在Flutter web項(xiàng)目中請(qǐng)求,http://127.0.0.1:9988的時(shí)候,遇到了跨域問(wèn)題,下邊分享下相關(guān)問(wèn)題及處理方式。

跨域問(wèn)題

跨域問(wèn)題描述

跨域資源共享(CORS) 是一種機(jī)制,它使用額外的 HTTP 頭來(lái)告訴瀏覽器 讓運(yùn)行在一個(gè) origin (domain) 上的Web應(yīng)用被準(zhǔn)許訪問(wèn)來(lái)自不同源服務(wù)器上的指定的資源。當(dāng)一個(gè)資源從與該資源本身所在的服務(wù)器不同的域、協(xié)議或端口請(qǐng)求一個(gè)資源時(shí),資源會(huì)發(fā)起一個(gè)跨域 HTTP 請(qǐng)求。

比如,站點(diǎn) http://domain-a.com 的某 HTML 頁(yè)面通過(guò) 的 src 請(qǐng)求 http://domain-b.com/image.jpg。網(wǎng)絡(luò)上的許多頁(yè)面都會(huì)加載來(lái)自不同域的CSS樣式表,圖像和腳本等資源。

出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨源HTTP請(qǐng)求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 這意味著使用這些API的Web應(yīng)用程序只能從加載應(yīng)用程序的同一個(gè)域請(qǐng)求HTTP資源,除非響應(yīng)報(bào)文包含了正確CORS響應(yīng)頭。 引自HTTP訪問(wèn)控制(CORS)

舉2個(gè)例子比如我們自身當(dāng)前域名為abc.com, 訪問(wèn)def.com 會(huì)出現(xiàn)跨域的問(wèn)題。
比如我們自身當(dāng)前域名為abc.com端口號(hào)為1234(abc.com:1234),那么訪問(wèn)abc.com:5678也會(huì)出現(xiàn)跨域問(wèn)題。

筆者使用Flutter web項(xiàng)目 請(qǐng)求服務(wù)端資源的時(shí)候遇到了跨域問(wèn)題。

http://localhost:55355/#/ 中的內(nèi)容訪問(wèn)http://127.0.0.1:9988

Flutter web項(xiàng)目跨域現(xiàn)象圖現(xiàn)象圖如下:

Flutter web項(xiàng)目跨域問(wèn)題

出現(xiàn)當(dāng)前跨域問(wèn)題的原因是端口號(hào)不同,訪問(wèn)Flutter web 的url 和 請(qǐng)求服務(wù)端資源的url的 端口號(hào) 不同。

請(qǐng)求的響應(yīng)頭中設(shè)置可跨域的origin,解決跨域問(wèn)題

設(shè)置跨域的url 有2種設(shè)置方式:

  • 1.設(shè)置一個(gè)或多個(gè)url;
    • 如:request.response
      ..headers
      .add('Access-Control-Allow-Origin', request.headers['origin'])
  • 2.設(shè)置跨域的值為*;
    •   ..headers.add('Access-Control-Allow-Origin', '*')
      

注意:如果在本地測(cè)試使用,可以使用第二種方式,直接了當(dāng)。但是一般線上的話最好使用第一種方式設(shè)置是否可以跨域請(qǐng)求。因?yàn)樵O(shè)置是否可以跨域,算是服務(wù)器在響應(yīng)瀏覽器請(qǐng)求數(shù)據(jù)時(shí)的一種保護(hù)策略。

其中重點(diǎn)是在響應(yīng)頭中添加可以跨域的請(qǐng)求域。

..headers.add('Access-Control-Allow-Origin', 'http://localhost:55355')

如'Access-Control-Allow-Origin'可以指定特定的url,使url能夠跨域請(qǐng)求。

如有需要指定允許多個(gè)url進(jìn)行跨域請(qǐng)求??梢愿鶕?jù)請(qǐng)求的origin的值,判斷是否要做跨域響應(yīng)頭的處理。

如:如下代碼設(shè)置了當(dāng)請(qǐng)求的origin 為http://localhost:63062http://localhost:55355 的時(shí)候,會(huì)添加跨域處理的響應(yīng)頭。

main() async {
  var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 9988);

  var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 9988);
  await for (var request in server) {
    var accessControlAllowOrigin = [
      'http://localhost:63062',
      'http://localhost:55355'
    ];
    if (request.headers['origin'] != null) {
      for (String tempAllowOrigin in accessControlAllowOrigin) {
        if (request.headers['origin'].first.contains(tempAllowOrigin)) {
          request.response
            ..headers
                .add('Access-Control-Allow-Origin', request.headers['origin'])
            // ..headers.add('Access-Control-Allow-Origin', '*')
            ..headers.contentType =
                ContentType('text', 'plain', charset: 'utf-8')
            ..write('Hello Dart! 你好Dart 跨域')
            ..close();
        }
      }
    } else {
      request.response
        ..headers.contentType = ContentType('text', 'plain', charset: 'utf-8')
        ..write('Hello Dart! 你好Dart 不需要跨域')
        ..close();
    }
  }
 }
解決跨域問(wèn)題

筆者在上邊說(shuō)明了處理運(yùn)行Flutter web項(xiàng)目的時(shí)候,處理本地服務(wù)端接口和Flutter web項(xiàng)目運(yùn)行網(wǎng)址出現(xiàn)跨域問(wèn)題的處理方式。(其實(shí)對(duì)于編譯后的Flutter web項(xiàng)目的產(chǎn)物直接放到自己的服務(wù)端項(xiàng)目的的靜態(tài)文件目錄下的時(shí)候,不會(huì)出現(xiàn)上述問(wèn)題)
有時(shí)候我們的請(qǐng)求內(nèi)容可能就需要跨域去請(qǐng)求數(shù)據(jù),而且對(duì)方如果也不便添加相應(yīng)的跨域響應(yīng)頭。此時(shí),可使用Nginx 做反向代理來(lái)處理跨域問(wèn)題。

Nginx 反向代理解決遠(yuǎn)端跨域問(wèn)題
server {
        listen       9080;
        server_name  localhost;
        
        location ~ /columns/Qtest {
            proxy_pass https://testerhome.com;
        }
    }

經(jīng)上述處理,可以在本地的127.0.0.1:9080/columns/Qtest請(qǐng)求到 Qtest測(cè)試之道https://testerhome.com/columns/Qtest 相應(yīng)數(shù)據(jù)。

本地Flutter web 項(xiàng)目跨域訪問(wèn)TesterHome Qtest測(cè)試之道

Nginx 配置反向代理及rewrite訪問(wèn)路徑可實(shí)現(xiàn)訪問(wèn)遠(yuǎn)端文件不跨域。

        location / {
           proxy_pass https://weekly.75team.com;
        }
        
        location ~ /api/qiwuzhoukanWeb {
            rewrite /api/qiwuzhoukanWeb /;
            proxy_pass https://weekly.75team.com;
        }

經(jīng)上述處理,可以在本地的127.0.0.1:9080/api/qiwuzhoukanWeb請(qǐng)求到 奇舞周刊https://weekly.75team.com 相應(yīng)數(shù)據(jù)。

本地Flutter web 項(xiàng)目跨域訪問(wèn)奇舞周刊

五、Flutter web 項(xiàng)目上線

flutter build web會(huì)在項(xiàng)目的build 目錄中生成相應(yīng)的資源文件及html 和js文件,把相關(guān)文件放置到服務(wù)端靜態(tài)文件目錄下即可。實(shí)現(xiàn)上線Flutter web項(xiàng)目。

Flutter web項(xiàng)目編譯產(chǎn)物

參考學(xué)習(xí)網(wǎng)址


推薦文章:
用AdHoc來(lái)測(cè)試iOS線上推送
Swift 5.1 (9) - 結(jié)構(gòu)體和類(lèi)
Swift 實(shí)現(xiàn)一個(gè)兼容iOS、tvOS、OSX的抽象層
iOS Password AutoFill
iOS 給UILabel添加點(diǎn)擊事件
用SwiftUI給視圖添加動(dòng)畫(huà)
用SwiftUI寫(xiě)一個(gè)簡(jiǎn)單頁(yè)面
Swift 5.1 (8) - 枚舉類(lèi)型
iOS App啟動(dòng)優(yōu)化(三)—— 自己做一個(gè)工具監(jiān)控App的啟動(dòng)耗時(shí)
iOS App啟動(dòng)優(yōu)化(二)—— 使用“Time Profiler”工具監(jiān)控App的啟動(dòng)耗時(shí)
iOS App啟動(dòng)優(yōu)化(一)—— 了解App的啟動(dòng)流程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容