chatapp3-flutter/docs/h5-summary.md
NIGGER SLAYER 77ce33bac9 appid
2026-04-15 12:53:01 +08:00

547 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# H5 相关实现梳理
## 1. 总体概览
项目里和 H5 相关的能力,当前主要分成 3 条线:
1. 内嵌 H5 页面承载:使用 `webview_flutter` 打开 H5 页面。
2. Flutter 与 H5 双向通讯:通过 JS 注入 + `JavaScriptChannel` 做交互。
3. 外部 H5 / Scheme 回流 App通过 `app_links` 监听深链。
相关依赖:
- `webview_flutter: 4.4.2``pubspec.yaml:61`
- `app_links: ^6.4.1``pubspec.yaml:130`
- `url_launcher: ^6.3.1` 已安装:`pubspec.yaml:63`,但本次检索未发现当前 `lib/` 下有明确的 H5 主流程调用。
---
## 2. H5 核心承载页
核心文件:`lib/modules/webview/webview_page.dart`
### 2.1 页面职责
`WebViewPage` 是项目里统一的 H5 容器页,负责:
- 加载 H5 URL
- 给 URL 统一追加 `lang` 参数
- 开启 JS
- 注入 `window.app`
- 接收 H5 发给 Flutter 的消息
- 读取 H5 页面的 `document.title`
- 承接标题栏显隐和样式
### 2.2 加载流程
关键代码位置:
- URL 组装:`lib/modules/webview/webview_page.dart:48-53`
- 开启 JS`lib/modules/webview/webview_page.dart:55-58`
- 注册消息通道:`lib/modules/webview/webview_page.dart:58-119`
- 页面完成后延迟注入:`lib/modules/webview/webview_page.dart:132-140`
- 加载请求:`lib/modules/webview/webview_page.dart:147`
实际流程:
1. 传入 `widget.url`
2. 若 URL 已有查询参数则追加 `&lang=...`,否则追加 `?lang=...`
3. 使用 `WebViewController` 加载页面
4. `onPageFinished` 后延迟 550ms再执行 JS 注入
5. 同时读取 `document.title` 作为标题
说明:
- JS 模式为 `JavaScriptMode.unrestricted`
- 延迟注入的注释写明是“等待 Vue3 初始化”
- 页面销毁时会加载 `about:blank``lib/modules/webview/webview_page.dart:150-153`
---
## 3. Flutter 与 H5 的通讯方式
### 3.1 H5 -> Flutter
Flutter 侧注册的 JS Channel 名称是:
- `FlutterPageControl``lib/modules/webview/webview_page.dart:58-60`
H5 发送消息后Flutter 根据字符串内容执行不同动作。
### 已支持的消息协议
| H5 消息 | Flutter 行为 | 代码位置 |
| --- | --- | --- |
| `close_page` | 关闭当前 WebView 页 | `lib/modules/webview/webview_page.dart:62-63` |
| `view_user_info:{userId}` | 跳到个人资料页 | `lib/modules/webview/webview_page.dart:64-75` |
| `go_to_room:{roomId}` | 进入房间 | `lib/modules/webview/webview_page.dart:76-87` |
| `private_chat:{userId}` | 打开私聊会话 | `lib/modules/webview/webview_page.dart:88-110` |
| `uploadImgFile` | 调起图片选择,并把图片路径再注入给 H5 | `lib/modules/webview/webview_page.dart:111-112``184-206` |
| `editingRoom` | 跳转房间管理搜索页 | `lib/modules/webview/webview_page.dart:113-114` |
| `editingUser` | 跳转用户管理搜索页 | `lib/modules/webview/webview_page.dart:115-116` |
### H5 侧当前更稳妥的调用方式
从 Flutter 代码来看,当前真正注册的 channel 只有 `FlutterPageControl`,因此 H5 若要主动发消息,建议按下面方式理解:
```js
FlutterPageControl.postMessage('close_page')
FlutterPageControl.postMessage('go_to_room:123456')
FlutterPageControl.postMessage('view_user_info:10001')
FlutterPageControl.postMessage('private_chat:10001')
```
### 3.2 Flutter -> H5
Flutter 会在页面加载完成后给 H5 注入 `window.app` 对象:`lib/modules/webview/webview_page.dart:156-181`
### 注入的方法
| 方法 | 作用 | 代码位置 |
| --- | --- | --- |
| `window.app.getAccessOrigin()` | 返回一段 JSON 字符串,里面包含鉴权和设备/渠道信息 | `lib/modules/webview/webview_page.dart:162-170` |
| `window.app.getAuth()` | 当前默认等价于 `getAccessOrigin()` | `lib/modules/webview/webview_page.dart:173-175` |
| `window.app.sendToFlutter(data)` | 试图把消息回传给 Flutter | `lib/modules/webview/webview_page.dart:177-180` |
### `getAccessOrigin()` 返回内容
其返回值是 JSON 字符串,不是 JS 对象H5 需要自行 `JSON.parse(...)`
大致结构如下:
```js
{
"Authorization": "Bearer <token>",
"Req-Lang": "<lang>",
"Req-App-Intel": "build=<build>;version=<version>;model=<model>;channel=<channel>;Req-Imei=<imei>",
"Req-Sys-Origin": "origin=<origin>;originChild=<originChild>"
}
```
这些值来自:
- Token`AccountStorage`,见 `lib/shared/data_sources/sources/local/user_manager.dart:23-55`
- 语言、版本、渠道、设备、来源:`SCGlobalConfig`,见 `lib/app/constants/sc_global_config.dart:24-43`
- 设备唯一标识:`SCDeviceIdUtils.getDeviceId()`,见 `lib/shared/tools/sc_deviceId_utils.dart:31-62`
### 图片上传相关注入
当 H5 发出 `uploadImgFile`Flutter 会调起图片选择器,然后重新注入另一组方法:`lib/modules/webview/webview_page.dart:184-206`
这时会新增:
- `window.app.getImagePath()`:返回所选图片本地路径的 JSON 字符串
并且会把:
- `window.app.getAuth()`
改写成:
- 返回 `window.app.getImagePath()`
也就是说,图片选择之后,`getAuth()` 不再表示鉴权信息,而变成了“图片路径”。
---
## 4. H5 路由与承载入口
### 4.1 统一路由
项目提供统一路由:
- 路由常量:`SCMainRoute.webViewPage = '/main/webViewPage'`
位置:`lib/modules/index/main_route.dart:42`
- 路由解析逻辑:
`lib/modules/index/main_route.dart:185-198`
解析行为:
- `url` 会先 `Uri.decodeComponent`
- `title` 直接透传
- `showTitle` 直接从 query 里取字符串
### 4.2 标题栏显隐规则
`WebViewPage` 默认 `showTitle = "true"``lib/modules/webview/webview_page.dart:27-31`
但通过路由进入时:
- `showTitle: params['showTitle']?.first ?? ""``lib/modules/index/main_route.dart:191-195`
因此目前有两种进入方式:
1. 直接 `WebViewPage(...)`
- 不传 `showTitle` 时,标题栏默认显示
2. 通过 `SCMainRoute.webViewPage?...`
- 通常会显式传 `showTitle=false`
- 若未来有人漏传 `showTitle`,最终拿到的是空字符串 `""`,标题栏也会被隐藏
---
## 5. 当前已接入的 H5 页面入口
### 5.1 登录页 / 关于页协议页
这两处是直接 new `WebViewPage`,默认显示标题栏。
| 场景 | URL 来源 | 代码位置 |
| --- | --- | --- |
| 登录页《Terms of Service》 | `SCGlobalConfig.userAgreementUrl` | `lib/modules/auth/login_page.dart:311-324` |
| 登录页《Privacy Policy》 | `SCGlobalConfig.privacyAgreementUrl` | `lib/modules/auth/login_page.dart:350-363` |
| 设置-关于-《Terms of Service》 | `SCGlobalConfig.userAgreementUrl` | `lib/modules/user/settings/about/about_page.dart:80-90` |
| 设置-关于-《Privacy Policy》 | `SCGlobalConfig.privacyAgreementUrl` | `lib/modules/user/settings/about/about_page.dart:112-123` |
### 5.2 首页 Party 榜单 H5
这些入口都走统一路由,并显式 `showTitle=false`
| 入口 | URL 来源 | 代码位置 |
| --- | --- | --- |
| 财富榜 | `SCGlobalConfig.wealthRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:295-300` |
| 房间榜 | `SCGlobalConfig.roomRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:330-335` |
| 魅力榜 | `SCGlobalConfig.charmRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:365-370` |
### 5.3 我的页面角色中心 H5
角色身份先通过 `/app/h5/identity` 拉取,再决定展示哪些入口。
身份拉取:
- `lib/services/auth/user_profile_manager.dart:90-95`
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart:539-551`
角色字段定义:
- `lib/shared/business_logic/models/res/sc_user_identity_res.dart:6-87`
当前角色入口:
| 身份条件 | 打开页面 | 代码位置 |
| --- | --- | --- |
| `anchor && agent` | `agencyCenterUrl` | `lib/modules/user/me_page2.dart:295-308` |
| `anchor && !agent` | `hostCenterUrl` | `lib/modules/user/me_page2.dart:309-322` |
| `!admin && bdLeader` | `bdLeaderUrl` | `lib/modules/user/me_page2.dart:325-339` |
| `!admin && bd` | `bdCenterUrl` | `lib/modules/user/me_page2.dart:340-353` |
| `freightAgent` | `coinSellerUrl` | `lib/modules/user/me_page2.dart:356-370` |
| `admin` | `adminUrl` | `lib/modules/user/me_page2.dart:372-386` |
### 5.4 Banner H5
Banner 打开逻辑在:
- `lib/shared/tools/sc_banner_utils.dart:10-27`
规则:
1. `item.content == ENTER_ROOM` 时,直接进房间
2. 否则只要 `params``http://``https://` 开头,就用内嵌 WebView 打开
注意:
- 这里只校验是不是 URL没有域名白名单
### 5.5 消息中心 / 通知里的外链 H5
只要 `message.extraData.link``http` / `https` 开头,就会直接进入统一 WebView
- 活动消息:`lib/modules/chat/activity/message_activity_page.dart:268-277`
- 通知消息:`lib/modules/chat/noti/message_notifcation_page.dart:280-289`
这两处同样没有域名白名单。
---
## 6. H5 URL 配置清单
配置接口定义在:
- `lib/app/config/app_config.dart:21-66`
- `lib/app/constants/sc_global_config.dart:7-18`
- `lib/app/config/configs/sc_variant1_config.dart:26-71`
当前 `variant1` 中的 H5 URL
| 配置项 | URL | 当前使用情况 |
| --- | --- | --- |
| `privacyAgreementUrl` | `https://h5.haiyihy.com/privacy.html` | 已使用 |
| `userAgreementUrl` | `https://h5.haiyihy.com/service.html` | 已使用 |
| `anchorAgentUrl` | `https://h5.haiyihy.com/apply/index.html` | 本次检索未发现引用 |
| `hostCenterUrl` | `https://h5.haiyihy.com/host-center/index.html` | 已使用 |
| `bdCenterUrl` | `https://h5.haiyihy.com/bd-center/index.html` | 已使用 |
| `bdLeaderUrl` | `https://h5.haiyihy.com/bd-leader-center/index.html` | 已使用 |
| `coinSellerUrl` | `https://h5.haiyihy.com/coin-seller/index.html` | 已使用 |
| `adminUrl` | `https://h5.haiyihy.com/admin-center/index.html` | 已使用 |
| `agencyCenterUrl` | `https://h5.haiyihy.com/agency-center/index.html` | 已使用 |
| `gamesKingUrl` | `https://h5.haiyihy.com/games-king/index.html` | 本次检索未发现引用 |
| `wealthRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Wealth` | 已使用 |
| `charmRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Charm` | 已使用 |
| `roomRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Room` | 已使用 |
| `inviteNewUserUrl` | `https://h5.haiyihy.com/invitation/invite-new-user/index.html` | 本次检索未发现引用 |
---
## 7. `/app/h5/identity` 接口
这个接口是 H5 相关链路里的关键接口。
### 7.1 接口位置
- 未授权校验时直接请求:`lib/shared/data_sources/sources/remote/net/api.dart:49-89`
- 路径映射表:`lib/shared/data_sources/sources/remote/net/network_client.dart:199`
- Repository 包装:`lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart:539-551`
### 7.2 当前用途
1. 拉取当前用户身份,用于决定“我的”页面展示哪些 H5 入口
2. 在网络层发生 401 时,用它来复核当前会话是否仍然有效
### 7.3 身份字段
当前模型里可见的角色字段包括:
- `agent`
- `anchor`
- `bd`
- `admin`
- `superAdmin`
- `bdLeader`
- `superFreightAgent`
- `manager`
- `freightAgent`
定义位置:`lib/shared/business_logic/models/res/sc_user_identity_res.dart:6-87`
---
## 8. 外部 H5 / Deep Link 回流 App
### 8.1 依赖与处理器
深链能力不是走 WebView而是走 `app_links`
- 依赖:`pubspec.yaml:130`
- 处理器:`lib/shared/tools/sc_deep_link_handler.dart:1-61`
`SCDeepLinkHandler` 做了两件事:
1. 启动时读取 `initialLink`
2. 运行时监听 `uriLinkStream`
### 8.2 App 根层接入
`main.dart` 已初始化深链监听:
- 挂载处理器:`lib/main.dart:246`
- `initState` 调用:`lib/main.dart:248-257`
- 深链初始化:`lib/main.dart:266-274`
但是当前真正处理链接的 `_handleLink(Uri uri)` 里只有日志和简单取参:
- `lib/main.dart:276-281`
也就是说:
- 深链基础设施已经接好了
- 但业务路由尚未真正实现
### 8.3 Android 配置
AndroidManifest 里已配置:
- `https://h5.yumi.com`
- `yumi://app`
位置:`android/app/src/main/AndroidManifest.xml:55-64`
### 8.4 iOS 配置现状
本次检索结果里:
- 未发现 `applinks:` Associated Domains
- 未发现针对 `yumi://app` 的明确 `CFBundleURLSchemes`
- `Info.plist``CFBundleURLTypes` 目前只有一个空壳配置:`ios/Runner/Info.plist:38-44`
因此至少从当前代码检索结果看:
- Android 侧有显式深链声明
- iOS 侧未看到与当前 H5 域名 / scheme 对应的完整配置
---
## 9. WebView 样式与测试
WebView 页面样式做了策略抽象:
- 接口定义:`lib/app/config/business_logic_strategy.dart:1451-1491`
- Base 实现:`lib/app/config/strategies/base_business_logic_strategy.dart:2024-2078`
- Variant1 实现:`lib/app/config/strategies/variant1_business_logic_strategy.dart:2003-2057`
测试覆盖位置:
- `test/business_logic_strategy_test.dart:1122-1183`
当前测试主要覆盖:
- 普通 WebView 页面的颜色策略
- 游戏 WebView 页面的颜色策略
- Variant1 对应策略返回值
额外说明:
- `WebViewPage` 里有 `_progressBar(...)` 方法:`lib/modules/webview/webview_page.dart:275-283`
- 但当前 `build` 里没有真正挂载这条进度条
---
## 10. 当前实现里的注意点 / 风险点
### 10.1 JS Channel 名称与注入 helper 名称不一致
Flutter 注册的是:
- `FlutterPageControl`
但注入到 H5 的 helper 写的是:
- `FlutterApp.postMessage(data)``lib/modules/webview/webview_page.dart:178-179``202-203`
本次检索未发现项目里其他地方定义了 `FlutterApp` JS 对象。
这意味着:
- 如果 H5 直接调用 `window.app.sendToFlutter(data)`,大概率会找不到 `FlutterApp`
- 当前更像是 H5 需要自己直接调用 `FlutterPageControl.postMessage(...)`
### 10.2 `uploadImgFile` 会覆盖 `getAuth()`
在初始注入时:
- `getAuth()` 等于 `getAccessOrigin()`
在用户选图后:
- `getAuth()` 被改成 `getImagePath()`
这会导致 H5 侧如果沿用 `getAuth()` 获取鉴权信息,选图后语义会发生变化。
### 10.3 缺少域名白名单,且注入内容包含鉴权信息
当前以下入口只检查“是不是 http/https”没有限制域名
- Banner`lib/shared/tools/sc_banner_utils.dart:18-24`
- 活动消息:`lib/modules/chat/activity/message_activity_page.dart:269-276`
- 通知消息:`lib/modules/chat/noti/message_notifcation_page.dart:281-288`
`WebViewPage` 会对已加载页面统一注入:
- Bearer Token
- 语言
- 设备标识
- 构建版本
- 渠道和来源信息
因此当前实现等于:
- 只要某个外链能被塞进 Banner 或消息链接
- 该页面理论上就能拿到 `window.app.getAccessOrigin()` 这组数据
这是当前 H5 链路里最需要重点关注的安全点。
### 10.4 Android 深链域名与业务配置域名不一致
当前业务配置里的 H5 域名主要是:
- `h5.haiyihy.com`
但 AndroidManifest 里声明的可验证 Host 是:
- `h5.yumi.com`
位置分别在:
- `lib/app/config/configs/sc_variant1_config.dart:26-71`
- `android/app/src/main/AndroidManifest.xml:59-64`
如果期望 H5 页面通过系统级深链回流 App这个域名不一致需要单独确认。
### 10.5 Deep Link 已接入,但业务处理未完成
`SCDeepLinkHandler` 已经能收到链接,但 `main.dart` 里的 `_handleLink` 还没有落业务跳转逻辑。
当前状态更接近:
- “能收到”
- “还没消费”
### 10.6 `lang` 参数是无条件追加
当前 URL 处理逻辑只判断是否已有 `?`,不会判断原 URL 是否已经存在 `lang` 参数:
- `lib/modules/webview/webview_page.dart:48-53`
如果某些 H5 页面本来就自带 `lang`,最终可能出现重复参数。
---
## 11. 给 H5 同学的当前可参考约定
如果只根据当前 Flutter 实现来对接,比较稳妥的方式是:
### 读取鉴权/设备信息
```js
const auth = JSON.parse(window.app.getAccessOrigin())
```
### 向 Flutter 发消息
```js
FlutterPageControl.postMessage('close_page')
FlutterPageControl.postMessage('view_user_info:10001')
FlutterPageControl.postMessage('go_to_room:123456')
FlutterPageControl.postMessage('private_chat:10001')
FlutterPageControl.postMessage('uploadImgFile')
```
### 选图后读取图片路径
```js
const imageInfo = JSON.parse(window.app.getImagePath())
```
注意:
- `window.app.sendToFlutter(...)` 目前从代码上看不可靠,因为它引用的是 `FlutterApp`
- `window.app.getAuth()` 在选图前后语义不同,不建议 H5 继续把它当成固定鉴权接口
---
## 12. 本次检索范围
本次重点检查了以下文件/模块:
- `lib/modules/webview/webview_page.dart`
- `lib/modules/index/main_route.dart`
- `lib/shared/tools/sc_banner_utils.dart`
- `lib/modules/auth/login_page.dart`
- `lib/modules/user/settings/about/about_page.dart`
- `lib/modules/home/popular/party/sc_home_party_page.dart`
- `lib/modules/user/me_page2.dart`
- `lib/modules/chat/activity/message_activity_page.dart`
- `lib/modules/chat/noti/message_notifcation_page.dart`
- `lib/app/config/app_config.dart`
- `lib/app/config/configs/sc_variant1_config.dart`
- `lib/app/constants/sc_global_config.dart`
- `lib/shared/data_sources/sources/remote/net/api.dart`
- `lib/shared/data_sources/sources/remote/net/network_client.dart`
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart`
- `lib/shared/business_logic/models/res/sc_user_identity_res.dart`
- `lib/services/auth/user_profile_manager.dart`
- `lib/shared/tools/sc_deep_link_handler.dart`
- `lib/main.dart`
- `android/app/src/main/AndroidManifest.xml`
- `ios/Runner/Info.plist`
- `test/business_logic_strategy_test.dart`