本文基于 HarmonyOS 6.1 的「扫码直达」官方文档与 App Linking 接入指导整理。文中代码是为说明问题自写的完整示例,不是官方示例的搬运;API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机表现的部分已明确标注,未做任何实测数据编造。

引子:扫码之后,用户落在了首页
有个做本地生活的朋友跟V哥抱怨:他在应用里生成了一张"扫一扫点单"的码,发出去给用户。用户用系统的扫一扫一拍------应用是打开了,但停在了启动页。点单页在哪?用户得自己再点两下。
他说:"这码白做了。"
这事V哥第一反应是"是不是V哥没配对"。翻完官方文档才看明白:从 6.1 起,系统把'扫码直达'这件事接好了大半,但'扫完落在哪一页'这件事,系统不知道,得你告诉它。
换句话说,入口是系统给的,落地是应用自己的活。把这两件分清楚,整套能力就顺了。
一、扫码直达到底是什么:系统扫,应用落
官方的描述很直白:开发者把域名注册到"扫码直达"服务后,用户通过控制中心等系统级常驻入口 ,扫描应用的二维码、条形码,就能跳转到应用对应服务页,实现一步直达(接入"扫码直达"服务)。
V哥把整条链路拆成四步,画成一张业务流:

| 步骤 | 谁来做 | 你写不写代码 |
|---|---|---|
| ① 域名注册到扫码直达服务 | 你在 AGC + 开发者网站 | 不写,配 |
| ② 用户从系统扫码入口发起扫码 | 系统(控制中心等) | 不写 |
| ③ 系统解析码值、查到对应应用 | 系统 | 不写 |
| ④ 拉起应用、跳到对应服务页 | 系统拉起 + 你做路由 | 写 |
一眼看清了吧:前③步都是系统的事,只有第④步的"落到哪个页",需要你接住码值再路由。
这里有个硬边界先说在前:扫码直达仅支持中国境内 (香港特别行政区、澳门特别行政区、中国台湾除外)接入使用(扫码直达 · 说明)。如果你的应用做海外发行,这条链路在你那不生效,别硬等。
二、第一步:先把域名交给 App Linking
扫码直达到底靠什么把"码"和"你的应用"绑在一起?答案是 App Linking。
官方的开发准备写得很明确:针对扫码直达,App Linking 是必选项 。你需要做四件事(开发准备 · 仅针对扫码直达必选):
- 在 AGC 控制台开通 App Linking 服务;
- 在开发者网站上关联应用;
- 在 App Linking 中配置二维码、条形码关联的网址域名;
- 在应用的
module.json5里关联这个域名。
三个V哥自己踩过的坑,提前说:
- 只能用 HTTPS 网址。App Linking 当前仅支持 HTTPS,HTTP 域名直接不被接受。
- 不能用自动签名。官方明确:接入 App Linking 不能使用 DevEco 的自动签名功能,必须手动签名。第一次V哥卡在这半天,编译能过、扫码不跳,最后发现是签名方式不对。
- 应用未安装时,码值对应的网页也得准备好。用户手机没装你的应用,系统会拉起浏览器打开那个 HTTPS 页面------这是你"沉默获客"的兜底。
V哥的判断就一句:域名是门票,App Linking 是那条门缝。门缝没开,后面再怎么写路由都是空转。
顺带说一句为什么这件事值得做:扫码直达的入口是系统级的常驻入口(控制中心等),比你在应用里自己塞一个扫码按钮浅得多。用户不用先打开你的 App、再找扫码入口,而是"系统一拍、直接落到服务页"。对拉新和复访来说,这条路径省掉的就是那两下点击------而这恰恰是最容易流失的地方。所以域名这一步,别嫌配起来麻烦。
三、第二步:在 EntryAbility 接住码值
这一步才是真正要写代码的地方。
系统拉起应用时,码值(那个 HTTPS 链接)会落在 Want 的 uri 字段里。你要做的,是在 EntryAbility 的两个入口都接住它:冷启动走 onCreate,热启动走 onNewWant。
下面是自写的完整示例,把接住码值、再跳页的逻辑收在 routeByUri 一处:
typescript
// entryability/EntryAbility.ets
import { UIAbility, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { router, window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private uiContext?: UIContext;
// 冷启动:系统扫码入口拉起时,码值经 App Linking 落在 want.uri
onCreate(want: Want): void {
this.routeByUri(want);
}
// 热启动:应用已在前台,再次被扫码入口拉起
onNewWant(want: Want): void {
this.routeByUri(want);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index');
windowStage.getMainWindow().then((win: window.Window) => {
this.uiContext = win.getUIContext();
});
}
// 把"码值 → 页面"的映射收口在一处,别散落到各 ability
private routeByUri(want: Want): void {
const uri: string | undefined = want?.uri;
if (!uri) {
return;
}
const target: string = ScanRouter.match(uri);
if (this.uiContext) {
const state: router.RouterState = this.uiContext.getRouter().getState();
// 已经在目标页就不重复压栈,避免用户回退时一路是同一页
if (state && state.name !== 'Access') {
this.uiContext.getRouter().pushUrl({ url: target }).catch((err: BusinessError) => {
hilog.error(0x0001, '[ScanDirect]', `路由失败 code: ${err.code}`);
});
}
}
}
}
这里有三个V哥自己加的细节,值得单说:
- 冷、热启动都接 :只写
onCreate,应用本来就在后台时再扫一次码,你收不到第二次------因为走的是onNewWant。这个坑很隐蔽,不报错,只是"第二次扫码没反应"。 uiContext异步拿到 :getMainWindow()是 Promise,页面没加载完时uiContext还是空的,直接路由会空指针。教训是路由动作要等uiContext就绪。- 已经在目标页就别压栈:不加这个判断,用户连扫两次,回退键要按好几下才能出去。
四、第三步:把码值收口成一张路由表
接住 uri 之后,最忌讳的就是"在 ability 里写一堆 if-else 判断路径"。码一多,逻辑就糊了。
V哥的做法是把"路径前缀 → 目标页"抽成一张规则表,集中维护:
typescript
// common/ScanRouter.ets
import { hilog } from '@kit.PerformanceAnalysisKit';
import { url } from '@kit.ArkTS';
interface RouteRule {
pattern: string; // 路径前缀
page: string; // 目标页面
desc: string; // 业务含义,便于排查
}
export class ScanRouter {
// 码值注册到扫码直达后,系统只负责把用户送到这;
// 具体落到哪个服务页,由这张表决定 ------ 别让用户停在首页
private static rules: RouteRule[] = [
{ pattern: '/scan/pay', page: 'pages/Pay', desc: '扫支付码 → 支付页' },
{ pattern: '/scan/order', page: 'pages/OrderDetail', desc: '扫订单码 → 订单详情' },
{ pattern: '/scan/coupon', page: 'pages/Coupon', desc: '扫券码 → 领券页' },
];
static match(uri: string): string {
try {
const path: string = new url.URL(uri).pathname;
for (const rule of ScanRouter.rules) {
if (path.startsWith(rule.pattern)) {
return rule.page;
}
}
} catch (err) {
// 解析失败别静默,至少回首页,也方便你定位问题
hilog.error(0x0001, '[ScanDirect]', `URI 解析异常: ${err}`);
}
return 'pages/Index';
}
}
把路由收口成表,好处是:新增一种码,只改这张表,不动 ability。 这是工程上最划算的一笔。

五、两个验证标准:装了跳履约页,没装跳网页
写完代码,别急着上线。官方给了两条验收标准,V哥建议你逐条对着查(开发后验证):
| 标准 | 类型 | 你在验证什么 |
|---|---|---|
| 应用已安装跳转体验 | 规则(必须满足) | 扫支付码跳到支付页,不是首页 |
| 应用未安装跳转体验 | 建议 | 系统拉起浏览器,打开码值对应的网页 |

第一条是硬指标。V哥那个朋友的问题,恰恰就是扫完停在首页------他压根没做路由,URI 到了 Want 里就被忽略了。
V哥的铁律:履约页不是首页。 扫支付码,用户要的是支付,不是你的启动页。断在首页,就是断在体验上。
第二条是兜底。没装应用的用户,扫了你的码,应该被引到网页完成动作或下载------这条做不好,等于白白浪费一次触达。
六、6.1 的增强与边界
扫码直达本身依赖系统路由,应用侧改动不大。但 6.1 在 Scan Kit 上确实加了料,和你做扫码相关功能时能用上(Scan Kit 开发详解):
- 默认界面扫码标题动态适配 :
ScanOptions里把scanTypes限定成单一制式(如只QR_CODE),系统扫码页标题会自动变成"扫描二维码"或"扫描条形码"。 - Wearable 全支持:6.1 起,带后置相机的穿戴设备也能用默认/自定义界面扫码。
- 复杂场景算法加固:曲面码、小角度码、污损码、远距离小码识别率提升。
但要把话说清楚:这些是 Scan Kit 的增强,不是扫码直达的增强。 扫码直达的核心是"系统扫、应用落"的路由分发,应用侧那三步(注册域名、接住码值、路由履约页)在 6.1 的主线上没变。别把两件事混成一团去讲。
还有一个边界提醒:扫码直达要求中国境内,且必须真机验证 ------模拟器不支持相机相关扫码能力,图像识码部分可模拟器调试,完整链路必须以真机实测为准(Scan Kit · 支持设备)。
七、上线自检清单
把上面几点整理成一份可以贴进 PR 描述的清单:
- 域名已在 AGC 开通 App Linking,并配置到扫码直达服务了吗?
-
module.json5里关联了正确的域名吗? - 用的是 HTTPS 域名吗?(HTTP 不被接受)
- 用的是手动签名吗?(自动签名会导致扫码不跳)
-
EntryAbility的onCreate和onNewWant都 接住了want.uri吗? - 路由动作等
uiContext就绪后才执行吗? - 码值 → 页面的映射收口在一张路由表里了吗?还是散落的 if-else?
- 扫支付码 / 订单码,跳的是履约页而不是首页吗?
- 应用未安装时,码值对应的网页能正常打开吗?
- 连续扫码会不会重复压栈(已在目标页不再 push)?
- 中国境内以外发行的版本,有降级方案吗(此能力不生效)?
- 在真机上用控制中心扫码入口完整走通一遍了吗?
参考与出处
本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:
最后一句 :扫码直达真正难的不是代码------满打满算就三步。难的是把"系统扫、应用落"这件事分清楚:门票(域名)交给 App Linking,接住码值写进 EntryAbility,路由履约页收成一张表。前三件做对,用户一扫就到;少一件,用户就停在首页。