Flutter Web CORS解决方案2
- local-cors-proxy
- shelf_proxy
- dart run shelf_proxy
- flutter run --dart-define
- wrap with shell
- help & usage
- mode & conf
- run & debug
- launch with proxy
- vscode
- Android Studio
- 遗留问题
本文介绍第二种解决FlutterWeb CORS问题的方案:通过启动本地shelf_proxy服务代理协议cgi请求,并给出通过 vscode、AndroidStudio IDE 配置代理脚本快速启动调试的方法。
在 1-禁用浏览器安全策略 中,通过禁用chrome浏览器的安全策略或在浏览器中使用Allow-CORS插件,可以解决CORS问题。但浏览器中使用 Allow-CORS 插件访问时,仍存在部分协议OPTIONS预检跨域问题。
以上方案都是基于浏览器,对于企业微信H5页面应用的开发调试,其内置webView无法禁用安全策略,也无法启用Allow-CORS插件。
迫切需要支持移动终端通过 LAN IP 访问局域网内的web服务,以便调试企业微信内的实际布局渲染效果,及时发现和解决一些跨端兼容性问题。
Why does my http://localhost CORS origin not work?
local-cors-proxy
npm install Local CORS Proxy: npm install -g local-cors-proxy
Simple proxy to bypass CORS issues. This was built as a local dev only solution to enable prototyping against existing APIs without having to worry about CORS.
This module was built to solve the issue of getting this error:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disable
执行 npm ls -g --depth=0 或 npm ll -g --depth=0 可查看全局安装的 node_modules 目录。
可打开 node_modules/local-cors-proxy 文件夹查看 local-cors-proxy
代码:
- 实现文件:lib/index.js
- node命令行:bin/lcp.js
假设本地调试导致跨域问题(CORS issues)的 cgi 请求路径如下:
http://xxx.coding.net/api/qci/
CGI_HOST 为 xxx.coding.net,也记做 API_BASE_URL,也可能是 IP:PORT 模式。
执行 lcp --proxyUrl
启动:代理目标域名路径:
支持通过 -p(–port) 选项参数指定端口,默认端口为 8010。
$ lcp --proxyUrl http://xxx.coding.net
Proxy Active
Proxy Url: http://xxx.coding.net
Proxy Partial: proxy
PORT: 8010
Credentials: false
Origin: *
To start using the proxy simply replace the proxied part of your url with: http://localhost:8010/proxy
根据提示,修改代码 http.dart,将 baseUrl 中的目标域名(CGI_HOST)替换为 local-cors-proxy 代理服务 API_BASE_URL —— localhost:8010/proxy
:
http://localhost:8010/proxy/api/qci/
接下来,在命令行执行 flutter run -d
启动chrome调试,即经过代理服务请求 CGI 协议,绕过跨域。
- flutter run -d chrome --web-renderer=html --web-port=8080
shelf_proxy
- Flutter Web初尝试以及一些问题解决
- Flutter 2 for Web 跨域问题
- Flutter Web 跨域问题解决方案
- Flutter Web 跨域请求踩坑笔记
- 在
pubspec.yaml
中引入 shelf 依赖:
shelf_proxy 1.0.1 requires SDK version >=2.14.0 ❤️.0.0
shelf: ^1.2.0
shelf_proxy: ^1.0.1
- shelf: A model for web server middleware that encourages composition and easy reuse
- shelf_static: Static file server support for the shelf package and ecosystem
- shelf_proxy: A shelf handler for proxying HTTP requests to another server
- shelf_cors_headers: CORS headers middleware for Shelf
- 新建
scripts/proxy/shelf_lan_cgi_proxy.dart
文件:
支持传入启动参数
SHELF_PROXY_PORT
为 shelf_proxy 代理服务监听端口。
支持传入启动参数SHELF_PROXY_DOMAIN
为被代理的 CGI 域名(CGI_HOST)。
configAccessControl 设置请求策略,以便允许跨域。实测只设置前两个即可?
- Access-Control-Allow-Origin
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
- Access-Control-Allow-Credentials
// scripts/proxy/shelf_lan_cgi_proxy.dart
// dart run --define=SHELF_PROXY_DOMAIN=xxx.testing.coding.net
// --define=SHELF_PROXY_PORT=8010 shelf_lan_cgi_proxy.dart
import 'dart:io';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_proxy/shelf_proxy.dart';
// 代理服务的地址(或域名)
// final String serveAddress = '127.0.0.1';
var serveAddress = InternetAddress.anyIPv6;
// 代理监听的服务端口
var listenPort = 8010;
// 需要代理的目标域名(默认测试环境)
var targetUrl = 'http://xxx.testing.coding.net';
// 设置请求策略,允许跨域
void configAccessControl(HttpServer server) {
server.defaultResponseHeaders.add('Access-Control-Allow-Origin', '*');
server.defaultResponseHeaders.add('Access-Control-Allow-Headers', '*');
// server.defaultResponseHeaders.add('Access-Control-Allow-Methods', '*');
// server.defaultResponseHeaders.add('Access-Control-Allow-Credentials', true);
}
Future main() async {
// -D(--dart-define) 传参
const proxyDomain = String.fromEnvironment('SHELF_PROXY_DOMAIN');
if (proxyDomain.isNotEmpty) {
targetUrl = 'http://$proxyDomain';
}
const proxyPort = int.fromEnvironment('SHELF_PROXY_PORT');
if (proxyPort > 0) {
listenPort = proxyPort;
}
var server = await shelf_io.serve(
proxyHandler(targetUrl),
serveAddress,
listenPort,
);
configAccessControl(server);
print('Shelf-Proxy @ http://${server.address.host}:${server.port}');
print('Proxy Target Url: $targetUrl');
}
dart run shelf_proxy
- 执行
dart run shelf_lan_cgi_proxy.dart
命令,启动本地 shelf_proxy 代理服务。
- dart run --define 传参 SHELF_PROXY_PORT 为代理监听端口,缺省为 8010;
- dart run --define 传参 SHELF_PROXY_DOMAIN 为被代理的目标域名,缺省为 xxx.testing.coding.net;
$ dart run --define=SHELF_PROXY_DOMAIN=xxx.testing.coding.net --define=SHELF_PROXY_PORT=8010 scripts/proxy/shelf_lan_cgi_proxy.dart
Shelf-Proxy @ http://localhost:8010
Proxy Target Url: http://xxx.coding.net
- 修改 flutter web app 代码 http.dart,将 cgi 目标域名
apiBaseUrl
(例如 xxx.coding.net)替换为代理服务 API_BASE_URL ——localhost:8010
(LAN_IP:PORT):
// http.dart
baseUrl: 'http://localhost:8010/api/qci/',
- 执行
flutter run -d chrome
命令,启动 chrome 浏览器调试。
- flutter run -d chrome --web-renderer=html --web-port=8080
- 如果要支持局域网访问调试,则可按以下步骤操作:
- shelf_lan_cgi_proxy.dart 中的 serveAddress 已修改为 anyIP,可以是 localhost、127.0.0.1 或 192.168.0.106;
- 执行
flutter run -d web-server
命令启动服务,指定--web-hostname 0.0.0.0
为 anyIP:- flutter run -d web-server --profile --web-renderer html --web-port 8080 --web-hostname 0.0.0.0。
- 这样,局域网内其他机器浏览器即可通过 LAN IP 地址链接(http://192.168.0.106:8080)访问服务。
- 思考:如何输出一些 proxyHandler 日志?
flutter run --dart-define
在上面第二步中,每次都要修改 http.dart 中写死的CGI域名(apiBaseUrl、APIBASEURL)、替换不同环境的认证token(authToken),这很不方便。
考虑将以上两个参数,抽出来由 flutter run 通过 --dart-define
传入参数,Dart 代码中调用 fromEnvironment 解析参数,替换占位变量 apiBaseUrl
和 authToken
,如下图分支 ①。
wrap with shell
编写辅助启动 shelf_proxy 和 web app 的 Shell 脚本 launch_shelf.sh,主要功能是执行 dart run 启动代理、执行 flutter run 启动服务。
# 从配置文件中读取配置
get_env_config $mode $need_token
# 兜底LAN IP和相关端口
get_local_ipport
if [ $role = server ]; then
echo -e "✅ server listening on \033[4mhttp://$lan_ip:$web_port\033[0m"
elif [ $role = proxy ]; then
echo -e "✅ proxy listening on \033[4mhttp://$lan_ip:$proxy_port\033[0m"
fi
echo "------------------------------------------------------------"
set -x
if [ $role != proxy ]; then
# 启动 shelf_proxy 代理(& 转移到后台)
dart run --define=SHELF_PROXY_DOMAIN="${api_base_url:?unset or null}" --define=SHELF_PROXY_PORT=$proxy_port "$(dirname "$0")"/shelf_lan_cgi_proxy.dart &
# 启动 client or server
if [ $role = client ]; then
flutter run --$mode -d chrome --web-renderer=html --web-port=$web_port --dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port --dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
elif [ $role = server ]; then
flutter run --$mode -d web-server --web-renderer=html --web-port=$web_port --web-hostname=0.0.0.0 --dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port --dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
fi
else
# 启动 shelf_proxy 代理
dart run --define=SHELF_PROXY_DOMAIN="${api_base_url:?unset or null}" --define=SHELF_PROXY_PORT=$proxy_port "$(dirname "$0")"/shelf_lan_cgi_proxy.dart
fi
set +x
# 尝试杀死后台挂起的进程
kill_run
执行 dart run shelf_lan_cgi_proxy.dart,启动 shelf_proxy 代理服务。
- 从配置文件(config/debug.conf)中读取 CGI_HOST 到 api_base_url 传参给 dart run
SHELF_PROXY_DOMAIN
; - 读取sh命令行参数
-s
存储到 proxy_port,传参给 dart runSHELF_PROXY_PORT
;
help & usage
执行 launch_shelf.sh -h
可查看脚本帮助:
$ ./scripts/proxy/launch_shelf.sh -h
launch_shelf.sh version: 1.0.0
Usage: launch_shelf.sh [-?hvCSdpr] [-i ip] [-w web-port] [-s shelf-port]
Options:
-?,-h,--help : show help and exit
-v, --version : show version and exit
-C, --client : start as client, default
-S, --server : start as server
-P, --proxy : start as proxy daemon
-d, --debug : run in debug mode, default
-p, --profile : run in profile mode
-r, --release : run in release mode
-i, --ip ip : set lan ip
-w, --web-port port : config flutter web port, default 8080
-s, --shelf-port port : config shelf proxy port, default 8010
主要涉及两组参数。
-
脚本启动角色(role):
-C
对应 flutter run -d chrome,启动web server,并拉起 chrome 运行;-S
对应 flutter run -d web-server,启动web server,需要自行打开浏览器输入调试 url。-P
只启动 shelf_proxy 代理,可搭配作为自行执行 flutter run 的代理使用,例如指定作为 vscode launch preLaunchTask 代理脚本。
-C, --client : start as client, default
-S, --server : start as server
-P, --proxy : start as proxy daemon
-
代理模式(
-P
):可搭配作为自行执行 flutter run 的代理使用。- 外部基于 web server 监听端口访问 web 服务,web内部请求 cgi 经由 shelf_proxy 代理。
- 这种模式目前主要用作指定作为 vscode launch preLaunchTask 代理脚本,参见下文。
-
非代理模式(
-C
、-S
):sh 脚本还会执行 flutter run 启动 web app(-d chrome、-d web-server)。- 读取sh命令行参数
-w
作为 web_port。 - flutter run 通过 --dart-define 传参 API_BASE_URL、AUTH_TOKEN。
在 CS 模式中,dart run shelf_lan_cgi_proxy.dart 末尾需指定
&
转移到后台执行,避免阻塞,以便继续执行后续 flutter run 等命令。 - 读取sh命令行参数
-
运行模式(mode):
-d
调试模式、-p
诊断模式、-r
发布模式。- 可按需模拟调试某个环境的运行效果。
- 在 get_env_config 函数中,根据运行模式读取对应的 token(
"$env_mode"_AUTH_TOKEN
)。
-d, --debug : run in debug mode, default
-p, --profile : run in profile mode
-r, --release : run in release mode
mode & conf
脚本 aux_etc.sh 中定义的 get_env_config 函数,从配置文件 debug(profile,release).conf 中读取被代理的协议域名(CGI_HOST)和认证TOKEN(AUTH_TOKEN)。
conf 配置文件的 Shebang (
#!/usr/bin/env bash
)也申明为shell脚本,source导入后直接以$var
形式引用变量,避免复杂的文本解析,方便处理。
由于 AUTH_TOKEN 涉及隐私和安全,故不再在conf中写死泄漏,改为读取环境变量("$env_mode"_AUTH_TOKEN
),故请先在 zshrc 中申明导出三个环境的 AUTH_TOKEN:
# ~/.zshrc
export DEBUG_AUTH_TOKEN= 69c1********************************ccb4
export PROFILE_AUTH_TOKEN=2b57********************************fe89
export RELEASE_AUTH_TOKEN=2b57********************************fe89
无论是 -C
还是 -S
模式,–dart-define 的两个变量拼接如下,http.dart 中将解析这两个参数替换占位变量。
--dart-define=API_BASE_URL="${lan_ip:?unset or null}":$proxy_port
--dart-define=AUTH_TOKEN="${auth_token:?unset or null}"
- 从脚本运行参数
-s
提取proxy_port
,作为 shelf_proxy 代理监听端口。 - 主脚本的 main 函数,调用辅助脚本 aux_etc.sh 中的 get_env_config 函数,从 conf 中解析设置
auth_token
。 - 主脚本的 main 函数调用 get_local_ipport 函数,调用 aux_etc.sh 中的帮助函数 get_lan_ip 解析获取
lan_ip
。
run & debug
在项目根目录执行 sh 脚本:
- 不指定角色,缺省
-C
(-d chrome) 启动 client 模式,起好 web-server 之后,创建拉起 chrome 独立进程访问web服务。
$ ./scripts/proxy/launch_shelf.sh
- 指定角色
-S
启动 -d web-server 模式,点击提示中的 server listening 局域网链接打开浏览器即可访问web服务。
局域网中的其他机器,也可以输入该url访问服务。
$ ./scripts/proxy/launch_shelf.sh -S
✅ server listening on http://10.20.89.64:8080
- 指定角色
-P
启动纯代理模式,作为 vscode/Android Studio launch 的 preLaunchTask 任务,启动 proxy daemon。
launch with proxy
vscode/Android Studio 启动执行 main.dart,实际上是运行于 client 模式使用,相当于 flutter run -d chrome
。
故如果可以基于脚本或配置,在启动之前预启动 proxy daemon,并将监听的 IP:PORT 传给 flutter run --dart-define=API_BASE_URL,这样就可以实现 IDE 一键 launch with proxy,方便开发调试!
vscode
在 vscode 启动配置(.vscode/launch.json)中新建启动任务 debug - shelf
:
preLaunchTask
提前启动 tasks.json 中定义的 shelf_proxy 代理脚本——launch_shelf.sh。- 由于 json 无法读取环境变量,需要自行替换
API_BASE_URL
和AUTH_TOKEN
!关于局域网IP,可以执行
./scripts/proxy/launch_shelf.sh -P
查看其输出的 proxy listening 信息。
// launch.json
{
"name": "debug - shelf",
"request": "launch",
"type": "dart",
"program": "lib/main.dart",
"flutterMode": "debug",
"deviceId": "chrome",
"args": ["--web-renderer=html", "--web-port=8080"],
"toolArgs": [
"--dart-define",
"API_BASE_URL=LAN_IP:PROXY_PORT", // 局域网代理监听IP:PORT
"--dart-define",
"AUTH_TOKEN=DEBUG_AUTH_TOKEN"
],
"preLaunchTask": "shelf_proxy - debug"
},
注意:tasks.json 的 task config 中需指定 "isBackground": true
,将代理服务运行于后台(等效命令行末尾置后运行的 &
)。
// tasks.json
{
"label": "shelf_proxy - debug",
"type": "shell",
"command": "./scripts/proxy/launch_shelf.sh -P", // 默认代理端口为8010
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new"
},
"isBackground": true
},
这样,在 vscode 侧边栏 - Run and Debug 就可以选择运行 debug - shelf
,启动代理调试,解决跨域问题。
Android Studio
能否按照 vscode preLaunchTask 预启动 proxy 的思路,支持 Android Studio launch + proxy 呢?答案是肯定的,Android Studio 启动配置也支持指定 Before launch 任务,定义启动之前的操作。
工具栏 main.dart 下拉点选 Edit Configurations:
将打开 Run/Debug Configurations,Additional run args 为 --web-renderer=html --web-port=8080 --dart-define=API_BASE_URL=xxx.coding.net --dart-define=AUTH_TOKEN=69c1********************************ccb4
点击 Before launch ➕ Run External tool:
在打开的对话框中,点击➕ Create Tool,Program 右侧 Browse 打开点选 Finder 中的 scripts/proxy/launch_shelf_fuse.scpt
,然后确认:
将 Additional run args 中的 API_BASE_URL 参数修改为局域网(LAN_IP):代理端口(PROXY_PORT):
关于局域网IP,可以执行
./scripts/proxy/launch_shelf.sh -P
查看其输出的 proxy listening 信息。
如此设置确认后,点击Android Studio 工具栏绿色爬虫按钮(Debug ‘main.dart’),即可启动 launch with proxy。
思考:launch_shelf_fuse.scpt 为 Apple Script,封装启动 launch_shelf.sh -Pd
,能否在 Program 直接指定启动 sh 呢?
-d
表示 debug, 还可指定-p
for profile、-r
for release,注意填写对应环境的 AUTH_TOKEN。
#!/usr/bin/osascript
-- scpt dir
set cwd to quoted form of POSIX path of ((path to me as text) & "::")
do shell script "echo cwd = " & cwd
-- parent dir
set pcwd to do shell script "echo $(dirname " & cwd & ")"
do shell script "echo pcwd = " & pcwd
-- grandfather dir
set ppcwd to do shell script "echo $(dirname " & pcwd & ")"
do shell script "echo ppcwd = " & ppcwd
tell application "Terminal"
do script "cd " & ppcwd & ";" & "./scripts/proxy/launch_shelf.sh" & space & "-P" & space & "-d"
activate
end tell
问题是 Program 没法指定 sh 参数,且运行完 Before launch 任务,sh进程立即就会退出,无法运作为proxy daemon。
故这里写成 osascript,打开 Terminal 并使之 activate。这样 osascript 退出 Terminal 仍在,等效实现了proxy daemon。
遗留问题
目前发现构建详情页,点击切换到【构建物】tab,拉取构建物列表还是报 CORS error:
http://10.65.91.54:8010/api/qci/rest-api/totalresult/13819946/artifacts?page=1&ver=2
可能需要深入 shelf_proxy.dart 源码调试,分析一下具体原因。