多标签浏览 dev 5.2.x 尝鲜 main 4.1.x 单页
版本边界(以 qxj_harmony_next_pad_nsp_browser 仓库为准)
- 交付版 main 4.1.x(4.1.54,versionCode 4001054):单页单标签,一个常驻 Web 组件,甲方实际验收形态。
- 实验版 dev 5.2.x(5.2.89,versionCode 5002089):多标签 v2(离线 Web 组件方案),并附带一批实验性浏览能力;仍有未关闭缺陷,未交付甲方。
- 两条分支的共同祖先为
06f5f3d;main 最新提交27e51a6。
1. 背景:为什么交付版是单页
多标签能力在两条分支上的取舍,是本项目最典型的一次「需求—平台约束—交付节奏」平衡:
- 平台约束:HarmonyOS 的 ArkWeb 内核对同时存活的 WebView 实例数量敏感。实测在同一应用内创建超过 10 个 Web 组件后,系统会回收后台实例的页面数据,切回时出现白屏、表单与滚动位置丢失等问题,无法通过应用层配置解除。
- 甲方诉求:项目交付目标是让业务人员稳定访问气象业务系统(Web 前端 + JSBridge 鉴权),并不要求通用浏览器的多标签形态;在专网平板上,稳定性与可维护性的优先级远高于标签数量。
- 版本决策:main 分支在 v4.1.16 从早期多标签实现回退为单页模式,并围绕单页把浏览、收藏、历史、主题、鉴权等能力做扎实;多标签则在 dev 分支以 v2 方案(离线 Web 组件)重新立项,作为后续演进方向尝鲜验证。
因此当前版本格局是:main 4.1.x 单页交付,dev 5.2.x 多标签尝鲜。
2. 两版能力总览
| 能力 | main 4.1.x(交付) | dev 5.2.x(实验) |
|---|---|---|
| 浏览形态 | 单页单标签,一个常驻 Web 组件 | 多标签,离线 Web 组件,最多 9 个 |
| 前进 / 后退 / 刷新 / 主页 | 有 | 有(按标签独立) |
| 地址栏与加载进度 | 有 | 有,新增地址联想 |
| 历史记录 | 有,上限 200 条 | 有,无痕标签不写入 |
| 收藏夹 | 固定 3 条 + 用户收藏,上限 100 | 沿用 |
| 搜索引擎 | 5 个预设可切换 | 沿用 |
| UA 切换 | mobile / pc | 沿用 |
| 网站访问控制 | 有(当前策略随版本固化) | 沿用,入口并入浏览器菜单 |
| 标签管理(标签条 / 标签页) | — | TabStripView / TabManagerView |
| 会话恢复 | — | 启动恢复上次标签组 |
| 无痕浏览 | — | 独立分区,不留历史/Cookie |
| 广告 / 跟踪拦截 | — | 约 80 条域名黑名单,默认开启 |
| 下载中心 | — | 系统下载代理,公共 Download 目录 |
| 页内查找 | — | Chrome 风格查找条 |
| 阅读模式 | — | 正文提取,字号 14~24 |
| 页面二维码 | — | 当前 URL 生成二维码 |
| 自定义错误页 | — | 主帧错误覆盖页 |
| 开发者工具 | — | 控制台 / Cookie / 存储 / 源码 |
| 用户脚本 | — | 油猴风格,文档起止注入 |
| 无图 / 禁用 JS | — | 有 |
4. 交付版(main 4.1.x · 甲方交付)
4.1 单页浏览架构
交付版采用单 Web 组件常驻的简单架构:
- 页面主体是一个声明式
<Web>组件,直接挂在组件树中随BrowserView同生命周期存活,不存在动态创建、卸载与重挂过程。 - 外层通过
Tabs承载「浏览器 / 设备」等业务分区,浏览器只是其中一个 Tab 页;所有浏览能力围绕这一个 Web 实例构建。 - Web 的属性(UA、暗色、访问拦截、JSBridge 等)与事件回调(加载开始/结束、进度、标题、SSL 错误、主帧错误等)集中在
BrowserView中绑定,状态单向流入 AppStorage,UI 通过@StorageLink响应。
这套架构的优势是确定性高:不存在实例复用、挂载时序、内核状态丢失等问题,冷启动直接加载业务首页,行为可预测、易测试,适合专网交付。
4.2 基础浏览能力
围绕单页实例提供完整的基础浏览闭环:
- 导航控制:后退、前进(基于内核历史栈,自动置灰不可用状态)、刷新、停止加载;主页按钮回到「新标签页」(
NewTabBuilder,内置导航入口),而非直接跳转第三方站点。 - 地址栏:实时显示当前 URL;支持输入 URL 或搜索词,按搜索引擎前缀拼接后加载;加载过程中展示进度条与当前地址。
- 标题与图标联动:页面标题通过
onTitleReceive回写 AppStorage,供收藏、历史、窗口标识使用。 - SSL 风险处理:证书异常时由
handleSslError处理,对可信内网/自签名场景提供确认通道,对真正高风险连接给出拦截提示,避免用户在无感知状态下进入风险页面。 - JSBridge 注入:Web 上注册 6 个桥方法(
login、getAccessToken、refreshAccessToken、sm4GcmEncrypt、sm4gcmDecrypt、logout),origin 白名单由应用侧checkOriginAllowed校验,只有受信业务页面能拿到令牌与加解密能力。
4.3 历史记录
- 每次页面加载完成写入一条记录(标题、URL、访问时间),持久化在首选项键
history_json,并通过 AppStorage 键browser_history_list驱动历史页响应式刷新。 - 上限 200 条,超出后淘汰最旧记录,防止长期使用后存储膨胀与列表渲染变慢。
- 历史页点击条目即跳转;内部维护
pending_url机制处理「Web 尚未就绪时先记录 URL、就绪后补加载」的时序问题。 - 历史入口位于工具栏(浏览器设置按钮左侧)。
4.4 收藏夹
收藏采用「固定收藏 + 用户收藏」两级结构:
- 固定收藏 3 条(
fixed=true,不可删除):应用中心(apps.nsp.ac.cn/appstore)、国家卫星气象中心(nsmc.org.cn)、中科院信息工程研究所(iie.ac.cn),保证常用官方入口永远可达。 - 用户收藏上限 100 条:通过地址栏星标或快捷操作加入,按 URL 去重;持久化在首选项,收藏列表通过 AppStorage 与工具栏星标实时联动(当前页已收藏时星标高亮)。
4.5 网站访问控制
访问控制在 Web 的 onLoadIntercept 中实现,对主帧与子资源请求按域名规则判定是否放行:
- 匹配规则:取 URL host 做域名后缀匹配——
host === rule或host以.rule结尾即命中;仅拦截 http/https 请求,自定义 scheme 不拦截。 - 当前交付口径:以代码实际实现为准,策略随版本固化(开关与黑白名单由版本配置决定),设置页用于展示当前访问规则与拦截情况。
- 命中规则的请求在拦截器内直接阻断,不进入网络栈。
说明:早期 README 对访问控制有「默认关闭、用户可管理黑白名单」的描述,与交付代码不一致。交付文档以代码行为为准。
4.6 搜索引擎与 UA 切换
- 搜索引擎:内置 Bing、百度、Google、头条、DuckDuckGo 共 5 个预设,选择后将搜索前缀持久化(
search_prefix,默认 Bing),地址栏非 URL 输入按该前缀发起搜索。 - UA 切换:提供 mobile / pc 两档。mobile 使用 Android 13、Pixel 7、Chrome/120 的移动端 UA;pc 使用 Windows 10、Chrome/120 的桌面端 UA,用于应对部分站点按 UA 强制跳转或降级的问题。选择持久化(
webview_mode,默认 mobile)。
5. 实验版(dev 5.2.x · 未交付)
5.1 多标签 v2:离线 Web 组件机制
dev 分支多标签的核心是 [features/browser/src/main/ets/view/OffscreenWeb.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/features/browser/src/main/ets/view/OffscreenWeb.ets),思路是让 Web 组件脱离声明式组件树、以命令式方式创建并长期存活:
- 命令式创建:通过
BuilderNode承载一个完整的OffscreenWebHost自定义组件(Web 的全部属性与事件都在该组件上声明),Web 在首次需要时才真正实例化。 - NodeController 挂载:每个标签槽位持有一个
OffscreenNodeController,其makeNode返回该标签的FrameNode(挂载)或null(卸载)。槽位只是一个「挂载点」,与 Web 实例解耦。 - 全局唯一挂载:任意时刻只有当前标签的 Web 被挂载到界面上。切换标签时:新槽位
makeNode返回已有 FrameNode → 旧槽位makeNode返回 null;先挂新、再卸旧,避免切换瞬间白屏。 - 内核状态保留:卸载只把节点移出组件树,并不销毁内核,因此后台标签保留滚动位置、历史栈、Cookie/会话与已填表单;这与「销毁重建」式方案有本质区别。
- 媒体挂起:通过
onActive()/onInactive()通知内核,后台标签暂停音视频播放与高开销任务,回到前台再恢复。 - 释放顺序:关闭标签时先解绑事件引用,再以
setTimeout(0)异步dispose(),避免在事件回调栈中同步销毁内核引发崩溃。
三个类各司其职:OffscreenWebHost(Web 属性与事件载体)、OffscreenNodeController(每槽位一个的挂载控制器)、OffscreenWebManager(BrowserView 持有,以 Map<tabId, controller> 管理全部标签)。
5.2 标签生命周期与上限
- 新建:创建新的 controller 与槽位,Web 延迟到首次激活时才实例化,首屏为
about:blank;window.open/ 页面target=_blank也会归一化为新建标签。 - 切换:仅改变挂载指向,不重载页面;地址栏、进度、标题随激活标签切换数据源。
- 关闭:关闭最后一个标签时自动补一个新标签页,保证浏览器始终有可用页面。
- 上限 9 个:针对「ArkWeb 超过 10 个实例后系统回收后台数据」的平台约束,应用层主动把标签数限制在 9 个并在 UI 上提示,把不可控的系统回收变成可预期的产品行为。
5.3 标签 UI:标签条与标签管理页
dev 按设备形态提供两套标签交互:
- 平板 / 2in1 — TabStripView:顶部横向标签条,以「芯片」形式展示各标签的标题/图标与关闭按钮,当前标签高亮,支持点击切换;数据来自浏览器状态层。
- 手机 — TabManagerView:全屏标签管理页,以卡片网格纵向排列各标签快照,支持切换、新建、批量关闭。
- 标签视图与内核解耦:UI 只消费标签快照(标题、URL、缩略状态),快照经 AppStorage 下发,避免标签页直接持有 Web 引用。
5.4 无痕浏览
IncognitoModeUtil提供全局无痕开关(incognito_mode,默认关闭)。- 开启后新标签使用 ArkWeb 的独立无痕分区(
incognitoMode),与正常标签的 Cookie / 存储隔离;无痕标签不写历史记录,关闭后其分区数据随内核释放。
5.5 广告与跟踪拦截
AdBlockUtil内置约 80 条广告 / 统计跟踪域名的后缀黑名单(覆盖 doubleclick、google-analytics、百度 cpro/hm.baidu、cnzz、51.la 等),默认开启(adblock_enabled)。- 在页面加载拦截链路中命中即阻断请求,并累计拦截计数(
browser_adblock_count),供界面展示拦截成效。
5.6 下载中心
BrowserDownloadManager为单例,对每个 WebviewController 安装同一个WebDownloadDelegate,统一接管onBeforeDownload、onDownloadUpdated、onDownloadFinish、onDownloadFailed回调。- 下载文件落到系统公共 Download 目录(
environment.getUserDownloadDir),可被系统文件管理器直接访问;新增READ_WRITE_DOWNLOAD_DIRECTORY权限。 - 内部以
Map<guid, WebDownloadItem>持有内核任务句柄,并生成DownloadSnapshot(文件名、URL、MIME、已收/总字节、速度、状态 pending/inprogress/paused/completed/failed/canceled)下发给 UI;下载弹层展示任务列表与进度,支持暂停 / 继续 / 取消。
5.7 页内查找、阅读模式与页面二维码
- 页内查找(FindBarView):Chrome 风格查找条悬浮于页面顶部,输入关键词即时高亮命中,显示「当前 / 总数」,可上一处 / 下一处跳转;0 命中时计数变红。
- 阅读模式(ReaderView):提取正文重新排版,去除广告与导航噪声,字号 14~24 可调,适合长文阅读。
- 页面二维码(QrCodeView):读取当前标签 URL 生成二维码并附 URL 文本,支持用手机扫码把当前页面接力到其他设备。
5.8 自定义错误页
ErrorPageView 在主帧发生断网、DNS 失败、连接拒绝等错误时覆盖展示:呈现错误码与错误描述,并提供「重试」「返回首页」操作,取代内核默认的生硬错误白屏,配色取自当前主题与界面色板。
5.9 开发者工具
DevToolsView 是参考 vConsole 思路的独立全屏调试页,含四个页签:
- 控制台:采集 Web
onConsole的console.*日志与未捕获异常,按级别过滤,经 AppStorage tick 驱动实时刷新; - Cookie:通过
WebCookieManager.fetchCookieSync读取当前标签域 Cookie,支持清空; - 存储:经
runJavaScript读取 localStorage / sessionStorage 键值; - 源码:读取
document.documentElement.outerHTML(上限 20 万字符,超出截断以防卡顿),支持复制。
当前标签的 WebviewController 与 URL 经 DevToolsBridge 从 BrowserView 获取。
5.10 用户脚本
UserScriptStore 提供油猴风格的用户脚本能力:脚本结构包含 id、name、code、enabled、matchRules、runAt(start / end),持久化于 user_scripts_json;通过 Web 的 javaScriptOnDocumentStart / javaScriptOnDocumentEnd 注入,并配套脚本管理页与编辑器页(手机、平板各一套)。
5.11 无图模式与禁用脚本
在离线 Web 宿主上可独立控制 imageAccess(无图模式,省流提速)与 javaScriptAccess(禁用 JS),配合无痕、广告拦截形成面向弱网 / 高安全场景的一组浏览开关。交付版这两项为固定开启状态。
5.12 会话恢复
BrowserSessionStore 在退出或被回收前保存标签组快照(entries 与 activeIndex,持久化于 session_json),下次启动按快照重建标签并恢复上次激活位置,实现「关掉再开,标签还在」。
5.13 已知问题(未交付的直接原因)
dev 分支保留着一份处于 OPEN 状态的启动空白问题记录(debug-startup-web-blank.md):
- 症状:首次打开应用时地址栏已显示业务前端 URL,但网页区域黑色无内容、标签标题停留在「新标签页」;有历史会话时标题可恢复但网页同样不加载;而手动新开标签页可正常显示
about:blank。 - 排查方向:
loadActiveTab的loadUrl异常未被兜底、会话恢复时序、请求被拦截器误拦、Web 可见性状态未翻转、主帧错误未上错误页等,均已埋点待证伪。
这类「冷启动首标签不渲染」的时序问题,正是离线 Web 方案复杂度的体现,也是 dev 多标签暂不具备交付条件、甲方版本继续使用 main 单页的直接原因。
6. 设计要点回顾
- 按交付目标做减法:在平台约束明确(10 实例回收)、甲方无标签诉求的前提下,果断回退单页,把资源投入鉴权、稳定性与主题体验,是务实的交付决策。
- v2 方案的关键创新:以
BuilderNode+NodeController让 Web 脱离组件树存活,用「唯一挂载 + 内核保留」兼顾多标签体验与实例数量限制;先挂新再卸旧、异步释放、后台媒体挂起等细节保证切换顺滑。 - 把平台限制产品化:9 标签上限将系统不可控的后台回收转化为用户可理解的明确约束。
- 复杂度与风险匹配:多标签附带的无痕、下载、会话恢复等能力显著提升了调试与维护成本,因此全部隔离在 dev 分支验证,不污染交付主线。