鸿蒙应用架构
QXJ 鸿蒙安全浏览器(风四C星浏览器)基于 HarmonyOS NEXT 的「一多」(一次开发、多端部署)架构: 5 个设备形态 entry HAP + 5 个功能 HAR + 1 个公共 HAR;ArkUI 状态管理 V1(@Component) 与 V2(@ComponentV2)混用;国密 SM2/SM3/SM4 能力沉淀在独立 native HAR,通过 NAPI/AKI 暴露给 ArkTS。
关键工程信息:
| 项 | 值 |
|---|---|
| Bundle Name | cn.ac.nsp.qxj.nsp_browser |
| 版本 | versionName 4.1.54 / versionCode 4001054 |
| targetSdkVersion | 6.1.1(24) |
| compatibleSdkVersion | 5.0.5(17) |
| runtimeOS | HarmonyOS(纯血鸿蒙,无 AOSP 兼容) |
| 工程模型 | StageMode + 多 HAR + 多 HAP |
| 严格模式 | caseSensitiveCheck + useNormalizedOHMUrl |
工程目录结构
qxj_harmony_next_pad_nsp_browser/
├── AppScope/ # 应用级配置:app.json5 + 全局图标/字符串
├── common/ # 公共 HAR(@ohos/common)
│ ├── Index.ets # 统一出口:MVVM 基类 / 工具 / 常量 / DEBUG
│ ├── BuildProfile.ets # 构建期生成:DEBUG、BUILD_MODE_NAME、TARGET_NAME
│ ├── consumer-rules.txt # 对外(消费方)混淆规则
│ └── src/main/
│ ├── ets/constant/ # 2 个常量类
│ ├── ets/model/ # 3 个数据模型
│ ├── ets/util/ # 16 个工具类
│ ├── ets/viewmodel/ # MVVM 三件套基类
│ └── resources/ # 7 语言目录
├── features/ # 功能 HAR
│ ├── browser/ # @ohos/browser 浏览器核心(9 视图 + 7 工具)
│ ├── device/ # @ohos/device 设备注册/服务器配置(MVVM)
│ ├── qxj_sdk/ # @nsp/qxj-sdk 国密 native 能力(C++/NAPI)
│ ├── helloworld/ # @ohos/helloworld NAPI 教学示例
│ └── showcase/ # @ohos/showcase 组件演示(debug/release 双源)
├── products/ # 设备形态 entry HAP
│ ├── phone/ # 手机:22 页面 + EntryAbility
│ ├── tablet/ # 平板:22 页面 + EntryAbility(交付主力)
│ ├── pc/ # 2in1:骨架(MainPage + PcAbility)
│ ├── tv/ # 智慧屏:骨架(MainPage + TvAbility)
│ └── wearable/ # 手表:骨架(MainPage + WearableAbility)
├── hpack/ # hpack 打包工具工作目录(sign 签名材料 + webpage 分发页)
├── build-profile.json5 # 工程级构建配置(签名/Product/模块清单)
├── oh-package.json5 # 工程级依赖
├── code-linter.json5 # ArkTS 静态检查规则
├── .clang-tidy / .clangd # C/C++ 静态检查与 clangd 配置
└── hvigorfile.ts # Hvigor 构建脚本入口2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
模块分层与依赖
依赖的关键事实:
- products 通过
oh-package.json5的file:引用全部 5 个 features,另含三方包@pura/harmony-utils、@pura/picker_utils和高德@amap/amap_lbs_common/location/map3d; @ohos/common反向依赖@nsp/qxj-sdk(工具类直接调用 native 加解密),因此 qxj_sdk 处于依赖链最底层;- qxj_sdk 自身只依赖
@ohos/aki(C++/JS 绑定框架)和两个预编译国密.so,不依赖任何业务 HAR。
Products:设备形态层
五种设备形态对比
| 特性 | Phone | Tablet | PC | TV | Wearable |
|---|---|---|---|---|---|
| module deviceTypes | phone | tablet | 2in1 | tv | wearable |
| 入口 Ability | EntryAbility | EntryAbility | PcAbility | TvAbility | WearableAbility |
| 页面数 | 22 | 22 | 1(骨架) | 1(骨架) | 1(骨架) |
| native 库 | 5 个 .so | 5 个 .so | — | — | — |
| 完成度 | ✅ 完整 | ✅ 完整(交付主力) | 🚧 骨架 | 🚧 骨架 | 🚧 骨架 |
phone/tablet 内置的 5 个 native 库(src/main/cpp/libs/arm64-v8a/): libaki_jsbind.so(AKI 绑定)、libc++_shared.so、libhandshake.so(qxj_sdk 的构建产物)、 libnspsm2handshake.so、libnspsmapi.so(预编译国密原语)。
签名 Product 与构建 Target:两套正交概念
容易混淆
build-profile.json5 里有 3 个 Product(default/appstore/appgallery),区别只在签名材料; 而 5 个设备形态模块是目录 products/ 下的不同 HAP。两者是正交概念。
// 工程 build-profile.json5(节选)
{
"app": {
"signingConfigs": [
{ "name": "appstore", "material": { "certpath": "hpack/sign/nsp_browser.cer",
"profile": "hpack/sign/nsp_browserRelease.p7b", "signAlg": "SHA256withECDSA" } },
{ "name": "appgallery", "material": { "profile": "hpack/sign/nsp_browser_test_releaseRelease.p7b" } },
{ "name": "default", "material": { } } // DevEco 自动生成的调试证书(~/.ohos/config)
],
"products": [
{ "name": "appstore", "signingConfig": "appstore" },
{ "name": "appgallery", "signingConfig": "appgallery" },
{ "name": "default", "signingConfig": "default" }
]
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
default:本地调试,使用 DevEco 自动生成、位于~/.ohos/config/的调试证书;appstore:企业应用商店分发,配套nsp_browser.cer+nsp_browserRelease.p7bProfile;appgallery:华为应用市场(测试)分发,配套nsp_browser_test_releaseRelease.p7b;- 证书库口令在配置文件中以加密形式保存(
000000...前缀),非明文密码。
phone/tablet 模块内部还定义了 2 个 target,通过 buildModeBinder 与构建模式绑定:
// products/phone/build-profile.json5(节选)
{
"buildModeBinder": [
{ "buildModeName": "debug", "mappings": [{ "targetName": "default" }] },
{ "buildModeName": "release", "mappings": [{ "targetName": "release" }] }
],
"targets": [
{ "name": "default", "source": { "sourceRoots": ["./src/debug"] } },
{ "name": "release", "source": { "sourceRoots": ["./src/release"] } }
]
}2
3
4
5
6
7
8
9
10
11
src/debug/ 与 src/release/ 是两套平行源码(各含一个 AMapPanel.ets),release 构建还会 排除高德 3D 地图 HAR 资源及其全部 .so:
// release 构建选项(节选)
{
"resOptions": { "excludeHarRes": ["@amap/amap_lbs_map3d"] },
"nativeLib": { "filter": { "select": [
{ "package": "@amap/amap_lbs_map3d", "excludePattern": ["**/*.so"] }
] } }
}2
3
4
5
6
7
22 个页面清单
phone/tablet 的 main_pages.json 注册了 22 个页面,按职责分组:
| 分组 | 页面 |
|---|---|
| 入口 | MainPage(@Entry)、LoginPage |
| 设备注册流程 | DeviceRegisterPage、ServerConfigPage、ServerEditPage、KeyNegotiationPage |
| 浏览器功能 | BrowserMenuPage、BrowserHistoryPage、BrowserFavoritePage、SearchEngineSettingsPage、SiteAccessSettingsPage、SiteInfoPage、JsBridgePermissionPage、AboutPage |
| 调试/演示 | DebugInfoPage、Sm2HandshakeDemoPage、HelloWorldPage |
| Showcase | ShowcaseIndexPage、ShowcaseLoggerPage、ShowcaseStateViewPage、ShowcaseDialogPage、ShowcaseBreakpointCardPage |
EntryAbility 生命周期
[EntryAbility.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/products/phone/src/main/ets/entryability/EntryAbility.ets) 承担应用生命周期管理,每个回调都有明确职责:
要点说明:
- onCreate:设置全局 colorMode 为
COLOR_MODE_NOT_SET(主题由应用内四套设置自行管理), 并通过applicationContext.on('abilityLifecycle', ...)注册ExitLifecycleCallback; - onWindowStageCreate:采用非全屏布局(内容从状态栏下方开始、挖孔自动避让), 底部导航条背景透明、图标深色以悬浮在内容之上;
loadContent成功回调里才初始化 PersistentStorage(依赖 UI 实例,过早调用会持久化失败)、高德定位 SDK,并执行冷启动残留凭据清理; - onForeground/onBackground:回前台时重新确保状态栏与导航条可见;若应用曾进入后台, 弹「欢迎回来」提示(带防叠加标志位);
- onDestroy:系统有序回收后台应用时会走此回调,尽力向服务器补发 token 拉黑请求并清除本地凭据; 强杀(划掉任务/force-stop)没有任何回调,由冷启动时的
cleanupStaleTokensOnLaunch兜底。
MainPage:debug 与 release 两种形态
MainPage 是唯一的 @Entry 页面,根据构建期常量 DEBUG 呈现完全不同的形态:
- debug 构建:底部双 Tab(浏览器 + 示例列表),示例列表提供「全流程」入口和 7 个调试入口; 「全流程」调用
DeviceStatusUtil.checkDeviceStatus()按注册步骤自动路由到对应页面; - release 构建:无 Tab,整页仅承载
BrowserView,用户看不到任何调试入口。
debug 的底部 Tab 使用了 HarmonyOS 新材质组件 HdsTabs:悬浮液态玻璃条 (barFloatingStyle + IMMERSIVE 材质,需 distributionOSApiVersion >= 6.0.1), 底部避让手势导航条,并可通过小圆球收起/展开,避免遮挡网页按钮;不支持新材质的设备自动回退为默认样式。
common:公共 HAR
@ohos/common 是全部业务模块的基础设施,入口 [Index.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/Index.ets) 统一导出 MVVM 基类、16 个工具类、常量与 DEBUG 开关。
16 个 util 工具类
| 工具类 | 职责 |
|---|---|
| Logger | 基于 hilog 封装(domain 0xFF00,前缀 [NSPBrowser]),debug/info/warn/error 四级 |
| HttpUtil | 基于 @ohos.net.http 封装:JSON POST、带鉴权 POST、原始二进制协议帧 POST;识别证书校验错误(code 2300060)并抛 CertValidationError;API 18+ 支持用户知情确认后 remoteValidation='skip' |
| QxjFrameUtil | 64B 自定义协议帧编解码器,与后端 apps/keymgr/utils/packet_parser.py 保持同步;含组帧/解析/describeFrame 逐字段诊断 |
| SM2HandshakeUtil | libhandshake.so 的 NAPI 包装:buildInitPacket/buildAckPacket 及 RESP/TOKEN 帧解析 |
| KeyNegotiationUtil | SM2 四来回密钥协商编排:INIT→RESP→ACK→TOKEN,进度回调,失败自动清除半成品凭据 |
| DeviceKeyUtil | 设备身份核心:SM2 密钥对生成(cryptoFramework SM2_256)、设备 ID(OAID 规则)、关键资产读写、登录令牌持久化、会话密钥清理 |
| DeviceRegisterUtil | 扫码内容解析(server_id#timestamp#server_pubkey)、设备配置文件导出(.dat)、服务器配置 JSON 导入/导出 |
| DeviceStatusUtil | 设备进入第一站:注册状态六步检查(DeviceRegisterStep 0~5)、会话密钥状态严格校验(防脏数据误判) |
| LoginUtil(EncryptedLoginUtil) | 加密登录五步流程,对应后端 apps/auth/views.py;登录明文 SM4-GCM 加密成帧,响应按加密帧/JSON 回退分发 |
| AuthApiUtil | 非加密认证接口:/api/v3/token/refresh/ 刷新、/api/v3/user/logout/ 登出,返回带 HTTP 码与原文的结构化结果 |
| JsBridgeHandle | 暴露给网页的原生能力:6 个方法,统一错误码,应用侧 origin 白名单校验,登录缺失凭据时自动协商 |
| JsBridgePermissionUtil | JSBridge 源白名单:取当前选中服务器的 4 个 URL 解析去重,fail-closed;提供 DEBUG 诊断脚本 |
| LocationUtil | 高德单次定位封装:申请权限→定位→5 秒超时兜底,返回 经度,纬度;任何失败降级为空串 |
| BrowserUiModeUtil | 浏览器自身 UI 亮/暗色板(13 个颜色字段),与网页暗色互相独立 |
| BreakpointSystem | 断点工具:按宽度返回 sm(<600vp)/md(600~840)/lg(>840),BreakpointType<T> 泛型取值 |
| ExitLifecycleCallback | 单例生命周期回调:onAbilityWillDestroy(API 12+)时弹窗提醒先在网页退出登录;主动退出流程可停用避免二次弹窗 |
model 与 constant
model(3 个):
- [DeviceModels.ets](file:///c:/Users/etsuyou/Desktop/qxj-project/qxj_harmony_next_pad_nsp_browser/common/src/main/ets/model/DeviceModels.ets):
SM2KeyPair、ServerQrInfo、DeviceRegisterStep(0 检查状态 / 1 待扫码 / 2 待生成密钥 / 3 待导出 / 4 可协商 / 5 可进浏览器)、DeviceRegisterStatus、SessionKeyStatus; - ServerConfig.ets:服务器配置模型(name + 鉴权 backend/frontend + 业务 backend/frontend + 扫码绑定的 serverId/serverPubKey),内置多套预置服务器与
PersistentStorage持久化注册; - GlobalInfoModel:运行时全局信息(宽高断点、状态栏/导航条高度、是否侧边栏布局、colorMode、窗口宽高)。
constant(2 个):
- CommonConstants:应用名「风四C星浏览器」、Tab 高度、主页按钮跳转地址(应用商店
https://apps.nsp.ac.cn/appstore)、全局存储键; - DeviceConstants:定义两套凭据存储键体系——
| 存储体系 | 类 | 生命周期 | 用途 |
|---|---|---|---|
关键资产存储(Asset Store Kit,AssetUtil) | DeviceAssetKey | 卸载保留(设备 SM2 钥)/ IS_PERSISTENT=false(会话钥) | 设备 SM2 公私钥、服务器公钥/ID、K_client、Token |
| 持久化存储(PersistentStorage → AppStorage) | DeviceStorageKey | 卸载清除 | 服务器配置列表(_v2 后缀避旧结构冲突)、当前索引、导出标记、access/refresh token |
持久化对象的两个坑
- PersistentStorage 反序列化会丢失类方法,不能调用
c.isBound(),必须用模块级函数isServerBound(c); 旧数据可能缺字段(undefined),要用真值判断而非!== ''。 - PersistentStorage 依赖 UI 实例,必须在
loadContent回调之后注册(工程内用幂等标记保护)。
MVVM 基类与 BuildProfile
common 提供官方 MVVM 模式三件套:
BaseState:状态基类(子类配合@Observed使用,才能被@State/@ObjectLink观察);BaseVM<T extends BaseState>:持有state,暴露getState(),约定抽象方法sendEvent(event);BaseVMEvent:事件接口,通过type字段区分事件类型。
BuildProfile.ets 由构建期自动生成,导出常量 DEBUG、BUILD_MODE_NAME、TARGET_NAME—— 业务代码一律用 DEBUG 而非自行判断构建类型,debug/release 包的值随构建自动切换。
Features:功能 HAR
browser:浏览器核心 HAR
7 个 common 工具:
| 工具 | 职责 |
|---|---|
| BrowserThemeUtil | 工具栏多主题:10 套完整色板,AppStorage + preferences 双写 |
| BrowserUiModeUtil(common 内) | App 自身页面亮/暗色板 |
| WebDarkModeUtil | 网页内容强制暗色(Web 组件 .darkMode/.forceDarkAccess) |
| WebViewModeUtil | UA 模式切换:固定下发标准手机 UA(Pixel 7)或标准桌面 UA(Win10 Chrome),不靠系统 UA 增删后缀 |
| SearchEngineUtil | 默认搜索引擎前缀,内置 5 个预设(必应/百度/谷歌/头条/DuckDuckGo) |
| SiteAccessUtil | 网站黑白名单(写死不可改),域名后缀匹配 |
| BrowserHistory | 历史记录(上限 200 条),含「待跳转 URL」中转 |
| BrowserFavoriteUtil | 收藏夹(上限 100 条):3 条固定收藏不可删 + 用户收藏,URL 去重 |
9 个 view:BrowserView(主视图,约 1600 行)、BrowserMenuView(菜单)、 SettingsMenuDialog(设置弹窗)、BrowserHistoryView、BrowserFavoriteView、 SearchEngineSettingsView、SiteAccessSettingsView、SiteInfoView(证书链查看)、AboutView。
device:设备注册 HAR(标准 MVVM)
- view:DeviceRegisterView(生成密钥/导出/扫码)、ServerConfigView(服务器列表与绑定);
- viewmodel:DeviceRegister 三件套(Event 定义 CHECK_STATUS/SCAN_IMPORT/GENERATE_KEYPAIR/EXPORT_CONFIG、 State、VM)+ ServerConfig 的 State/VM。异步操作(扫码/导出)在
sendEvent中以 fire-and-forget 方式启动,通过 State 的isLoading/errorMessage反馈 UI。
qxj_sdk:国密 native 能力 HAR(重点)
@nsp/qxj-sdk 把国密能力从 ArkTS 业务中彻底剥离,自下而上分为五层:
预编译库(双 ABI):libs/arm64-v8a/(真机)与 libs/x86_64/(模拟器)各含 libnspsm2handshake.so(SM2 握手)和 libnspsmapi.so(SM2/SM3/SM4 原语)。新增 ABI 需 建目录并在 build-profile 的 abiFilters 追加。
C++ 头文件(include/libs/,10 个):
- 编排类:
Client.h(客户端 A 侧)、Server.h(服务器 B 侧)、Utility.h(hex/字节工具); - 协议:
agree.h(握手帧/参数定义); - 国密:
sm2.h、sm2_agree.h、sm3.h、sm4.h; - 大数库:
miracl.h、mirdef.h(MIRACL 大整数库定义)。
构建(CMakeLists.txt):find_package(Aki) → 收集 src/*.cpp → add_library(handshake SHARED) → 链接 Aki::libjsbind、libhilog、libace_napi 以及 libs/${OHOS_ARCH}/ 下的两个预编译 .so。产物名 libhandshake.so 与 TS 声明、import 严格一致。
NAPI 注册(napi_init.cpp):通过 AKI 的 JSBIND_GLOBAL() 暴露 6 个函数—— buildInitPacket、buildRespPacket、buildAckPacket、buildTokenPacket、 sm4_gcm_encrypt、sm4_gcm_decrypt。入参在 C++ 侧做手写校验(hex 长度、ID 字符集、 时间戳数字),不使用 std::regex 以避免额外链接与启动开销;日志只在失败路径打 ERROR。
ArkTS 封装(ets/utils/,3 个类):
| 类 | 形态 | 职责 |
|---|---|---|
| QxjHandshakeUtil | 实例类 | 4 个握手函数(客户端实际只用 buildInit/buildAck)+ RESP/TOKEN 帧静态解析;构造参数含 DEMO 默认值仅供联调,生产必须显式注入真实密钥 |
| QxjCryptoUtil | 静态类 | SM4-GCM 原始加解密(hex 进 hex 出);密钥固定 32 字节(NSP 变体),返回 cipher/plain + 16B mac |
| QxjSessionCrypto | 静态类 | 业务推荐入口:明文 → 随机 16B IV → SM4-GCM → 组装/解析 64B 传输帧;提供 UTF-8/hex 转换 |
与老项目 qxj-code-app 的差异:handshake + cryptography 两个 HAR 合并为单一 HAR; 两个胶水 .so 合并为一个含全部 6 函数的 libhandshake.so;C++ 参数校验从 std::regex 改为手写; SM4 函数 20+ 条 INFO 日志精简为失败路径 ERROR;删除 myadd 示例函数;密钥不再硬编码。
设计文档中的 C 接口原型(nsp_sm2_build_*)
密钥协商协议 v8 按密码设备 SDF 风格(GM/T 0018 的 ECCrefPrivateKey_st / ECCrefPublicKey_st 结构)定义了 4 个组包函数,是当前 NAPI 6 函数的设计原型:
/* 握手第一包:长期密钥对 + 临时密钥对 + 对端公钥 + 双方 ID + 期望密钥长度 */
int nsp_sm2_build_init_packet(
ECCrefPrivateKey_st *myPriKey, /* 发起方长期私钥 dA */
ECCrefPublicKey_st *myPubKey, /* 发起方长期公钥 PA */
ECCrefPrivateKey_st *myTmpPriKey, /* 临时私钥 rA */
ECCrefPublicKey_st *myTmpPub, /* 临时公钥 RA=[rA]G */
ECCrefPublicKey_st *hePubKey, /* 响应方长期公钥 PB */
unsigned char *pucSponsorId, unsigned int uiSponsorIdLen, /* ID_A */
unsigned char *pucResponsorId, unsigned int uiResponsorIdLen,/* ID_B */
unsigned int uiKeyLen, /* 期望会话密钥长度 */
unsigned int *puiOutLen, unsigned char *pucOutFrame);
/* 握手第二包:比 INIT 多出已导出密钥 pucKey、RA 与其签名、A 的时间戳 */
int nsp_sm2_build_resp_packet(..., unsigned char *pucKey,
const unsigned char *pucRA, unsigned int uiRALen,
const unsigned char *pucSigA, unsigned int uiSigALen,
unsigned long long ulltstampMs, ...);
/* 握手第三包:入参带 RB 与 SB,输出 SA */
int nsp_sm2_build_ack_packet(..., unsigned char *pucKey,
const unsigned char *pucRB, unsigned int uiRBLen,
const unsigned char *pucSB, unsigned int uiSBLen, ...);
/* 握手第四包:token 有效期、待签内容与 HMAC 输出 */
int nsp_sm2_build_token_packet(..., unsigned char *pucKey,
const unsigned char *pucRA, unsigned int uiRALen,
const unsigned char *pucSA, unsigned int uiSALen,
unsigned long long ullExpirationMs,
const unsigned char *pucWaitedHmacData, unsigned char *pucHmacData, ...);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
返回值非 0 即失败;puiOutLen 既是输入容量也是输出长度。工程演进到 ArkTS 侧时, SDF 结构体被拍平为 hex 字符串参数(密钥 64/128 hex、ID 为 ASCII),组包与 SM4 加解密统一收进 6 个 JSBIND_GLOBAL 函数,详见 SM2 密钥协商。
helloworld:NAPI 教学示例 HAR
最小可运行的原生模块参考实现:C++ 侧(napi_init.cpp + CMakeLists + types 声明) 提供 add(a, b) 和 getHelloMessage() 两个 NAPI 接口,使用原生 Node-API(非 AKI); ArkTS 侧含 NativeHelloWorld 封装、HelloWorldView 和完整 State/VM/Event,演示从零搭建一个 native HAR 的全部要素。
showcase:组件演示 HAR(debug/release 双源)
通过 target 的 sourceRoots 切换实现「debug 有内容、release 是空壳」:
// features/showcase/build-profile.json5
{
"targets": [
{ "name": "default", "source": { "sourceRoots": ["./src/debug"] } },
{ "name": "release", "source": { "sourceRoots": ["./src/release"] } }
]
}2
3
4
5
6
7
src/debug/:6 个通用组件(EmptyContentView/ErrorView/LoadingView/NoNetworkView/ ShowcaseCard/ShowcaseNavBar)、5 个 Demo 页(断点卡片/弹窗/日志/状态视图/索引)和自带 BreakpointSystem;src/release/:同名 5 个页面全部是 stub 空实现,随 release 包编译但不含演示逻辑;src/main/只放ShowcaseViews组装出口和 module.json5,保证两个 target 共用模块声明。
自定义协议帧:64B v2 帧结构
握手与业务通信共用同一套 64 字节帧头(2026 改造自旧 32B 帧头,发送方从 4B 系统编号 改为 36B UUID):
偏移 长度 字段 说明
────────────────────────────────────────────────────────────
0 1 版本号 0x01(FRAME_VERSION)
1 1 主命令码 0x00 密钥协商 / 0x01 报警 / 0x02 加密完保通信
2 2 子命令码 最高位 = 上下行(0 上行 Pad→服务器 / 1 下行)
4 2 总长度 全帧总字节数(含帧头、认证校验域)
6 2 帧序号
8 36 发送方 ID ASCII UUID:上行 IDA(设备)/ 下行 IDB(服务器)
44 1 加密认证模式 高 4 位加密模式 + 低 4 位认证;加密完保帧 = 0x40(SM4-GCM)
45 16 IV 加密帧随机生成;握手帧由 C 库生成(全零)
61 3 保留
64 … 数据域 加密完保帧尾部附加 16B GCM tag;握手帧无 tag2
3
4
5
6
7
8
9
10
11
12
四种握手帧的固定长度:
| 帧 | 整帧 | 数据域布局 |
|---|---|---|
| INIT | 280B | total_len(2) + ida/idb/session_key 长度各 2 + timestamp(8) + ida(36) + idb(36) + RA(64) + 签名(64) |
| RESP | 162B | total_len(2) + RB(64) + SB(32) |
| ACK | 98B | total_len(2) + SA(32) |
| TOKEN | 146B | total_len(2) + ida_len(2) + hmac_len(2) + ida(36) + expiration_ms(8,有效时长非时间戳) + HMAC(变长) |
QxjFrameUtil.describeFrame() 能把任意帧按字段逐行解析,每行带字节偏移区间 (如 发送方ID(36B)@[8:44]),并自动校验「总长度」口径,供开发时与协议文档、后端日志逐字节对照。
JSBridge 安全体系
浏览器通过 Web 组件的 javaScriptProxy 向网页注入原生对象 window.jsbridgeHandle。
6 个桥方法(JSB_METHOD_NAMES,注册与诊断共用单一数据源):
| 方法 | 入参 | 返回可选字段 |
|---|---|---|
| login | JSON 字符串(username/password/verification_code/position) | accessToken、refreshToken |
| getAccessToken | 无 | accessToken |
| refreshAccessToken | 无 | accessToken |
| sm4GcmEncrypt | 明文字符串 | data(完整 64B 帧 hex) |
| sm4GcmDecrypt | 帧 hex | data(明文字符串) |
| logout | 无 | — |
统一契约与错误码:所有方法返回结构化 JSON 字符串、Promise 永不 reject;code=0 成功:
| code | 含义 | code | 含义 |
|---|---|---|---|
| 0 | 成功 | -51 | 本地凭据清理失败 |
| -11 / -12 / -14 | 明文空 / 超 64KB / 加密失败 | -60 | 非白名单页面调用 |
| -21 / -22 | 密文格式错 / GCM 完整性校验失败 | -70~-75 | 登录段:payload 非法/未绑定服务器/密钥协商失败/设备 ID 失败/被拒/异常 |
| -31 | access token 不存在 | -41 / -44 | refresh token 不存在 / 服务器拒绝刷新 |
双层源白名单:
- 设计上,API 12+ 的
javaScriptProxy支持permission参数按 scheme/host/port 白名单注入; 但内核该路径存在返回值转换 bug(网页侧 Promise 永远失败),因此对象对所有页面注入; - 实际防护改由每个桥方法入口做应用侧实时校验:取当前页面 URL,与「当前选中服务器」的 4 个 URL(backend/frontend/businessBackend/businessFrontend)比对 origin, 非白名单一律返回 code=-60;白名单为空或取不到 URL 时 fail-closed 全拒绝;
- 每次调用实时读取配置,切换服务器立即生效;BrowserView 另在白名单变化时通过
jsbEpoch销毁重建 Web 组件保持状态一致。
login 的自愈逻辑:网页只传账密,客户端自动补齐设备 ID 与位置(未传 position 时调 LocationUtil 单次定位);发现 K_client 缺失时静默自动执行一次 SM2 协商,无需用户跳协商页; 遇到自签名证书时弹风险提示,API≥18 用户确认后可跳过证书校验重试,API<18 明确告知无法忽略。
ArkUI 状态管理(V1/V2)
MVVM 架构
状态管理约束
关键约束
ArkUI V1 有严格的状态管理约束,违反会导致编译错误或运行时崩溃。
// V1:@Component 中派生值用普通方法(@Computed 是 V2 装饰器,不能在此使用)
@Component
struct RightComponentV1 {
@State count: number = 0
@State accessEnabled: boolean = true // 避免与内置属性同名
getDoubleCount(): number {
return this.count * 2
}
}
// V2:@ComponentV2 才支持 @Computed
@ComponentV2
struct RightComponentV2 {
@Local count: number = 0
@Computed
get doubleCount(): number {
return this.count * 2
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
工程中 V1/V2 的分工:通用页面与浏览器主体沿用 V1(@Component + @State/@StorageLink), 多标签等新特性在 V2(@ComponentV2 + @Local/@Computed)中开发,两套装饰器不可混用装饰同一 struct。
响应式实战:@StorageLink + @Watch
BrowserView 集中演示了 V1 响应式组合用法:
// 订阅全局存储,变化时回调;homeRefreshTick 由 MainPage.onPageShow 递增
@StorageLink(DeviceStorageKey.HOME_REFRESH_TICK)
@Watch('refreshServerDisplay')
homeRefreshTick: number = 0
// 收藏列表直接 @StorageLink,星标状态随收藏增删自动刷新
@StorageLink(FAVORITE_LIST_KEY)
favoriteList: FavoriteEntry[] = []2
3
4
5
6
7
8
ForEach key 约束
// ❌ 错误:缺少 key
ForEach(this.items, (item: Item) => {
ItemView({ item })
})
// ✅ 正确:提供唯一 key
ForEach(this.items, (item: Item) => {
ItemView({ item })
}, (item: Item) => item.id)2
3
4
5
6
7
8
9
BrowserView 中重建 Web 的 ForEach 以 jsbEpoch 为 key('web_jsb_' + epoch), 保证白名单变化时 Web 组件被真正销毁重建而非复用。
国际化架构
语言资源结构
工程在 common 及各 feature HAR 中各自维护相同的 7 语言目录:
common/src/main/resources/
├── base/
│ └── element/
│ └── string.json # 兜底资源
├── zh_CN/ # 简体中文
├── zh_TW/ # 繁体中文(台湾)
├── zh_HK/ # 繁体中文(香港)
├── zh_MO/ # 繁体中文(澳门)
├── en_US/ # 英语
└── ja_JP/ # 日语2
3
4
5
6
7
8
9
10
资源引用
// 本模块引用
Text($r('app.string.browser_back'))
// 跨模块引用(@ohos/browser 的资源)
Text($r('@ohos/browser:string.browser_back'))2
3
4
5
语言匹配规则
主题、外观与 UA:四套独立设置
浏览器的「外观」实际由四套互相独立的设置组成,全部遵循同一套持久化范式: 运行时值放 AppStorage(@StorageLink 订阅即时重渲染),写入时异步落盘到 preferences(store 统一为 browser_prefs),init() 在 BrowserView aboutToAppear 时读回。
| 设置 | 工具类 | AppStorage key | preferences key | 默认 | 控制对象 |
|---|---|---|---|---|---|
| 工具栏主题 | BrowserThemeUtil | browser_theme_id | browser_theme | dark | 工具栏 8 色完整色板 |
| 界面亮/暗 | BrowserUiModeUtil | browser_ui_dark_mode | ui_dark_mode | dark | App 自身全部页面(13 色色板) |
| 网页暗色 | WebDarkModeUtil | browser_web_dark_mode | web_dark_mode | dark | 网页内容强制暗色 |
| UA 模式 | WebViewModeUtil | browser_webview_mode | webview_mode | mobile | 网页请求的 User-Agent |
工具栏预置 10 套主题(BROWSER_THEMES):默认白、深邃黑、晴空蓝、薄荷绿、暖阳橙、 靛青蓝、黛紫等,每套是包含 toolbarBg/chipBg/textPrimary/textSecondary/textPlaceholder/ textDisabled/clearBtnBg 的完整色板——不允许只换背景不换字色;彩色主题用半透明白 (#29FFFFFF)叠出按钮层次感。
为什么不用系统 colorMode 资源?
系统 colorMode 资源只能跟随系统深/浅色两种状态;自定义 10 主题、网页暗色、界面暗色、 UA 等需要独立开关和即时生效的场景,一律走「AppStorage + preferences」自管机制。
断点适配
BreakpointSystem 实现「一多」布局的宽度断点:
// 断点阈值(vp):sm < 600,md 600~840,lg > 840
export function getBreakpointByWidth(width: number): WidthBreakpoint {
if (width < 600) {
return WidthBreakpoint.WIDTH_SM
} else if (width < 840) {
return WidthBreakpoint.WIDTH_MD
} else {
return WidthBreakpoint.WIDTH_LG
}
}
// 泛型断点取值:一次声明三端各自的值
const bp = new BreakpointType<number>({ sm: 48, md: 56, lg: 64 })
const height = bp.getValue(getBreakpointByWidth(windowWidth))2
3
4
5
6
7
8
9
10
11
12
13
14
平板(lg)采用工具栏一体化布局,手机(sm)按钮移至底部,即是按设备形态常量 + 断点共同决定。 showcase HAR 在 debug 源中自带一份 BreakpointSystem 副本用于演示。
安全区域与挖孔屏适配
挖孔屏适配
应用采用非全屏布局,窗口内容区从状态栏下方开始;页面仅向底部 SYSTEM 安全区扩展, 顶部挖孔由系统在状态栏范围内自动避让。
// EntryAbility.ets
onWindowStageCreate(windowStage: window.WindowStage): void {
const mainWindow = windowStage.getMainWindowSync()
// 内容区从状态栏下方开始,避开挖孔
mainWindow.setWindowLayoutFullScreen(false)
// 状态栏和导航条可见
mainWindow.setWindowSystemBarEnable(['status', 'navigation'])
}
// tablet 页面底部沉浸式(phone 底部有按钮栏,不扩展)
Column() {
// 内容
}
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])2
3
4
5
6
7
8
9
10
11
12
13
14
悬浮 Tab/悬浮球等浮层还会读取 TYPE_NAVIGATION_INDICATOR 避让区高度,专门为手势导航条留出边距。
证书链查看器:手写 X.509 解析
BrowserView 内置了一个 Chrome 风格的网站证书查看器(约 500 行),不依赖系统证书接口, 手写 DER TLV 解析:
- 基础解析原语:
readDerLen(DER 长度字段)、oidBytesToString(OID 点分编码)、parseSubjectPublicKeyInfo(SPKI 公钥结构,区分 RSA 模数位长与 EC 曲线/点长度); CertDetails描述单张证书约 20 个字段:主体/颁发者 DN 与 CN、版本、序列号、有效期状态 (ok/notyet/expired)、签名算法及 OID、公钥展示(如RSA (2048 bits)、EC P-256)、 KeyUsage/ExtKeyUsage、BasicConstraints(pathLen)、SAN、CRL 分发点、SHA-256/SHA-1 指纹;- 完整证书链按叶子→中间 CA→根排列,在 SiteInfoPage 中逐张查看;安全级别由
WebviewController的getSecurityLevel获取,配合证书加载状态机(idle/loading/ok/error)驱动 UI。
网站访问控制与 SSL 错误处理
网站访问控制(onLoadIntercept):
- 配置写死、始终开启:黑名单
baidu.com(域名后缀匹配,同时命中www.baidu.com等所有子域名), 白名单为空表示除黑名单外全部放行;设置页只读,任何修改接口都是 no-op; - Web 的
onLoadIntercept在请求发起前判定,命中黑名单即拦截并展示内置拦截页; 非 http(s) 资源(about/data/本地)不参与拦截。
SSL 证书错误(onSslErrorEvent):
- 主帧证书错误(域名不匹配 HostMismatch / 日期无效 DateInvalid / 不受信任 Untrusted) 弹出原生确认框,由用户选择取消或「继续访问」(红色风险按钮),文案按错误类型本地化;
- 子帧一律静默阻止;确认框显示期间的新错误直接取消,防止弹框叠加。
Web 调试开关
// features/browser/src/main/ets/view/BrowserView.ets
aboutToAppear(): void {
// 只有调试构建才启用 8888 端口 Web 调试(支持有线/无线 DevTools 接入)
if (DEBUG) {
webview.WebviewController.setWebDebuggingAccess(true, 8888)
Logger.info(TAG, 'Web debugging enabled on port 8888')
} else {
webview.WebviewController.setWebDebuggingAccess(false)
Logger.info(TAG, 'Web debugging disabled (release build)')
}
}2
3
4
5
6
7
8
9
10
11
release 包必须关闭该开关,避免生产环境对外暴露调试端口;另有 DEBUG 专用诊断脚本 (window.__jsbridgeDebug),release 构建不注入,防止向网页泄露服务器地址。
工程质量配置
| 配置文件 | 作用范围 | 关键内容 |
|---|---|---|
| code-linter.json5 | 全部 .ets | 规则集 @performance/recommended + @typescript-eslint/recommended;@security/* 安全规则:禁用不安全 AES/Hash/DH/DSA/ECDSA/RSA 加解密与签名/3DES(error),不安全 MAC 为 warn;排除测试/mock/build |
| .clang-tidy | C/C++ | 10 项检查:switch case 完整性、NAPI 模块名规范、if-else→三元组、未使用变量/参数、modernize-use-auto、系统能力可读性、CAPI 版本校验 |
| .clangd | clangd | 追加 -Wunreachable-code-aggressive;未使用 include 严格检查、未使用函数检查;unused-parameters 严格模式 |
| obfuscation-rules.txt | 各模块 | release 混淆规则文件(当前 enable: false,规则已就位) |
| consumer-rules.txt | HAR 消费方 | common/qxj_sdk/helloworld 对外暴露的混淆保留规则 |
| oh-package.json5(工程根) | 测试 | devDependencies:@ohos/hypium(单元测试框架)+ @ohos/hamock(mock 框架) |
构建系统与 hpack
Hvigor 构建
工程使用 Hvigor(hvigorw)构建,工程根与每个模块各有 hvigorfile.ts, 配置集中在各级 build-profile.json5;build-profile.json5.example 提供不含本机路径/口令的模板。
hpack:一键打包与分发页工具
hpack 是独立的命令行打包工具(GitHub: iHongRen/hpack,pip 安装,首次使用需 hpack init); 鸿蒙仓内的 hpack/ 目录是它的工作目录而非工具本体:
hpack/
├── config.py # 打包配置:分发域名、应用信息、签名材料、hvigorw 命令
├── PackFile.py # 打包前/后/失败/模板回调(可自定义上传、webhook)
├── sign/ # 签名材料:cer / csr / p12 / p7b + material 哈希材料
└── webpage/ # 分发页:index-debug.html / index-release.html / manifest.json52
3
4
5
常用命令(配置内自动映射到对应的 buildMode 与 module@target):
# Debug 包:default target,含高德 3D 地图,包体约 55MB
hpack pd
# Release 包:release target,排除高德 3D 地图资源与 .so,包体约 7MB
hpack pr2
3
4
5
# 切换入口模块(默认 tablet;phone 同理)
$env:HPACK_MODULE='phone'
hpack pr
# 恢复默认
Remove-Item Env:HPACK_MODULE2
3
4
5
6
hpack 实际下发的 hvigorw 命令(config.py 组装):
hvigorw assembleHap --mode module \
-p product=appstore \
-p module=tablet@release \
-p buildMode=release \
--no-daemon2
3
4
5
必须显式指定 module@target 与 buildMode,否则 hvigorw 会同时编译两个 target, 且 release 的地图排除配置不生效。pc/tv/wearable 骨架模块不经过 hpack,直接用 hvigorw 编译。
分发产物:安装包与分发页托管于腾讯云 COS(北京),webpage/manifest.json5 描述 bundle 信息、各模块包下载地址、SHA 哈希与清单签名;分发页支持模板选择 (default/simple/tech/cartoon/tradition/custom)并配置了「历史版本」按钮。