17 KiB
H5 相关实现梳理
1. 总体概览
项目里和 H5 相关的能力,当前主要分成 3 条线:
- 内嵌 H5 页面承载:使用
webview_flutter打开 H5 页面。 - Flutter 与 H5 双向通讯:通过 JS 注入 +
JavaScriptChannel做交互。 - 外部 H5 / Scheme 回流 App:通过
app_links监听深链。
相关依赖:
webview_flutter: 4.4.2:pubspec.yaml:61app_links: ^6.4.1:pubspec.yaml:130url_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
实际流程:
- 传入
widget.url - 若 URL 已有查询参数则追加
&lang=...,否则追加?lang=... - 使用
WebViewController加载页面 onPageFinished后延迟 550ms,再执行 JS 注入- 同时读取
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 若要主动发消息,建议按下面方式理解:
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(...)。
大致结构如下:
{
"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.decodeComponenttitle直接透传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
因此目前有两种进入方式:
- 直接
WebViewPage(...)- 不传
showTitle时,标题栏默认显示
- 不传
- 通过
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-95lib/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
规则:
item.content == ENTER_ROOM时,直接进房间- 否则只要
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-66lib/app/constants/sc_global_config.dart:7-18lib/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 当前用途
- 拉取当前用户身份,用于决定“我的”页面展示哪些 H5 入口
- 在网络层发生 401 时,用它来复核当前会话是否仍然有效
7.3 身份字段
当前模型里可见的角色字段包括:
agentanchorbdadminsuperAdminbdLeadersuperFreightAgentmanagerfreightAgent
定义位置: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 做了两件事:
- 启动时读取
initialLink - 运行时监听
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.comyumi://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-71android/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 实现来对接,比较稳妥的方式是:
读取鉴权/设备信息
const auth = JSON.parse(window.app.getAccessOrigin())
向 Flutter 发消息
FlutterPageControl.postMessage('close_page')
FlutterPageControl.postMessage('view_user_info:10001')
FlutterPageControl.postMessage('go_to_room:123456')
FlutterPageControl.postMessage('private_chat:10001')
FlutterPageControl.postMessage('uploadImgFile')
选图后读取图片路径
const imageInfo = JSON.parse(window.app.getImagePath())
注意:
window.app.sendToFlutter(...)目前从代码上看不可靠,因为它引用的是FlutterAppwindow.app.getAuth()在选图前后语义不同,不建议 H5 继续把它当成固定鉴权接口
12. 本次检索范围
本次重点检查了以下文件/模块:
lib/modules/webview/webview_page.dartlib/modules/index/main_route.dartlib/shared/tools/sc_banner_utils.dartlib/modules/auth/login_page.dartlib/modules/user/settings/about/about_page.dartlib/modules/home/popular/party/sc_home_party_page.dartlib/modules/user/me_page2.dartlib/modules/chat/activity/message_activity_page.dartlib/modules/chat/noti/message_notifcation_page.dartlib/app/config/app_config.dartlib/app/config/configs/sc_variant1_config.dartlib/app/constants/sc_global_config.dartlib/shared/data_sources/sources/remote/net/api.dartlib/shared/data_sources/sources/remote/net/network_client.dartlib/shared/data_sources/sources/repositories/sc_user_repository_impl.dartlib/shared/business_logic/models/res/sc_user_identity_res.dartlib/services/auth/user_profile_manager.dartlib/shared/tools/sc_deep_link_handler.dartlib/main.dartandroid/app/src/main/AndroidManifest.xmlios/Runner/Info.plisttest/business_logic_strategy_test.dart