HarmonyOS Web Scheme Handler:支持“外链确认弹窗”的工程级写法

HarmonyOS Web Scheme Handler:支持“外链确认弹窗”的工程级写法

鸿蒙第四期开发者活动

做 Web 组件时,外链治理其实就两个目标:

  1. 安全:别让 Web 随便把用户带到未知站点
  2. 体验:别“一刀切”拦死,用户想去也给个选择

所以我在项目里基本都会加一个“外链确认弹窗”:

当 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 的不确定性收口到应用可控的体验里”。
该放的放、该拦的拦,用户知道发生了什么,你也能兜住安全底线。

posted @ 2025-12-18 16:28  骑老爷爷过马路  阅读(4)  评论(0)    收藏  举报