📦 Flutter 三方库 webview_cookie_manager 是用于读写 WebView Cookie 的 package。在鸿蒙端回读时,原有实现会将与控制属性同名的业务键当作响应属性处理而丢失名称和值,我在原库基础上修正了这段解析逻辑。
完整代码已提交 PR 至官方仓库 CPF-Flutter/fluttertpc_webview_cookie_manager,有问题可以到仓库提 Issue,也可以来 鸿蒙系统Flutter开源库社区 交流。

前言

我做了一个 Flutter 离线诊断页,先后在 Mate X7 模拟器和华为畅享 100 Pro Max 实体真机上写入四组 Cookie,再读回来核对。列表里仍有四项,其中三个名称却成了空字符串。顺着系统回读和插件返回值往下查,我发现数据已经存好,是插件把部分业务名称当成了 Cookie 控制属性。删掉这段错误分支后,同样四组输入在模拟器和真机上都能按原名读回,多等号的值也保持完整。

修复并跑完模拟器对照后,我又把原版代码和故障线索交给 HarmonyOS Dev Assistant 复核。它给出了相近的方案,不过报告里声称处理好的空名问题,沿着返回路径检查仍有遗漏。这次我也把这段核查过程记下来,看看工具的解释和代码行为是否一致。

一、使用场景与痛点

1.1 从合成输入复现 Cookie 丢名

混合页面Cookie回读与属性同名键解析场景

图1 回读场景

Flutter 混合应用常需要在关闭内嵌网页后读取网页留下的键值状态,再决定跳转到哪个模块或恢复哪个工作区。假如既有网页把业务状态存成 path=setup、domain=workspace 这类 Cookie,Dart 调用方就需要按原始名称取值。键名恰好与 HTTP Cookie 控制属性同名,不应成为插件丢弃业务数据的理由。

我用合成数据模拟向导步骤、工作区和偏好,不接入真实网页、线上账号或登录服务,业务会话恢复也不在测试范围内。实际运行的是一个纯 Flutter 离线诊断页,目标地址固定为 RFC 2606 保留测试域下的 https://pgc-cookie.example.test/app,宿主不申请 ohos.permission.INTERNET 权限。

为了避免把不同层次的职责混为一谈,我将实验涉及的四层边界分开核对:

  • ArkWeb 系统存储层:负责在系统内核保存并按 URL 返回原始 Cookie 字符串,本次实际保存并读回了 path、domain 等业务键名。
  • Flutter 插件跨端层:即 webview_cookie_manager 鸿蒙端,负责把系统返回串拆解并通过 MethodChannel 映射为 Dart Cookie 列表,我定位到的缺陷就在这一层。
  • 诊断宿主观察层:仅在同款 Example 中读取系统原始字符串,用来确认前置写入已经成功,不替待测插件改写或补齐结果。
  • 应用业务消费层:拿到公开 List<Cookie> 后按业务键名读取配置,不应被迫在业务代码里猜测空名称原本对应哪个字段。
示例用途合成输入本篇核对内容
向导步骤path=setup名称与值是否原样保留
工作区标识domain=workspace业务键名是否误入域属性
偏好状态secure=yes字符串值是否被改成布尔值
含等号的值session=fixture=token==原有多等号切分是否保持

表1 合成输入

表 1 前三项触发同名键缺陷,第四项检查兼容性。原版已经能保留 fixture=token==,补丁需要保持这一行为。

1.2 查库范围与基线选择

我检查了 oh-Flutter、CPF-Flutter 的相关适配仓库、原上游包和同功能替代包,重点看公开读写接口及鸿蒙端的实际解析代码。

原上游是 fryette 维护的 webview_cookie_manager 仓库,提供 Flutter 侧的 Cookie 读写、查询与清理接口。图 2 是仓库页面的实际截图;当前 master 已有 ohos 目录,包内版本为 2.0.7,原生解析中仍保留了按属性名分支的逻辑。pub.dev 当前发布版本是 2.0.6,不能把源码里的版本号当作已经发布的版本。

fryette维护的webview_cookie_manager仓库目录

图2 上游仓库

pub.dev 的 2.0.6 发布页 对应原发布包;CPF-Flutter 社区适配仓 则提供本篇使用的鸿蒙实现及稳定标签。社区版本已经接好了 Dart 接口与 ArkWeb,本文是在这份实现上修正回读解析。各候选的核对范围如下。

来源版本或分支核对结果
webview_cookie_managerpub.dev 2.0.6原发布包仅提供 Android、iOS 实现
fryette 原上游仓库master,非发布包已有鸿蒙目录,仍含属性名分支;仅源码核对
CPF-Flutter 社区库2.0.7-ohos-1.0.0已有鸿蒙实现,同名键解析失败
同一社区库开发分支dev,非稳定标签主机同输入检查仍失败
webview_cookie_manager_flutterpub.dev 3.1.0;上游 v3.1.0 / master发布包已含 OHOS,同输入检查仍失败

表2 查库快照

替代包 webview_cookie_manager_flutter 3.1.0 已经内置 OHOS 支持。我固定社区稳定标签 2.0.7-ohos-1.0.0 作为基线,是为了给正在使用该仓库的项目提供窄范围、可逐行对照的最小补丁;同时核查 dev 分支与替代包,排除“升级到已核对版本即可绕开问题”的可能。模拟器实测覆盖选定的社区稳定标签,其余候选结论来自主机源码检查。

将问题定性为插件缺陷之前,我先核对了鸿蒙目录 README、Dart API 注释与原生实现:文档明确将 getCookies(String? url) 列为已支持接口,注释也约定它返回指定 URL 下的 Cookie。合法业务键在回读时丢失名称与值,直接违背了现有接口契约,并非额外新增的产品需求。

原版缺陷的复现步骤与观察结果按顺序如下:

  1. 固定依赖 2.0.7-ohos-1.0.0,调用公开接口 setCookies 向合成测试域写入表 1 的四项数据。
  2. 通过独立观察通道读取 ArkWeb 原始键值串,确认四组数据均已真实落入系统存储;若前置检查失败则立即停止后续插件判定。
  3. 调用 getCookies('https://pgc-cookie.example.test/app'),预期返回的四个 Cookie 对象在名称和值上均与输入一致。
  4. 原版实际仍返回四个对象,但名称数组变成 ["", "", "", "session"];一旦按名称汇总为键值字典,三个空名条目互相覆盖,最终只剩 {"": "", "session": "fixture=token=="},呈现数量正确但内容缺失的假象。

除上述三项外,原生分支还涉及 httpOnly、expires 及大小写变体。我在模拟器上验证了前三个同名键,其余条件的检查方式和结果列在后文的边界表中。

二、修复前后的效果

2.1 同页、同输入的模拟器对照

我为原版 baseline 和人工修复版 patched 配备了完全一致的诊断页面、宿主声明与依赖锁,只改动插件内的一个原生解析方法。两组使用同一应用包名,先后覆盖安装到同一台 Mate X7 折叠屏模拟器。应用启动约三秒后自动串行执行初始化、写入、系统回读、插件回读与定向过期请求,结果不受人工点击时机影响。

原版基线在Mate X7模拟器上的Cookie回读过程

图3 修复前动图

最小修复版在Mate X7模拟器上的Cookie回读过程

图4 修复后动图

原版基线回读完成页面

图5 原版结果

最小修复版回读完成页面

图6 修后结果

两组的“系统真实写入和回读”均显示通过,读到的系统原始串都是 path=setup; domain=workspace; secure=yes; session=fixture=token==。这一步把故障范围收敛到了系统回读之后:数据已经完整保存在 ArkWeb 内核中,差异完全出在插件解析并返回给 Dart 的结构上。诊断页用 jsonEncode 渲染名称列表,让空字符串 "" 直观显露出来,避免被误看成少返回了条目。

核对项原版 baseline人工修复 patched
系统原始回读四组键值完整四组键值完整
公开列表长度44
返回名称数组["","","","session"]["path","domain","secure","session"]
键值字典与输入一致否是
值中的多个等号保持完整保持完整
定向过期请求调用返回调用返回

表3 实测对照

表 3 最后一项只代表清理调用正常返回。我没有在过期请求后再次回读,尚不确定系统存储是否已清空。

2.2 环境、配置与日志

环境项使用环境
DevEco Studio26.0.0.865
HarmonyOS SDK26.0.0.105 Release,API 26
Flutter 鸿蒙社区版3.41.9+ohos-1.0.2
Dart3.11.5
社区插件标签2.0.7-ohos-1.0.0,包内版本 2.0.7
模拟器环境Mate X7 折叠屏,镜像 7.0.0.107 / 系统回读 OpenHarmony-7.0.0.105
实体真机环境华为畅享 100 Pro Max(型号 YEW-AL80),固件 7.0.0.100 / 系统回读 OpenHarmony-7.0.0.105,arm64-v8a
诊断宿主模拟器使用 unsigned debug HAP;真机在隔离目录配本地调试签名,每次切换前卸载旧包
复核工具VS Code HarmonyOS Dev Assistant v0.2.3
工具模式 / 模型三方库鸿蒙化 / DeepSeek/DeepSeek V4.1 Flash 1017

表4 验证环境

模拟器安装 --debug --no-codesign 调试包,华为畅享 100 Pro Max 真机则在纯英文隔离目录配置本地调试签名后安装验证,两组均不申请网络权限。

插件配置声明与复现工程目录结构

图7 配置与目录

我把两组工程的页面、配置和依赖保持一致,只切换被测的解析方法:

  • 保留原仓库示例不动:上游自带的 example/ 会引入另一套 WebView 组件并请求外网,我保留原样不改,在同级新增 diagnostic_example/ 通过 path: …/ 消费当前插件,单独隔离 Cookie 读写链路。
  • 控制单一生产变量:两组均基于同一提交的 131 个上游跟踪文件导出,并放入完全相同的 37 个诊断源文件与锁配置;生产代码仅有 WebviewCookieManagerPlugin.ets 一处差异,公开 Dart 接口与其他平台代码保持不变。
  • 插件与探针通道分离:待测插件由 ohos/index.ets 导出,宿主内的观察通道单独在 EntryAbility 注册,只负责观察系统结果。

Mate X7模拟器CookieNamesProbe原始HiLog对照

图8 关键日志

图 8 按 CookieNamesProbe 过滤原始 HiLog:原版进程 5712 的 system_readback 通过后,public_readback 名称数组出现三个空串;修复后进程 6360 在同一事件中完整带回四组键值,末尾 probe_complete 输出 systemPassed: true, pluginPassed: true,与界面状态逐项吻合。

2.3 华为畅享 100 Pro Max 真机复测

为了确认实体手机上的 ArkWeb 内核返回格式与模拟器一致,我又通过 USB 连接 华为畅享 100 Pro Max 实体真机(型号 YEW-AL80,固件 7.0.0.100,系统回读 OpenHarmony-7.0.0.105),在纯英文隔离目录为原版与人工修复版配上本地调试签名,每次切换前执行 hdc uninstall 保证全新冷启动环境。

真机运行结果与表 3 完全一致:两组的“系统真实存入和回读”均通过,原始回读串均为 path=setup; domain=workspace; secure=yes; session=fixture=token==;原版返回的插件实际名称数组仍为 ["","","","session"],修复版则恢复为完整的 ["path","domain","secure","session"]。图 9 和图 10 是经 DevEco Studio 投屏(顶栏设备序列号已做不可逆像素马赛克脱敏)截取的真机前后对照画面。

华为畅享 100 Pro Max 真机原版基线回读丢名结果

图9 真机原版

华为畅享 100 Pro Max 真机人工修复版完整回读结果

图10 真机修复

三、原理与核心代码

3.1 解析器混淆了两种 Cookie 语法

Cookie原始返回与解析修复流程图

图11 解析对比

理解这个缺陷的关键,是区分 HTTP 状态管理规范里的两种不同文本格式。

RFC 6265 §4.1.1 与 §5.4 分别说明了两种语法:Set-Cookie 响应头在首个 cookie-pair 之后才跟随 ; Path=/app; Domain=...; Secure 等控制属性;而 Cookie 请求头中,分号隔开的每一段都是独立的 cookie-name=cookie-value 业务键值对,不再携带属性声明。

ArkWeb 的 webview.WebCookieManager.fetchCookieSync(url) 返回的正是后者——纯键值拼接串,而非服务端当初下发的整条 Set-Cookie 响应头。因此,返回串里的 path=setup 只能是名为 path、值为 setup 的业务 Cookie;反过来,仅凭这条返回串也无法逆向还原某条 Cookie 写入时的真实 Path、Secure 或 HttpOnly 属性。

原生分支误吞同名键

原版 getCookies 先按分号切开 fetchCookieSync 返回串,随后却在 parseCookie 里把每段等号左侧的名称转为小写,按响应头属性逐一分支处理(完整原版见 原版 WebviewCookieManagerPlugin.ets):

  • path 与 domain 分支:如 path=setup、domain=workspace 命中分支后,只把值塞进 Map 的 path 或 domain 元数据槽位,根本没有写入 name 和 value。
  • secure 与 httpOnly 分支:如 secure=yes 只在值恰好等于 'true' 时把布尔属性设为真,业务值 'yes' 直接变成 secure: false,同样丢失 name 和 value。
  • 大小写变体一并命中:因为进 switch 前执行了 toLowerCase(),诸如 PATH、Domain、Secure 等大写业务键也会落入上述属性分支。
  • expires 分支的潜在类型异常:源码把字符串值直接写入 resultMap.set('expires', value),未做时间戳转换,为下游 Dart 消费埋下了运行时错误。
Dart 空串兜底掩盖缺名

残缺 Map 经 MethodChannel 传回 Dart 后,由原包的 getCookies 组装为 Cookie 对象。下面摘录完整 Dart 消费函数,仅将注释改成中文以便阅读,执行逻辑保持原样(出自 webview_cookie_manager.dart),空名条目为何能混进返回列表、后续开发助手方案为何仍有漏网之鱼,都要检查这段返回路径:

  // _channel 是类内已有的 MethodChannel;字符过滤沿用原包方法。
  Future<List<Cookie>> getCookies(String? url) {
    return _channel.invokeListMethod<Map>('getCookies', {'url': url}).then(
        (results) => results == null
            ? <Cookie>[]
            : results
                .map((Map result) {
                  Cookie? c;
                  try {
                    c = Cookie(result['name'] ?? '',
                        removeInvalidCharacter(result['value'] ?? ''))
                      // 原包沿用跨端属性槽位,不能据此还原系统中的安全属性。
                      ..path = result['path']
                      ..domain = result['domain']
                      ..secure = result['secure'] ?? false
                      ..httpOnly = result['httpOnly'] ?? true;

                    if (result['expires'] != null) {
                      c.expires = DateTime.fromMillisecondsSinceEpoch(
                          (result['expires'] * 1000).toInt());
                    }
                  } on FormatException catch (_) {
                    // 仅过滤构造失败的条目;空名称不会触发这个异常。
                    c = null;
                  }
                  return c;
                })
                .whereType<Cookie>()
                .toList());
  }

当原生 Map 缺少 name 和 value 时,result['name'] ?? '' 会静默补上空字符串。当前 Dart SDK 中 Cookie('', '') 构造不会抛出 FormatException,于是每条残缺 Map 都顺利变成了合法的 Cookie 实例,列表总长度依旧是 4。

从上述过期时间转换代码还能推导出 expires 同名键的连锁风险:一旦原生端把业务字符串放进 result['expires'],Dart 执行 result['expires'] * 1000 会触发字符串重复拼接,紧接着在 String 上调用不存在的 .toInt() 抛出 NoSuchMethodError,不会被下面的 on FormatException 捕获。这条风险来自源码推导;模拟器上的四键样本没有包含 expires。

3.2 人工补丁只改原生名称解析

定位清楚后,我只改原生解析:删掉把键值段当响应属性拆解的 switch 分支,让所有包含等号的键值段统一写入 name 和 value,并保留外层分号切分与默认元数据兜底。下面是修复后的完整 parseCookie 及字符过滤辅助方法,中文注释为阅读补充(完整文件见 修后 WebviewCookieManagerPlugin.ets):

  private parseCookie(cookie: string): Map<string, Any> {
    let resultMap: Map<string, Any> = new Map<string, Any>();
    try {
      if (cookie.indexOf('=') != -1) {
        // 后半段重新拼回,保留业务值内部的等号。
        let parts = cookie.split('=');
        let rawName = parts[0].trim(); 
        let rawValue = parts.slice(1).join('='); 

        let name = this.removeInvalidCharacters(rawName);
        let value = this.removeInvalidCharacters(rawValue);

        // fetchCookieSync 返回 Cookie 键值对,不是 Set-Cookie 响应属性。
        // path、domain 等也可以是业务 Cookie 名,必须按原名返回。
        resultMap.set('name', name);
        resultMap.set('value', value);
      }
    } catch (err) {
      // 沿用原有日志与返回约定,异常时仍会继续补齐默认字段。
      Log.e(TAG, "Failed to parse cookie: " + err.message);
    }

    // 删除接口依赖默认域名筛选条目,因此保留这组兼容字段。
    if (!resultMap.has('domain') || resultMap.get('domain') == null) {
      resultMap.set('domain', this.tempDomain);
    }
    resultMap.set('path', resultMap.get('path') || '/');
    resultMap.set('httpOnly', resultMap.get('httpOnly') || false);
    resultMap.set('secure', resultMap.get('secure') || false);

    return resultMap;
  }

  // 沿用原有可打印 ASCII 过滤规则,不在此处扩展输入契约。
  private removeInvalidCharacters(input: string): string {
    if (!input) return '';

    return input.replace(/[^\x21-\x7E]/g, '');
  }

这里的 Any、Log 和 TAG 都来自原文件,tempDomain 是类内已有字段,由外层方法设置。补丁按下面的顺序收敛范围:

  1. 保留原有多等号拼回写法:原版 split('=') 配合 parts.slice(1).join('=') 已经能正确保留 fixture=token==,无需为了换风格重写切分算法。
  2. 保留默认域名等兼容元数据:Dart 侧既有的 removeCookie(currentUrl) 会先调用 getCookies,再比对返回对象的 domain 筛选目标条目;贸然删掉 this.tempDomain 补齐会破坏原有删除逻辑。
  3. 将未定契约的问题留待单独评估:诸如 =orphan 这类无名畸形片段,或带端口 URL 的域提取规则,涉及公开返回契约是否变更,留待单独评估,不在修复同名键的补丁里暗自改写。

3.3 让 Example 观察系统,不替插件修复

诊断页面双通道跨端调用与前置核验时序

图12 双通道时序

观察通道与待测插件职责隔离

诊断工程要分别回答两个独立问题:ArkWeb 到底有没有存下合成数据,以及待测插件有没有把数据原样带回 Dart。为此我在两组宿主的相同位置增加了只读观察插件 CookieObservationPlugin.ets,由同目录的 EntryAbility.ets 在引擎挂载时绑定 pgc_cookie_observation 通道、卸载时清空处理器。

由于纯 Flutter 诊断页没有挂载原生 Web 组件,观察插件在 initialize 中先于平台线程执行 webview.WebviewController.initializeWebEngine();随后的 readRaw 只读取写死的合成常量地址,不接受外部传入任意 URL,失败时通过 result.error 返回明确错误码与系统异常信息,避免把观察通道故障掩盖成空字符串。

先核对系统,再检查插件

完整驱动逻辑位于 诊断页面 main.dart。如图 12 所示,_run 依次执行:

  1. 调用观察通道 initialize 完成 ArkWeb 内核初始化,再通过公开 WebviewCookieManager.setCookies 写入四项合成 Cookie。
  2. 调用观察通道 readRaw 取得未经插件解析器加工的原始串,用独立的 _rawPairs 按首个等号切分并校验 systemPassed。
  3. 先检查系统写入:若 systemPassed 为假,抛出异常并停止,先排查系统写入问题;只有前置通过,才调用 getCookies 校验列表长度与完整键值字典。
  4. 在 finally 块中将四个合成键的过期时间设为 1970 年发起定向清理请求,避免调用全局 clearCookies() 误伤其他数据。

这份 Example 用于核对系统写入和插件回读,不包含账号登录或页面状态恢复。业务项目接入插件时无需复制观察通道;但如果应用同样在首个 WebView 创建之前读写 Cookie,仍须自行确保内核已初始化。

3.4 带着症状和协议线索复核助手方案

复核分工与输入边界

我把人工改动与工具输出分开保存:

  • 原包与社区移植维护者:webview_cookie_manager 上游作者定义了跨端 Dart 接口与 Cookie 映射结构,CPF-Flutter 社区维护者完成了 2.0.7-ohos-1.0.0 的鸿蒙 ArkWeb 初始移植。
  • 本文作者(abaoge):完成同名键丢名复现、原生单方法最小补丁、双通道诊断 Example 与 Mate X7 模拟器实测归档。
  • HarmonyOS Dev Assistant(v0.2.3):作为辅助开发工具,在给定故障症状与协议线索的前提下,对未修改的原版代码执行独立检查并生成修复建议。

人工补丁跑通模拟器对照后,我把未修改的社区稳定标签源码单独复制到另一工作区,在 VS Code 启动 HarmonyOS Dev Assistant 的“三方库鸿蒙化”模式。提示词里没有透露人工补丁写法,但明确给出了空名称现象、四项固定样本、目标文件路径及 RFC 6265 语法区分。因此,这属于有上下文线索的独立代码复核,并非让工具从零排查未知故障的盲测。

向HarmonyOS开发助手输入复核指令

图13 助手输入

我在输入指令中要求助手沿着“原生解析 → MethodChannel 传输 → Dart Cookie 构造”整条路径核查,保持公开 API、通道名及其他平台文件不变,并指出主机侧测试的局限。这些提示缩小了助手需要检查的范围。

HarmonyOS开发助手检查工作区代码

图14 代码检查

HarmonyOS开发助手生成原生修复

图15 助手改码

HarmonyOS开发助手输出诊断报告

图16 助手报告

助手改动与返回路径核查

图 13 至图 16 记录了输入、检查、改码和总结。助手把切分逻辑改写为 indexOf('=') 配合 substring,并加上两道条件:仅当 separatorIndex > 0 且过滤非法字符后的 name.length > 0 时,才往 resultMap 写入 name 与 value;公开源码分支使用的是前面已经验证过的人工补丁。

初看这两行条件,我以为它能滤掉空名。继续顺着返回路径检查,问题还在:

  1. 当输入为 =orphan 等畸形片段时,守卫确实跳过了 resultMap.set('name', ...),但函数并未提前返回空信号,而是继续执行末尾的默认属性填充,返回一个包含 domain、path、httpOnly、secure 的非空 Map。
  2. 外层 getCookies 只判断分号切分后的字符串非空,便无条件执行 resultMap.push(cookieMap)。
  3. 回看 3.1 节的 Dart 代码,result['name'] ?? '' 再次把缺失的名称补成空字符串,构造出 Cookie('', '')。

所以,赋值条件虽然收紧了,公开结果里仍有空名对象。若要处理畸形输入,需要先敲定“静默跳过”还是“显式报错”的接口契约,再把内层返回值、外层数组收集与 Dart 侧过滤联动起来设计。

对照项人工最小补丁助手方案与代码复核结果
合法同名键删除属性分支,原名写回方案方向一致
多等号值保留原有 split + join改用 indexOf + substring
畸形空名称未纳入最小补丁范围加了赋值守卫,但公开结果仍会混入空名
样本一致性模拟器实测使用多等号样本提醒早期主机记录仍为连字符样本
构建与运行证据有独立构建日志与模拟器归档仅报告文字自述,缺完整命令输出与退出码

表5 方案复核

分别核对解释、代码和日志

除了空名守卫外,助手报告还有两处细节值得掰开核对:

  • 文件样本差异提示有价值,但适用范围有限:助手发现早期静态记录 reproduction.json 写的是 fixture-token,而诊断工程使用的是 fixture=token==,这条跨文件比对很有帮助;但它由此推断多等号场景“未被覆盖”,只对应它当时读取的工作区文件,不能推翻此前已完成的人工版模拟器实测。
  • 主机测试不能简单一刀切否定:助手将宿主侧测试一概评价为“只能验证 Mock 映射、无法执行原生解析”。这句批评适用于纯伪造通道返回值的桩测试,却不符合本文的主机探针——探针将真实 ArkTS 源码转译后放进 Node 沙箱执行,能真实跑通 parseCookie 分支并导出 Map,缺的只是鸿蒙 ArkWeb 内核与设备存储环境。

我检查助手方案时,分别核对根因解释、生成代码和运行记录。它在报告中称 HAP 构建与契约映射通过,但归档里缺少完整命令输出和退出码;我能确认的模拟器结果仍对应人工补丁。

助手还指出了带端口 URL 的正则、空 URL 行为和默认安全属性等周边问题,我把它们单独记下,留待确认契约后再处理。

四、踩坑与边界条件

4.1 复现过程里的工程问题

环节观察到的问题处理方式
中文工程路径Hvigor 报 00306003 Invalid project path将完整工程复制到纯 ASCII 路径构建
修后包首次安装报错 9568288,模拟器存储空间不足保留失败轮次记录,清理旧探针包后重跑
纯 Flutter 宿主页面未挂载原生 Web 组件在平台线程调用 initializeWebEngine()
断言设计原版列表长度同样为 4同时断言名称数组与完整键值字典相等

表6 排障记录

首次安装报空间不足的 patched-01 轮次根本没有启动应用,有效运行样本数为零,不能记作插件修复失败;回收模拟器旧测试包后成功跑通的轮次记为 patched-01a。我把两次记录都留在本地实验档案中,方便还原真实排障过程。

开发期间配合使用的主机探针,通过转译真实 WebviewCookieManagerPlugin.ets 源码在 Node 沙箱中执行,仅桩入最底层的 fetchCookieSync 返回串,再把生成的真实 Map 交给 flutter test 消费。在这套主机回归中,原版在“属性同名键”和“大小写变体”两项失败、在“普通多等号值”和“空系统返回”两项通过;人工补丁四项全部通过,同时原包自带的一项单元测试在两组下均保持通过。主机探针适合快速检查解析逻辑是否退化,但不能替代模拟器上的内核读写验证。

4.2 这次没有验证的部分

条件当前边界
path / domain / secureAPI 26 Mate X7 模拟器与畅享 100 Pro Max 真机实测通过
同名键大小写变体主机探针回归通过,未单独录制设备样本
expires / httpOnly 同名键已做源码链路分析,四键设备样本未覆盖
原始作用域与安全属性fetchCookieSync 返回串不含属性,无法逆推
畸形空名、带端口 URL人工最小补丁未改;助手空名守卫亦未闭环
定向过期清理效果仅确认调用返回,未追加二次原始回读
旧版 API、其他平台尚未执行运行回归

表7 验证边界

⚠️ 使用边界:插件返回对象里的默认 secure: false、httpOnly: false 与补齐的 domain 仅用于维持原包跨端数据结构兼容,业务代码切勿将其当作服务端真实安全配置依据。

补丁恢复了合法业务 Cookie 名称和值的回读。单点登录、Token 刷新和跨应用免登需要各自的业务验证。涉及真实鉴权与注销清理的应用,仍须结合具体线上域名、路径作用域与正式证书环境完成测试。

五、结论与可直接复用的建议

这份补丁解决的是鸿蒙端 Cookie 回读丢名:网页使用 path、domain、secure 等业务键时,Flutter 可以重新按原名取得对应值。公开 API 没有变化,已有的读取代码可以保留;它也不要求网页改名,或在业务层按列表位置猜测字段。

5.1 添加固定版本依赖

社区仓库合并并发布包含修复的版本前,可使用个人 Fork 的 fix/ohos-cookie-attribute-names 分支。它包含前文实测的人工补丁与同款 Example;包名仍是 webview_cookie_manager,根目录就是 package。

在应用的 pubspec.yaml 中,将原有同名依赖替换为以下 Git 依赖,再运行 flutter pub get。固定提交可避免分支后续移动影响依赖内容,原社区标签 2.0.7-ohos-1.0.0 不包含修复。

dependencies:
  webview_cookie_manager:
    git:
      url: https://atomgit.com/abaoge/fluttertpc_webview_cookie_manager.git
      ref: 188f99eb9217fb4c82b391c0dc2aac146ad970af

这份实现需要使用支持鸿蒙插件注册的 Flutter 环境;本篇环境见表 4。若应用通过其他 package 间接引入它,还要检查最终依赖锁,确认解析到的是修复提交。仓库保留原有 MIT 许可与三方声明。

5.2 按网页原名读取状态

对于开篇的工作区状态,可以在 WebView 已完成初始化、网页已写入 Cookie 后调用下面的函数。传入实际网页的完整 URL,让系统按这个地址匹配 Cookie,不要把整个 URL 当作 Cookie 的域属性。

import 'package:webview_cookie_manager/webview_cookie_manager.dart';

Future<String?> readWorkspaceCookie(String pageUrl) async {
  final cookies = await WebviewCookieManager().getCookies(pageUrl);
  for (final cookie in cookies) {
    // domain 是网页约定的业务键名,这里不读取 cookie.domain 元数据。
    if (cookie.name == 'domain') return cookie.value;
  }
  return null;
}

这个函数返回匹配到的第一个业务值;没有该键时返回 null,插件调用失败则向调用方抛出异常。页面应分别处理“没有保存工作区”和“读取失败”,不能都显示成默认工作区。如果网页在不同路径下使用了同名 Cookie,应先明确业务选择规则,不能依靠这段示例消除作用域歧义。

💡 补丁不需要额外开关,也不需要复制诊断 Example 的观察通道。应用若在首个 WebView 创建前操作 Cookie,仍要先完成 ArkWeb 内核初始化;本篇的纯 Flutter 诊断页为此做了单独处理。只读已有 Cookie 时直接调用读取接口即可,不必先写入本文的四组测试数据。

回读结果适合取业务名称和值。安全属性与作用域的限制见 4.2 节;注销清理也要单独确认,不能把一次清理调用返回当作数据已经清空。同款 Example、原版对照、构建命令及采样步骤集中在公开仓库的 REPRODUCTION.md,需要追查实现时再查阅。

小结

这个问题最容易漏看的地方,是返回列表的长度一直正确。把系统原始数据和 Flutter 最终拿到的名称逐项对上后,我才确认数据是在插件解析时丢失的。修复后,离线诊断页里的四组业务键值都能原样读回,应用可以继续按网页约定的名称取值。

开发助手给出的空名守卫也提醒了我:局部条件写对了,仍要追到调用方实际收到什么。今后检查这类跨端映射,我会把公开返回值作为判断依据,再回头看中间每层的解释是否成立。

参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐