初识Flutter web

级别:★☆☆☆☆
标签:「Flutter web」「Dart Server」「blocked by CORS Policy」「跨域」
作者: WYW
审校: QiShare团队

前言 笔者最近了解了Flutter web相关的内容,本文会分享创建Flutter web项目、Flutter web项目预览,Flutter web项目和Flutter mobile(Flutter Android/iOS)项目的差别、搭建简易Dart服务器(解决跨域问题)、上线Flutter web项目相关内容。

一、创建Flutter web 项目

准备Flutter web 环境

更新本地环境为 beta channel最新版。(dev channel 也可以)

flutter channel beta

flutter upgrade

Flutter有如下4个channel:

flutter channel
Flutter channels:
  beta
* dev
  master
  stable

Flutter 官方建议使用 stable 的channel。

master 是当前最新的channel;

dev 是当前最新的充分测试后的channel;

beta是每个月Flutter官方调整选出来的最好的dev的channel,并提升为beta channel;

stable是Flutter 认为是当前最稳定的channel。

稳定性而言:master < dev < beta < stable 。更多内容可查看:Flutter build release channels

开启项目支持Flutter web

flutter config --enable-web

如果想在当前已有项目Flutter mobile项目的基础上,添加Flutter web支持,可 cd 到 Flutter mobile 项目目录下,添加Flutter web支持。

新建Flutter web项目

如果之前没有创建过Flutter 项目,新建一个Flutter web项目可以使用如下命令。

flutter create 项目名(小写) 如:flutter create qi_flutter_web_demo

现有项目生成Flutter web 相关文件

如果之前创建过Flutter 项目,想现有项目生成web文件夹及index.html等文件可使用如下命令。

flutter create .

Flutter web 新增index.html 等

运行项目命令:flutter run -d chrome

遇到问题:运行失败

运行失败报错如下:

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.

猜测原因:访问网址https://storage.flutter-io.cn.不可达

起初,笔者猜测原因是这个网址https://storage.flutter-io.cn.访问不可达;不过试过运行新创建的Flutter web项目,发现新建的Flutter web项目可以正常运行,可以排除问题不在于网址https://storage.flutter-io.cn.访问不可达。

Document not found

继续看这段报错,可以发现Flutter web 项目的main 方法中不能有参数。

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

问题在于main方法中参数

运行Flutter web 项目的时候,main方法中不能有参数。

void main(List<String> args) {

}

// 删除main方法名中的参数后,可以正常运行。

void main() {

}

笔者以之前写的项目qi_flutter_page为例:运行起来的效果如下:

Flutter web 项目预览
Flutter web 项目预览

上周和同事CH聊天学到的内容:Flutter web项目显示的网页的特点:

显示网页源代码的时候,可以网页发现显示的内容是html的body 中嵌套的main.dart.js。

显示页面源文件
页面源文件

二、Flutter web 项目预览

运行Flutter web项目 默认会在Chrome浏览器中显示,不过在本机的Safari 浏览器中及模拟器中的浏览器中输入相应的网址,也可以显示相应的视图。

Flutter web 项目预览

三、Flutter web项目 与 Flutter mobile 项目的不同

笔者在把现有Flutter mobile项目,直接支持Flutter web 的过程中遇到了网络请求报异常的问题,另外简单测试了2个三方库的在Flutter web项目中的体现。

HttpClient() 不能用于Flutter web 项目

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

捕获异常:NoSuchMethodError: invalid member on null: 'indexOf'

Flutter web 项目的网络请求可以使用html.httpRequest。

import 'dart:html' as html;

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

三方库支持情况

笔者这里举2个自己使用过的2个三方库,shared_preferences、url_launcher均支持 Flutter web 项目。

下列代码对于Flutter web 项目中仍然支持打开加载url的窗口。

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

由如下代码及相应结果可知,shared_preferences 也支持 Flutter web 项目。

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.

三方库一般会注明支持的平台(Android、iOS或Web)。
url_launcher 5.4.1支持 Flutter web 项目。
shared_preferences 支持 Flutter web 项目。
sqflite自己注明了支持Android和iOS,是否支持Flutter web没有做说明。

如下图所示:

url_launcher
shared_preferences
sqflite

四、简易Dart服务器

使用如下代码,可以本地启动一个Dart服务。

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();
  }
 }

浏览器中直接请求http://127.0.0.1:9988 示意图如下:

简易Dart 服务器示意

笔者在Flutter web项目中请求,http://127.0.0.1:9988的时候,遇到了跨域问题,下边分享下相关问题及处理方式。

跨域问题

跨域问题描述

跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求

比如,站点 http://domain-a.com 的某 HTML 页面通过 的 src 请求 http://domain-b.com/image.jpg。网络上的许多页面都会加载来自不同域的CSS样式表,图像和脚本等资源。

出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。 引自HTTP访问控制(CORS)

举2个例子比如我们自身当前域名为abc.com, 访问def.com 会出现跨域的问题。
比如我们自身当前域名为abc.com端口号为1234(abc.com:1234),那么访问abc.com:5678也会出现跨域问题。

笔者使用Flutter web项目 请求服务端资源的时候遇到了跨域问题。

http://localhost:55355/#/ 中的内容访问http://127.0.0.1:9988

Flutter web项目跨域现象图现象图如下:

Flutter web项目跨域问题

出现当前跨域问题的原因是端口号不同,访问Flutter web 的url 和 请求服务端资源的url的 端口号 不同。

请求的响应头中设置可跨域的origin,解决跨域问题

设置跨域的url 有2种设置方式:

  • 1.设置一个或多个url;
    • 如:request.response
      ..headers
      .add('Access-Control-Allow-Origin', request.headers['origin'])
  • 2.设置跨域的值为*;
    •   ..headers.add('Access-Control-Allow-Origin', '*')
      

注意:如果在本地测试使用,可以使用第二种方式,直接了当。但是一般线上的话最好使用第一种方式设置是否可以跨域请求。因为设置是否可以跨域,算是服务器在响应浏览器请求数据时的一种保护策略。

其中重点是在响应头中添加可以跨域的请求域。

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

如'Access-Control-Allow-Origin'可以指定特定的url,使url能够跨域请求。

如有需要指定允许多个url进行跨域请求。可以根据请求的origin的值,判断是否要做跨域响应头的处理。

如:如下代码设置了当请求的origin 为http://localhost:63062http://localhost:55355 的时候,会添加跨域处理的响应头。

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();
    }
  }
 }
解决跨域问题

笔者在上边说明了处理运行Flutter web项目的时候,处理本地服务端接口和Flutter web项目运行网址出现跨域问题的处理方式。(其实对于编译后的Flutter web项目的产物直接放到自己的服务端项目的的静态文件目录下的时候,不会出现上述问题)
有时候我们的请求内容可能就需要跨域去请求数据,而且对方如果也不便添加相应的跨域响应头。此时,可使用Nginx 做反向代理来处理跨域问题。

Nginx 反向代理解决远端跨域问题
server {
        listen       9080;
        server_name  localhost;
        
        location ~ /columns/Qtest {
            proxy_pass https://testerhome.com;
        }
    }

经上述处理,可以在本地的127.0.0.1:9080/columns/Qtest请求到 Qtest测试之道https://testerhome.com/columns/Qtest 相应数据。

本地Flutter web 项目跨域访问TesterHome Qtest测试之道

Nginx 配置反向代理及rewrite访问路径可实现访问远端文件不跨域。

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

经上述处理,可以在本地的127.0.0.1:9080/api/qiwuzhoukanWeb请求到 奇舞周刊https://weekly.75team.com 相应数据。

本地Flutter web 项目跨域访问奇舞周刊

五、Flutter web 项目上线

flutter build web会在项目的build 目录中生成相应的资源文件及html 和js文件,把相关文件放置到服务端静态文件目录下即可。实现上线Flutter web项目。

Flutter web项目编译产物

参考学习网址


了解更多iOS及相关新技术,请关注我们的公众号:

小编微信:可加并拉入《QiShare技术交流群》。

关注我们的途径有:
QiShare(简书)
QiShare(掘金)
QiShare(知乎)
QiShare(GitHub)
QiShare(CocoaChina)
QiShare(StackOverflow)
QiShare(微信公众号)

推荐文章:
用AdHoc来测试iOS线上推送
Swift 5.1 (9) - 结构体和类
Swift 实现一个兼容iOS、tvOS、OSX的抽象层
iOS Password AutoFill
iOS 给UILabel添加点击事件
用SwiftUI给视图添加动画
用SwiftUI写一个简单页面
Swift 5.1 (8) - 枚举类型
iOS App启动优化(三)—— 自己做一个工具监控App的启动耗时
iOS App启动优化(二)—— 使用“Time Profiler”工具监控App的启动耗时
iOS App启动优化(一)—— 了解App的启动流程
奇舞周刊