HarmonyOS Web Scheme Handler:支持“外链确认弹窗”的工程级写法
HarmonyOS Web Scheme Handler:支持“外链确认弹窗”的工程级写法
做 Web 组件时,外链治理其实就两个目标:
- 安全:别让 Web 随便把用户带到未知站点
- 体验:别“一刀切”拦死,用户想去也给个选择
所以我在项目里基本都会加一个“外链确认弹窗”:
当 Web 要跳到白名单外的 URL(或某些 Scheme),先弹出提示:
“即将离开应用,是否继续?”
- 继续:用系统浏览器打开
- 取消:留在当前页
下面给你一套工程里能长期复用的写法:
✅ 支持白名单/黑名单
✅ 支持常见 scheme(tel/mailto/weixin/alipays 等)
✅ 支持外链确认弹窗
✅ 拦截后不让 Web 自己加载(避免报错/体验乱)
说明:Web 的“URL 将要加载”拦截回调在不同 API 版本命名会略有差异(有的叫 onLoadIntercept / shouldOverrideUrlLoading 风格),但核心逻辑一致:拿到 URL → 判断 → return 拦截/放行。
另外注意:POST 请求通常不会触发这个回调,别把它当万能网关。
1)一套可复用的“决策器”:判断放行/拦截/外部打开
type Decision =
| { action: 'ALLOW'; reason: string }
| { action: 'BLOCK'; reason: string }
| { action: 'EXTERNAL_CONFIRM'; reason: string };
const ALLOW_SCHEMES = new Set(['https', 'http']);
const ALLOW_HOSTS = new Set([
'www.example.com',
'm.example.com'
]);
const ALLOW_HOST_SUFFIX = ['.example.com']; // *.example.com
const BLOCK_HOSTS = new Set([
'ads.bad.com',
'tracker.bad.net'
]);
function safeParse(rawUrl: string): URL | null {
try { return new URL(rawUrl); } catch { return null; }
}
function hostInAllowlist(host: string): boolean {
if (ALLOW_HOSTS.has(host)) return true;
return ALLOW_HOST_SUFFIX.some(suf => host === suf.slice(1) || host.endsWith(suf));
}
function decideUrl(rawUrl: string): Decision {
const u = safeParse(rawUrl);
if (!u) return { action: 'BLOCK', reason: 'URL parse failed' };
const scheme = u.protocol.replace(':', '').toLowerCase();
const host = u.hostname.toLowerCase();
// 黑名单优先
if (BLOCK_HOSTS.has(host)) {
return { action: 'BLOCK', reason: `blocked host: ${host}` };
}
// 非 http/https:交给 scheme handler 处理(这里先让外层判断)
if (!ALLOW_SCHEMES.has(scheme)) {
return { action: 'EXTERNAL_CONFIRM', reason: `non-http scheme: ${scheme}` };
}
// 白名单放行
if (hostInAllowlist(host)) {
return { action: 'ALLOW', reason: 'allow by whitelist' };
}
// 不在白名单 → 外链确认
return { action: 'EXTERNAL_CONFIRM', reason: `not in whitelist: ${host}` };
}
2)外链确认弹窗:用户点“继续”才打开系统浏览器
这里用 ArkUI 的
AlertDialog,逻辑简单又稳定。
import { UIContext } from '@kit.ArkUI'; // API 不同版本可能是 UIAbilityContext / getContext(this)
import { wantAgent } from '@kit.AbilityKit';
function openExternalBySystem(url: string, uiContext: UIContext) {
// 用系统能力打开(不同版本可用 startAbility / wantAgent)
// 这里给一个“意图打开”的通用写法(你按工程实际 API 调整)
uiContext.getHostContext()?.startAbility({
uri: url
});
}
function showExternalConfirm(url: string, uiContext: UIContext, onCancel?: () => void) {
AlertDialog.show({
title: '即将离开应用',
message: `将通过系统浏览器打开:\n${url}`,
primaryButton: {
value: '取消',
action: () => onCancel?.()
},
secondaryButton: {
value: '继续',
action: () => openExternalBySystem(url, uiContext)
}
});
}
小建议:message 里把 URL 展示出来,用户会更安心,也能减少误点投诉。
3)把它接进 Web 拦截回调(核心集成点)
你在 Web 组件创建处加入拦截逻辑:
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct WebWithExternalConfirm {
private controller: webview.WebviewController = new webview.WebviewController();
build() {
const uiContext = getContext(this); // HarmonyOS 常用获取 UIContext 的方式
Column() {
Web({ src: 'https://www.example.com', controller: this.controller })
.javaScriptAccess(true)
// ⚠️ 这里的方法名/返回值语义要以你工程 API 版本为准
// 目标:在“URL 将要加载”时拿到 url,然后决定拦不拦
.onLoadIntercept?.((event) => {
const url = event?.url ?? event?.data?.url ?? '';
if (!url) return false;
const d = decideUrl(url);
console.info(`[NavGuard] ${d.action} ${url} | ${d.reason}`);
if (d.action === 'ALLOW') {
return false; // 放行:让 Web 正常加载(若你版本语义相反就换成 true)
}
if (d.action === 'BLOCK') {
// 你可以 toast 一句“已拦截不安全链接”
return true; // 拦截:不让 Web 加载
}
// EXTERNAL_CONFIRM
showExternalConfirm(url, uiContext);
return true; // 拦截 Web 内打开,交给系统浏览器
})
}
.width('100%')
.height('100%')
}
}
4)把 Scheme 单独做“更人性化”的处理(推荐)
很多 Scheme 不应该弹“外链确认”,比如:
tel:应该直接弹拨号mailto:应该打开邮件weixin:/alipays:你可以弹一次确认(更稳)
你可以把 decideUrl() 的“非 http/https”分支拆成:
function decideScheme(rawUrl: string): Decision {
const u = safeParse(rawUrl);
if (!u) return { action: 'BLOCK', reason: 'URL parse failed' };
const scheme = u.protocol.replace(':', '').toLowerCase();
if (scheme === 'tel' || scheme === 'mailto') {
return { action: 'EXTERNAL_CONFIRM', reason: `system scheme: ${scheme}` }; // 也可不弹窗直接打开
}
if (scheme === 'weixin' || scheme === 'alipays') {
return { action: 'EXTERNAL_CONFIRM', reason: `payment scheme: ${scheme}` };
}
return { action: 'BLOCK', reason: `unknown scheme: ${scheme}` };
}
然后在拦截回调里:
- scheme 是 tel/mailto → 直接
startAbility(uri=url) - scheme 是 weixin/alipays → 弹窗确认后再打开
- 其它未知 scheme → 拦截并提示
这样体验会更像“正经 App”,也更安全。
5)你一定会遇到的两个坑(提前说清)
坑 1:回调返回值语义不一致
不同 API/版本里,拦截回调可能是:
return true表示“我处理了,别加载”return false表示“我不处理,继续加载”
也可能反过来。
你用一次日志验证就行:
点一个外链,看 Web 是否还在内部打开,如果还打开了,说明 return 值要反一下。
坑 2:POST 请求不触发拦截回调
这个官方明确提到过:POST 请求一般不会触发 URL 将要加载回调。
所以这套更适合做“导航层拦截”,不是拦所有接口请求。
6)一句话总结(博客收尾)
外链确认弹窗的本质不是“限制用户”,而是“把 Web 的不确定性收口到应用可控的体验里”。
该放的放、该拦的拦,用户知道发生了什么,你也能兜住安全底线。

浙公网安备 33010602011771号