多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

鸿蒙 Flutter 网络层适配:dio_web_adapter 跨域拦截与 Cookie 实战解析

鸿蒙 Flutter 网络层适配:dio_web_adapter 跨域拦截与 Cookie 实战解析 这两年做 Flutter 跨端筛过不少轮子最让我纠结的永远是网络层。尤其是项目要落到鸿蒙上跑 Flutterdio 的三方适配器dio_web_adapter一下就变成了绕不开的实战重点。很多朋友第一反应是“鸿蒙不是兼容 Android 吗直接上dio_http_adapter不就行了”但真的在真机上跑一遍你会发现 WebView 环境、跨域策略、Cookie 透传、请求拦截这几座大山每座都能卡掉一两天工期。这篇内容就是把我自己从零适配dio_web_adapter到鸿蒙的全过程、踩坑记录和最终解决方案整理出来适合正在做 Flutter 鸿蒙化、或者被浏览器/WebView 环境网络问题折磨的移动端同学参考尤其是涉及跨域拦截、请求穿透这类细节的场景可以直接照着改。1. 为什么鸿蒙上要单独聊 dio_web_adapter1.1 dio 适配器的分层逻辑——先搞清楚 adapter 到底做了什么很多 Flutter 开发者天天用 dio却很少打开它的源码看一次。dio 本身并不是网络请求的真正执行者它只负责把BaseOptions、RequestOptions、拦截器、取消令牌这些上层逻辑整理好真正发出 HTTP 请求的动作全部委托给httpClientAdapter完成。这个 adapter 就是真正的“运输队”而 dio 只是“调度中心”。默认情况下dio 在移动端用的是IOHttpClientAdapter它背后是dart:io的HttpClient走的是标准 Socket 链路。在 Android、iOS 上这套链路非常成熟几乎不用操心。但在鸿蒙场景下问题来了dart:io的HttpClient在部分鸿蒙 Flutter 运行时里并不是完整的实现或者因为权限、代理设置、证书校验策略等原因表现得很不稳定。我实测过的表现包括请求发出后长时间不回调、onHttpClientCreate里自定义的证书校验不生效、偶发直接抛出UnimplementedError。这些故障还不是每次必现排查起来极其折磨。dio_web_adapter提供的BrowserHttpClientAdapter则完全是另一条链路。它不依赖dart:io而是把请求转换成 Web 环境下的HttpRequest底层是 XHR/fetch由 WebView 或浏览器的网络栈真正发出请求。如果鸿蒙上的 Flutter 容器本身提供了可用的 Web 运行时能力那么这条路比硬啃dart:io要稳得多。1.2 鸿蒙环境的特殊性网络栈、WebView 与沙箱限制鸿蒙不是 Android也不是 iOS它是一个独立的操作系统。虽然早期版本通过兼容层可以跑 APK但如果你做的是 HarmonyOS NEXT 或基于 OpenHarmony 的 Flutter 应用就要面对一套全新的运行时约束。最直接影响网络层的有三点。第一系统网络权限管控严格。普通 Android 需要在AndroidManifest.xml里加INTERNET权限鸿蒙则要在module.json5里声明ohos.permission.INTERNET漏了就直接断网而且有时候编译不报错运行到真机上请求全失败非常隐蔽。第二WebView 环境的同源策略和 CORS 约束依然生效。如果你在 Flutter 页面内嵌了 ArkWeb 组件或者你的 Flutter 容器本身活动在 Web 兼容层上那么发出去的请求必须遵守跨域规则。dio 的拦截器可以对业务层做管理但浏览器内核层面对跨域请求的拦截不受 Dart 代码控制只能通过服务端响应头、withCredentials配置、预检请求preflight等方式去“对齐规则”。第三Cookie 管理机制不同。在原生 Android 上Cookie 由系统级CookieManager统一管理。在鸿蒙的 Web 环境下Cookie 的存取行为和浏览器保持一致但跨会话持久化未必自动完成。稍后我会给出针对性的持久化方案。1.3 判断依据什么时候必须切换到 Web 适配器不是所有鸿蒙项目都需要切到dio_web_adapter但下面这些信号只要命中任意一条你就应该认真考虑切换项目在鸿蒙真机调试时出现了UnimplementedError、HttpClient is not implemented这类运行时异常你的页面以内嵌 WebView 为主Flutter 侧的请求需要和 H5 页面共享 Cookie、鉴权体系网络请求需要借助 Web 运行时来“穿透”某些沙箱限制走通标准链路需要精确控制跨域请求的凭据携带、预检行为而原生 adapter 暴露不了这层能力。简单说当dart:io链路在鸿蒙上“使不上劲”时dio_web_adapter就成为了那个最贴近底层、最可控的替代方案。2. 适配前的工程准备依赖、权限与运行时判断2.1 梳理依赖不是简单替换一个包如果你以为把dio_http_adapter换成dio_web_adapter就完事了那后面会踩很多坑。dio_web_adapter的BrowserHttpClientAdapter依赖 Web 标准 API在纯 Dart VM 环境比如鸿蒙原生线程中直接使用会出问题。所以第一步是确认自己的项目是否满足使用 Web adapter 的前置条件。在实际项目中我的做法是把可运行平台明确写进条件判断里而不是硬编码。依赖方面在pubspec.yaml里确保有这些dependencies: dio: ^5.4.0 dio_web_adapter: ^1.0.0然后创建一个统一的适配器工厂让不同平台各取所需。这里注意鸿蒙设备上不要简单粗暴地通过Platform.isAndroid之类的判断走原逻辑因为鸿蒙系统也会在某些场景下让这个判断结果变得模棱两可。更稳妥的做法是用kIsWeb结合运行时能力检测再加上一个可手动覆盖的开关。import package:flutter/foundation.dart; import package:dio/dio.dart; import package:dio_web_adapter/dio_web_adapter.dart; HttpClientAdapter createAdapter() { // 鸿蒙项目里如果 Web 运行时可用优先走 Browser adapter // 其他原生平台继续走 IOHttpClientAdapter保证稳定性 if (kIsWeb || isHarmonyWebRuntime) { return BrowserHttpClientAdapter(withCredentials: true); } return IOHttpClientAdapter(); }这里的isHarmonyWebRuntime不是官方 API而是我在入口处做的一个运行时探针。比如通过状态通道从原生侧读一次系统版本号或者尝试访问 Web 运行时提供的对象来判断。核心原则是“先探测再决策”不要在还不确定环境时就初始化 adapter。2.2 module.json5 里的网络权限与安全策略鸿蒙的权限声明位置和 Android 不一样。找到你的工程的entry/src/main/module.json5在module节点下增加请求权限{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.INTERNET, reason: 需要访问网络数据, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }如果漏了这一步dio 发出的请求会在最底层直接失败而 Dart 侧拿到的错误往往是泛化的 SocketException 或网络异常很难一眼定位是权限问题。我在第一次真机调试时就白白花了半天排查这个问题。还要注意明文流量。鸿蒙跟 Android 9 之后的策略类似默认情况下对明文 HTTP 请求是限制的。如果你的后端接口还有http://的需要在网络安全配置里放行。具体位置可能在resources/rawfile/network_security_config.json然后在module.json5的安全配置里引用它或者在调试阶段临时允许明文流量。这个点虽然不是dio_web_adapter本身的问题但不提前处理适配做完依然还是连不通。2.3 运行环境自检把故障提前暴露在启动阶段适配器切换完之后我建议把网络链路的自检放在应用启动阶段而不是等用户点击某个功能时才慢慢报错。一个很实用的做法是启动后用一个最小化的 dio 实例请求一个静态接口比如GET https://www.example.com/health把耗时、状态码、响应头打印到日志里。这一步能快速区分“适配层问题”和“业务层问题”。我在日志里额外打印了 adapter 的类型、是否走 Web 链路、浏览器内核的 userAgent 信息。这些信息一旦出现问题能帮你快速缩小范围。另外Flutter 鸿蒙化环境下日志输出不一定走标准debugPrint有时需要配合hdc shell hilog才能看到完整 Dart 日志。提前把这些检查项固化下来后面排障会舒服很多。3. 鸿蒙化适配实战从挂载 adapter 到跨域拦截3.1 第一步最小化跑通一个 GET 请求不要一上来就铺开所有接口。先用一个最简单的 dio 实例做验证把复杂拦截器和业务逻辑全部去掉。代码大致长这样import package:dio/dio.dart; import package:dio_web_adapter/dio_web_adapter.dart; Futurevoid smokeTest() async { final dio Dio(BaseOptions( baseUrl: https://api.example.com, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); dio.httpClientAdapter BrowserHttpClientAdapter(withCredentials: true); try { final response await dio.get(/ping); debugPrint(smoke success: ${response.statusCode}); } catch (e) { debugPrint(smoke failed: $e); } }这里有一个容易被忽略的关键参数withCredentials: true。它决定了跨域请求是否携带 Cookie、TLS 证书等凭据信息。如果你在鸿蒙 Web 环境里要复用 H5 登录态这一项必须设为true。但它也带来一个副作用服务端的Access-Control-Allow-Origin不能再返回*必须返回具体的源否则浏览器会因为安全策略直接拦截响应。也就是说withCredentials打开了跨域凭据通道那服务端 CORS 策略也必须跟着精调。用最小请求确认整条链路通之后才好进入下一步。3.2 第二步把 adapter 注入正式网络层用拦截器统一处理请求与响应在实际项目里网络层一般都有一个单例 Dio 封装类似下面这种结构。这里建议借鉴 Flutter 里使用 Provider 管理状态时的思路把依赖统一在入口处注入而不是在业务页面里到处 new Dio。adapter 也一样只初始化一次全局共用。class ApiClient { ApiClient._() { dio Dio(BaseOptions( baseUrl: https://api.example.com, headers: {Content-Type: application/json}, )); dio.httpClientAdapter createAdapter(); dio.interceptors.add(_buildInterceptors()); } static final ApiClient instance ApiClient._(); late final Dio dio; Interceptor _buildInterceptors() { return InterceptorsWrapper( onRequest: (options, handler) { final token storage.read(token); if (token ! null) { options.headers[Authorization] Bearer $token; } options.headers[X-Platform] harmony; handler.next(options); }, onResponse: (response, handler) { handler.next(response); }, onError: (DioException e, handler) { handler.next(e); }, ); } }拦截器是理解 dio 体系的关键。onRequest阶段可以用来统一加签名、加 token、改写请求头onResponse阶段用来做全局响应解包onError阶段做统一错误上报。跨域拦截的“拦截”二字在这个阶段体现为你可以拦截请求、修改它、放行或终止它但要注意这属于应用层的拦截浏览器内核层面的 CORS 拦截只能靠配置去对齐不能靠 Dart 代码强行绕过。3.3 第三步Cookie 与登录态跨会话持久化鸿蒙 Web 运行时环境下Cookie 的存取行为和浏览器内核强相关。但一个很现实的问题是BrowserHttpClientAdapter并不会自动把响应里的Set-Cookie帮你持久化到磁盘。如果你的 App 重启后希望登录态还在就得自己处理。有一个比较可落地的手动方案用shared_preferences或者鸿蒙侧的 Preferences 能力在响应拦截器里读取set-cookie头解析出关键 Cookie 字段然后持久化下次启动时在请求拦截器里拼装进Cookie请求头。onResponse: (response, handler) async { final setCookies response.headers[set-cookie]; if (setCookies ! null) { for (final rawCookie in setCookies) { // 只保存 namevalue 部分Expires、Path、Secure 等属性单独按需处理 final cookiePair rawCookie.split(;).first; await cookieStorage.save(cookiePair); } } handler.next(response); }这一步看起来很绕但实际价值非常大。因为当你的 Flutter 页面和 H5 页面共存在鸿蒙容器里时只要 Cookie 能够统一读写两边登录态就是打通的。用这个方案我成功让 Flutter 业务和 H5 业务做到了免二次登录体验和原生一模一样。至于 Cookie 里的HttpOnly字段虽然从 Dart 侧读不到但 WebView 内核自己会维护前提是请求确实通过 Web 链路发出。3.4 第四步跨域拦截与 preflight 预检请求的精细控制这是标题里“跨域拦截实战”真正要展开的地方。在鸿蒙 Web 环境里只要请求的目标源和当前页面源不一致就可能触发 CORS 预检。预检通常是浏览器自动发起的OPTIONS请求Dart 代码里看不到这个请求但服务端必须正确处理。跨域请求能否成功取决于三件事请求头里有没有自定义头、Content-Type 是否是非简单类型服务端是否正确返回Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-MethodswithCredentials为true时服务端是否允许携带凭据并返回具体源。实际操作中我见过太多的后端同学只配了一个Access-Control-Allow-Origin: *结果前端一旦加上 Authorization 头就开始报 CORS 错误。正确做法是后端根据请求头动态返回允许的源或者至少在预检请求里准确返回Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With。如果你自己掌控服务端可以用下面这个精简示例作为跨域策略的参考Access-Control-Allow-Origin: https://your-harmony-app.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-Platform Access-Control-Allow-Credentials: true Access-Control-Max-Age: 86400Access-Control-Max-Age是减少预检次数的重要参数。如果在鸿蒙 Web 环境里发现每一次 POST 请求都要多一次OPTIONS请求那就是这个缓存时间没设置好合理设置之后能明显减少网络往返。3.5 第五步上传下载与 FormData 在 Web 链路上的差异文件上传和下载也是适配中的重灾区。在原生 adapter 下FormData走的是标准 HTTP multipart 协议到了 Web 链路BrowserHttpClientAdapter内部会把FormData转成 Web 环境能识别的格式。大体上能用但有几个细节要注意。上传文件时如果你传入的是文件路径字符串在原生环境没问题在 Web 环境可能会直接失败。因为 Web 运行时根本没有“本地文件路径”这个概念你需要先把文件转成blob或字节数组。Flutter 侧用http_parser包里的MultipartFile.fromBytes会更稳妥。下载文件也一样。Web 链路拿到的ResponseBody可能是以内存流形式存在的你不能像原生环境那样直接落盘到一个路径。需要把字节取出来再通过鸿蒙的文件管理能力写入应用沙盒。我在项目里的做法是统一封装一个saveBytesToHarmony(bytes, filename)方法底层调用系统的文件保存 API这样上层业务不用关心当前跑在哪条链路上。4. 常见问题与排障实录4.1 高频报错与解决方案速查这里我把适配鸿蒙过程中出现频率最高的几个问题整理成了速查表每一条都是我至少踩过一次、真实解决了之后才敢写进来的。现象可能原因解决思路启动后所有请求立刻失败错误为 SocketExceptionmodule.json5缺少网络权限或明文流量被拦截检查ohos.permission.INTERNET配置网络安全策略放行调试域名请求能发出去但响应永远被浏览器拦截报 CORS error服务端Access-Control-Allow-Origin配置不完整或和withCredentials冲突服务端改返回具体源补齐 Allow-Headers、Allow-Methods、Credentials每次 POST 都多出现一个OPTIONS请求自定义请求头触发 preflight且未设置缓存服务端设置Access-Control-Max-Age减少预检次数Cookie 登录态在 App 重启后丢失BrowserHttpClientAdapter不负责持久化自行解析set-cookie写入 Preferences下次请求手动拼装上传文件失败或报“文件路径不存在”Web 链路不能直接读取本地文件路径改用MultipartFile.fromBytes或先读取为字节数据偶发UnimplementedErrordart:io相关能力在鸿蒙 Flutter 运行时未完整实现切换到BrowserHttpClientAdapter确保 Web 运行时可用某些接口在 Android 正常鸿蒙上 statusCode 为 0大多是跨域拦截被内核直接阻断Dart 层拿不到响应用抓包工具确认是否 preflight 失败优先排查响应头表格里出现最多的是 CORS 相关的问题因为它在鸿蒙上表现得最像“网络错误”但实际并没有走到服务器。定位时别只盯 Dart 报错要结合浏览器内核日志一起看。4.2 抓包工具与日志定位技巧鸿蒙上抓包不像 Android 那么顺手但也不是没办法。我在项目里常用两种方式。第一种是在 Flutter 侧开启 dio 的日志拦截器把LogInterceptor放到所有拦截器最前面记录请求方法、路径、请求头、响应状态码和耗时。注意响应体别全量打印线上环境数据量大只打印前几百字节就够定位问题了。第二种是抓网络层完整请求。如果你用的是 DevEco Studio可以配合网络抓包工具或鸿蒙侧的网络日志能力看请求是不是真的发出了服务端有没有返回响应头带的是什么。我遇到过一个极其隐蔽的问题服务端其实已经返回了正确的 JSON但就因为响应头里少了Access-Control-Allow-Origin请求在 Dart 侧表现为“网络错误”。如果只看业务层日志永远别想定位到原因。4.3 独家避坑经验适配顺序比适配本身更重要适配dio_web_adapter到鸿蒙我个人体会最深的一点是不要急着处理所有接口也不要急着写一堆兼容代码。正确顺序应该是先跑通最小请求再检查 CORS 策略再处理 Cookie最后才上复杂业务。另外建议在代码里保留一个手动开关比如通过环境变量或者bool.fromEnvironment(USE_WEB_ADAPTER)控制是否启用 Web adapter。这样万一遇到某些业务场景确实需要原生链路随时能切换不用重新发版。这个开关救过我一次有一次鸿蒙的 Web 运行时在某个系统版本上出现适配问题我远程把开关关掉App 立刻切回原生链路服务不受影响然后才有时间慢慢排查。最后说几句整套方案在我手头的鸿蒙 Flutter 项目里已经稳定运行了三个迭代版本。最开始我也觉得直接沿用现成的dio_http_adapter是天经地义的事直到被UnimplementedError和 CORS 反复摩擦才彻底理解“平台适配”这四个字有多重。如果你也在鸿蒙上做 Flutter我的建议是从最小请求开始一层层放行跨域、Cookie、上传下载这些阻碍每做一步就验证一步别指望能一口气吃成胖子。把dio_web_adapter的机制吃透你其实就掌握了 Web 环境下网络请求的底层脉搏以后再遇到类似的平台适配思路都会清晰很多。
返回列表