做 UniApp 多端开发这几年,我手里大部分 Vue3 项目都是“非 CLI 项目”——也就是直接在 HBuilderX 里创建,没有独立 src 目录和 package.json 管理的那套。最近为鸿蒙(HarmonyOS)适配 Deep Linking,恰恰这种非 CLI 形态最容易翻车:HBuilderX 一编译,鸿蒙工程里很多配置都是"生成即覆盖"的状态,外部链接要精准唤起 App 并跳到指定页面,配置必须从三处同时下手。这篇文章把我在 HarmonyOS NEXT 上配置深链的完整过程写出来,从解开 URL Scheme 与 App Linking 的概念,到非 CLI 项目里改哪几个文件、怎么写 module.json5、怎么在 UniApp 侧拿到参数,再到真机验证方案,适合正在做鸿蒙版适配、又被深链折腾到头疼的 UniApp 同学参考。
1. 为什么要给 UniApp 的鸿蒙产物单独配置 Deep Linking
1.1 “深链”到底解决什么问题
Deep Linking 说人话,就是把一个链接变成打开 App 内部页面的钥匙。用户点营销短信里“领取优惠券”,系统并不只是把链接丢给浏览器,而是找到手机里已安装的对应 App,直接唤醒后跳到券包页;如果你没装 App,它还可以跳到应用市场或者一个 H5 落地页兜底。这个场景在鸿蒙生态里越来越刚需,因为鸿蒙应用分发的入口不只有桌面图标,还会从华为的扫码、小艺建议、AppGallery 推广位、短信邮件等路径进来,没有深链就意味着所有外部流量都被挡在“安装 App”这一步,根本无法复用到应用内的转化闭环。
具体到业务上,常见的使用场景有这么几类:
- 短信营销和 PUSH 通知:用户点击追踪链接,直接进入商品详情或活动页。
- 扫码场景:线下物料二维码扫完不打开网页,而是拉起 App 并对参数做鉴权。
- 广告投放:广告平台回传的点击 URL 需要落地到 App 指定页面。
- 应用内互相跳转:比如从手机厂商的全局搜索、语音助手识别的某个业务,跳回自己的应用内部。
它的核心动作只有两个:第一步,系统解析链接;第二步,把解析出来的 scheme、host、path、query 交给 App,由 App 决定路由到哪个页面。听起来简单,真配置起来你会发现在不同端上根本不是一回事,尤其是跨端框架 + 新操作系统的组合,往往要跨好几个工具链来回试。
1.2 非 CLI 项目在哪一层配置深链
很多教程默认你用的是 CLI 创建的 UniApp 工程,里面的 HarmonyOS 打包目录是可控的,改完代码提交 Git 就完事。但非 CLI 项目不一样。非 CLI 项目是 HBuilderX 图形化管理的工程,它没有外层 package.json、没有 scripts 脚本,manifest.json 和 pages.json 才是入口。每次构建,HBuilderX 都会重新生成unpackage/dist/build/下的鸿蒙工程产物。也就是说,如果你只改生成产物里的module.json5,下一次 Build 就会被工具静默覆盖。
所以正确姿势应该是:以 manifest.json 为配置入口,以生成产物为校验点,两边配合。manifest.json 里能表达的字段先写进 manifest,工具支持不了的部分,再在生成工程里补技能配置,并且把“重构建后需要复查”这个动作固定到流程里,避免同事一提交就说深链失效了。这也是为什么我特别想写一篇“非 CLI 项目”专属指南,因为很多坑都是工程形态带来的,跟 HarmonyOS 平台本身反而关系不大。
我把非 CLI 和 CLI 的关键差异列了个表,方便快速建立概念:
| 对比维度 | CLI 工程 | 非 CLI 工程(HBuilderX 创建) |
|---|---|---|
| 代码组织 | 有 src、package.json,可配置自定义构建脚本 | 以 HBuilderX 内建结构为准,src 在外层不可见 |
| 鸿蒙工程产物 | 通常由自定义脚本输出到可控目录,可入库 | 生成在 unpackage/dist/build/ 下,重新构建会被覆盖 |
| 深链主要配置入口 | manifest.json + 自定义鸿蒙工程文件 | manifest.json + 生成产物里的 module.json5 补改 |
| 版本管理影响 | 稳定,改动可追溯 | 生成目录通常不进版本库,需要额外写复核文档 |
搞清楚这个背景,后面所有操作就比较有目的性了:既要让工具“认识”你的深链,又要保证工具重新生成后配置不丢。
2. HarmonyOS 里 Deep Linking 的两个关键概念
2.1 从 URL Scheme 到 App Linking
HarmonyOS 上做深链,最底层的能力是“技能配置”(skill)。你可以把 skill 理解为 Android 里的 intent-filter,它声明了当前应用能够响应哪些系统行为。具体到深链,就是在一个能力(Ability)里声明skills数组,数组里写actions、entities、uris三组字段,系统在收到一个协议链接时,会拿着链接的协议名、域名、路径去遍历所有应用的 skills,匹配上的就被唤起来。
这里有两个梯度的方案:
第一个是自定义 URL Scheme,比如myapp://h5/purchase/detail。这种方案完全自主,适合应用内互跳、测试环境联调、内部工具类 App。缺点是外部生态不会自动认识你的 scheme,需要在链接分发时明确告知用户“复制链接到浏览器”或“打开 XXApp”。
第二个是 App Linking,基于 HTTPS 标准的https://yourdomain/path。它比自定义 scheme 更正规,落地页天然可用,系统也更容易识别和信任。副作用是必须要有可控域名,并且需要到华为 AppGallery Connect 里配置 SHA 指纹、域名校验文件,整体链路多了一道云端服务。
对大多数 UniApp 项目,我建议先做自定义 scheme,把接收链路跑通,再根据线上投放需求升级到 App Linking。因为深链配置的难点不在于“选哪个协议”,而在于“传参对不对、跳转准不准”,先把地基打好更重要。
2.2 配置前必须备好的物料清单
在动手改配置前,把下面物料准备齐,能省掉八成报错:
- HBuilderX 版本:建议 4.x 以上,并选择包含鸿蒙编译能力的版本,确认 Manifest 可视化界面里能看到“鸿蒙配置”相关入口。
- DevEco Studio:华为官方 IDE,用于打开、调试和签名鸿蒙工程,版本建议跟着 HarmonyOS SDK API 12+(对应 HarmonyOS NEXT 5.0.0(12))走。
- 真机或模拟器:强烈建议真机,因为 skill 匹配和系统弹窗交互在模拟器上表现有差异。
- 签名文件:Debug 调试包用工程的自动签名即可,发布版必须确认证书、Profile 都正确关联。
- 域名和 HTTPS 证书:仅 App Linking 需要,如果只做自定义 scheme,这条可跳过。
- 一个固定的产品 bundleName:深链跳转后要回到对应应用,bundleName 错一个字母都匹配不上。
另外提醒一句,鸿蒙 SDK 版本迭代很快,所有配置文件都以你本机 DevEco 关联的 SDK 版本为准。标题里提到harmonyos next sdk(api 12+ / 5.0.0(12)),我这边就是按这个版本跑通的,API 版本不同时,字段名可能有微调,但配置的嵌套结构基本一致。
3. 实操:UniApp(Vue3 非 CLI)鸿蒙深链配置全过程
3.1 第一步:确认工程能跑通鸿蒙板
千万别跳过这步。我见过有人在深链配置上折腾一下午,最后发现是 HBuilderX 的鸿蒙编译目标没启用,压根没安装到真机。
先在 HBuilderX 里打开你的 Vue3 非 CLI 项目,进入 manifest.json 的可视化界面,找到“鸿蒙配置”或者“HarmonyOS”相关的 Tab,确认是否勾选开启鸿蒙编译。然后点击菜单栏“运行 — 运行到手机或模拟器 — 运行到鸿蒙设备”,让 UniApp 自动把 js、pages、manifest 配置生成一份鸿蒙原生工程,再通过 DevEco 编译安装到真机。
首次跑通会有几个前置条件:本机已经安装了 DevEco Studio、真机开启了开发者模式并信任调试电脑、HBuilderX 的鸿蒙插件识别到了 DevEco 的 SDK 路径。这些做完,你能在手机上看到自己的 App 图标,才算拿到深链调试的最低基线。
这一步还有个很好的副产品:HBuilderX 会输出一个鸿蒙工程目录,你可以在 DevEco 里直接打开它,后续三步都在这个工程里操作。非 CLI 项目不需要手动创建任何工程,别自己在 DevEco 里 New Project,否则会跟 HBuilderX 生成的工程冲突。
3.2 第二步:在 manifest.json 里预留深链配置
为了让非 CLI 项目在重构建时不至于丢失所有自定义信息,我会先在 manifest.json 里做一层“占位”。以 UniApp 当前鸿蒙适配的实现为例,可以在app-plus节点下新增distribute的子节点harmony,里面用deepLink做配置入口:
{ "app-plus": { "distribute": { "harmony": { "deepLink": { "scheme": "ruoyiuni", "host": "h5", "path": "/purchase/detail" } } } } }不过要特别说明,不同版本的 HBuilderX 对这个字段的解析深度不一样。老版本可能只在可视化界面生成 scheme,不负责把 skills 完整写入 module.json5;新版本则可能直接干预生成逻辑。所以这段配置只能算“上游声明”,真正生效还要看第三步生成的 module.json5 是否出现了对应的技能块。
如果你在 manifest.json 里写完这段,发现构建后的 module.json5 没有任何变化,不用慌。说明你当前 HBuilderX 版本还未支持该字段自动生成,那就直接按第三步手改产物,然后把“构建后需要复核”记录进团队文档即可。
3.3 第三步:正确改写 module.json5 的 skills 块
这是整个深链配置的核心。用 DevEco Studio 打开 HBuilderX 生成的鸿蒙工程,找到入口模块的module.json5,一般在entry/src/main目录下。先理解它的结构:module 下面会挂一个abilities数组,数组里每个 Ability 代表一个页面入口能力,深链的 skills 必须挂在“能够被外部拉起”的那个 Ability 上,通常是EntryAbility。
打开文件,定位到abilities数组,找到入口 Ability,在里面加一个skills数组:
{ "module": { "name": "entry", "type": "entry", "description": "$string:module_desc", "mainElement": "EntryAbility", "deviceTypes": [ "phone", "tablet" ], "deliveryWithInstall": true, "installationFree": false, "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "description": "$string:EntryAbility_desc", "icon": "$media:app_icon", "label": "$string:EntryAbility_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "actions": [ "ohos.want.action.viewData" ], "entities": [ "entity.system.browsable" ], "uris": [ { "scheme": "ruoyiuni", "host": "h5", "path": "/purchase/detail" }, { "scheme": "https", "host": "app.yourdomain.cn", "path": "/purchase/detail" } ] } ] } ] } }这段配置的作用是告诉系统:这个 Ability 可以处理“查看数据类”的意图,并且在浏览器场景下可见。entity.system.browsable很关键,加上它,系统才认为可以在链接唤起时把应用列为可选项。uris数组可以写多个,scheme、host、path 各有侧重:一边留自定义 scheme 服务内跳转,一边留 https 给 App Linking 兜底。
改完保存后,用 DevEco 重新编译安装。如果配置写得不合法,编译阶段就会报错,这其实是好事,总比运行时静默失败好定位。验证方式可以看编译产物中是否存在skills块,也可以直接用下面第五步的方式从外面拉起试试。
注意:不要把这个 skills 块写到 module 顶层。我见过不少人照着 Android 的 intent-filter 习惯去写,结果把 skills 放到 module 根目录,编译通过但链接死活唤不起。HarmonyOS 的 skills 必须挂在
abilities数组的具体ability节点下,尤其是你已经定义了 mainElement 的入口 Ability。
3.4 第四步:在 UniApp 侧接收参数并跳转到目标页面
工程配置只是让系统愿意拉起 App,拉起之后怎么跳转,就要靠 UniApp 的页面生命周期来接手。以 Vue3 的非 CLI 项目为例,你可以在App.vue中写入接收逻辑。先建立一个通用的 URL 参数解析函数:
function parseDeepLink(url) { if (!url) return null try { const decoded = decodeURIComponent(url) const [base, queryString] = decoded.split('?') const match = /^([a-z]+):\/\/([^/]+)(\/[^?]*)?/.exec(base) const result = { scheme: match ? match[1] : '', host: match ? match[2] : '', path: match && match[3] ? match[3] : '/', query: {} } if (queryString) { queryString.split('&').forEach((item) => { const [key, value] = item.split('=') if (key) result.query[key] = value || '' }) } return result } catch (e) { console.error('deep link parse error', e) return null } }然后在生命周期里去读取参数。这里有个关键点:冷启动和热启动拿参数的位置不一样。杀进程后的首次启动,onLaunch能拿到;但如果 App 已经在后台,系统再次用深链唤回,onLaunch不会再触发,必须去onShow里补一次。
import { onLaunch, onShow } from '@dcloudio/uni-app' onLaunch((options) => { const link = parseDeepLink(options && options.query && options.query.url) if (link && link.path === '/purchase/detail') { uni.navigateTo({ url: `/pages/purchase/detail?id=${link.query.id || ''}` }) } }) onShow((options) => { const link = parseDeepLink(options && options.query && options.query.url) if (link && link.path === '/purchase/detail') { uni.redirectTo({ url: `/pages/purchase/detail?id=${link.query.id || ''}` }) } })实际开发中你会遇到一个现象:鸿蒙桥接层传给 UniApp 的 options.query 可能不是扁平对象,而是一个query字符串,或者url字段里才放着完整地址。所以我强烈建议你在 onLaunch 第一步先把options完整打出来看一遍结构,再决定是从options.query.id取还是从options.query.url里解析。上面代码里的写法兼容了“完整地址放在 query.url”的情况。
如果要在页面内更优雅地接收,也可以把这段逻辑抽成一个 composable 或者 mixin,在首页调用一次即可,不必污染全局 App.vue。但深链跳转往往发生在用户还没有进入业务首页的状态,所以我还是倾向于在 App 生命周期统一处理。
3.5 第五步:真机验证一条龙
配置完不能只看编译通过,真正要验证的是从系统层面拉起这个动作。推荐的做法是把链接通过手机自带的备忘录、短信或者一个简单的 HTML 页面发出来,点击后用系统弹窗确认能否唤起。
我的完整验证路径是这样:
- 先用 DevEco 安装 Debug 包到鸿蒙真机。确保手机上图标已经出现,且应用能正常打开。
- 在备忘录里输入
ruoyiuni://h5/purchase/detail?id=10086,长按识别为链接。 - 点击该链接,观察系统是否弹出“打开 ruoyiuni 应用”的浮层;如果没有弹窗,八成是 skills 没生效。
- 点击唤起后,在 DevEco 的 Log 窗口里过滤关键词
deepLink或query,确认 UniApp 侧是否打印出解析后的参数。 - 再测一次热启动:保持 App 在后台,重新从备忘录点链接,确认
onShow分支能正常跳转。
如果手头没有消息应用可发链接,用hdc命令行也可以模拟一部分外部唤起的流程。比如hdc shell aa start -d "ruoyiuni://h5/purchase/detail?id=10086",如果你能看到应用被拉起,就说明系统侧的 skills 匹配已经成功。不同 SDK 版本的 aa 命令参数会有差异,最稳的还是靠一个真实可点击的链接来测。
注意:整个验证过程中,如果 App 一直处于调试模式,系统对 scheme 的处理可能和 Release 模式有细微差异。所以上线前我还会打一个 Release 包,在系统浏览器里做一遍全链路回归,防止 Debug 签名下的行为误导排查方向。
4. 四类高频坑与排查方案
4.1 scheme 配了但系统不识别
这个问题出现的频率最高,症状就是点链接没有弹窗,就像没配过一样。按顺序排查以下四件事:
skills是否嵌在abilities数组里的入口 Ability 下,而不是 module 顶层。exported字段是否为true。深链唤起的 Ability 如果没有导出,外部系统没有权限访问,配置再多也没用。entity.system.browsable是否写对。字段多一个字符、少一个字符都会导致匹配失败。- 装饰过的 scheme 是否和链接完全一致,注意大小写。系统匹配是区分大小写的。
我这里有个通用检查法:在 DevEco 里打开已安装应用的配置,把module.json5中的 skills 块和应用安装后的实际配置比对,看两者是否一致。如果实际配置里没有 skills,那问题大概率出在 HBuilderX 构建覆盖了你的手改内容,需要回到 3.2 里的 manifest.json 兜底方案。
4.2 拿着参数却拿不到 onLaunch 的 query
有时 App 能正常被拉起,但 onLaunch 里的 options 是空的,或者只有一堆看不懂的序列化字符串。这种情况通常不是配置问题,而是参数传递方式差异。
- 如果链接是
ruoyiuni://h5/purchase/detail?id=10086,UniApp 多数情况会把id=10086作为 query 传下来。 - 如果链接是
ruoyiuni://h5/purchase/detail?id=10086&source=scan,有些桥接层会对参数顺序敏感,需要你 decode 后再取值。 - 如果链接是
ruoyiuni://h5?page=/purchase/detail&id=10086,那就不要指望 path 里带信息,所有业务参数都在 query。
我的建议是不要在 onLaunch 里赌一个字段名,而是统一把链接原始地址打出来,再交给 parseDeepLink 解析。这里再强调一次:热启动必须在onShow里再取一次参数,这是 Uniapp 多端框架通病,鸿蒙也不会例外。
4.3 非 CLI 项目的构建覆盖问题
非 CLI 项目手改的生成产物,大概率会在下次构建时被冲掉。这种问题最阴间的点在于:它不是立即报错的,而是某天你把手机上的应用卸载重装,突然发现深链又挂了。
针对覆盖问题,我的经验是按阶段做好三件事:
- 构建前,把所有需要写入 module.json5 的自定义深链配置先记录到 manifest.json 的
app-plus.distribute.harmony.deepLink节点,能识别就让工具自动写,省得每次都手动改。 - 构建后,用 DevEco 全局搜索“uris”,快速确认 skills 是否还在;不在就立刻补写,并做一次完整验证。
- 版本管理上,把“深链复核”做成发布清单里的检查项,而不是随缘检查。团队规模一大,靠人肉记忆一定会漏。
这也是我在这篇里反复强调“manifest 为入口、产物为校验点”的原因。非 CLI 项目没法像 CLI 那样把整个鸿蒙工程纳入 Git 管理和 CI 构建,但通过固定流程是可以保证配置稳定性的。
4.4 真机调试签名与环境不一致
深链配置本身没问题,但真机上就是打不开,往往是签名问题。DevEco 里自动签名时给的指纹,和手机设备端实际注入的开发证书指纹不一致,就会把应用拦在外面。
排查方法很直接:在 DevEco 的 Signing Configs 里确认当前用的证书、Profile、bundleName 三者匹配,然后重新执行签名同步。我遇到过一次,签名后应用能正常打开,但系统在按链接匹配应用时根本不到桌面图标,换了个调试证书再签一次就好了。如果换了证书还不行,就把应用卸载重装,排除系统缓存了旧证书信息的可能。
另外,不要在鸿蒙深链调试阶段混用多个华为开发者账号。每个账号生成的证书指纹不同,应用一旦安装过 A 账号签名的版本,再用 B 账号覆盖安装很容易出现“可打开但不可外部唤起”的诡异状态。
5. 一点私货:非 CLI 项目配鸿蒙深链的简化路径
根据我这段时间的实操,给你一条我验证过最顺的路径:先用自定义 scheme 把整条链路跑通,再决定要不要上 App Linking。不要一上来就搞 HTTPS 域名、AppGallery Connect 配置,那样排查环节会成倍增加,特别是非 CLI 项目本身就已经多了一层“生成产物覆盖”的变量,新手很容易陷入怪圈不知道是业务链路问题还是平台配置问题。
具体执行顺序,我会固定成:确认能运行到鸿蒙真机 → manifest.json 里留下 deepLink 声明 → 在生成工程里补 skills → 在 App.vue 里解析并跳转 → 真机验证冷热启动 → 重构建一次再验证深链是否稳定。这套顺序把风险点拆得很小,每一步都只引入一个变量,出问题能快速定位。
最后想提醒的是,HBuilderX 和 DevEco 都在高频迭代,你找到的教程版本号如果和手头不一样,不要觉得是自己抄错了。字段名变了、生成目录变了、可视化入口换了,这些在鸿蒙适配期都是常态。关键是把配置逻辑吃透,然后用我前面的“复核三步法”去验证现状,比死记一段配置更靠谱。