返回探索
指南VibeFix 编辑部更新于 2026年10月18日

点链接直达 App 内页:vibe 项目的深度链接实战

用户点了一个 https 链接,手机如何跳过浏览器直达你的 App 内页?三种链接选型、iOS AASA 五个魔鬼细节、Android 指纹头号死因、验证文件托管、智能落地页、FDL 下线后的延迟深度链接自建方案、微信生态特供与 10 项测试排错清单。

题图:深度链接主题配图,手机与应用跳转示意

先划三条界,免得和站内已有的指南撞车:本文不讲 App 上架提审的流程与被拒申诉——请移步《移动应用上架指南》;不讲分享链接的权限控制、过期与撤回,那是《分享链接指南》的地盘;也不讲二维码的生成、核销与统计,请看《二维码生态指南》。本文只回答一个具体问题:用户点了一个 https 链接,手机如何跳过浏览器、直接打开你的 App 并落到正确的页面?

深度链接是"分享增长"的技术地基:邀请链接、营销落地页、推送跳转,背后都是它。vibe 项目最常见的翻车现场有四种:第一,用了自定义 Scheme(myapp://),结果被其他 App 劫持,点链接打开了别人的应用;第二,Universal Links 配了半年,iOS 上死活不跳转,最后发现是 AASA 文件多了个 .json 后缀;第三,Android 上测试机能跳转,用户手机只弹选择框——Android 12 的验证收紧没人告诉你;第四,Firebase Dynamic Links 下线了,page.link 的短链全变 404,邀请链路一夜归零。本文按实战顺序讲透:三种链接的选型、iOS 实战、Android 实战、服务端域名配置、路由与参数设计、延迟深度链接、中国生态特供、测试排错清单。先看全景:

深度链接完整路由流程图:用户点击 https 链接后先做域名验证(iOS 查 AASA 文件、Android 查 assetlinks.json),验证通过且已安装则直达 App 内页,未安装则进智能落地页分发到应用商店,安装后首次启动走延迟深度链接还原参数

一、三种链接先分清:别一上来就配 Universal Links

很多人一张口就是"我要做 Universal Links",但深度链接有三种,每种解决的问题完全不同,选错就是返工:

自定义 Scheme(myapp://)iOS Universal LinksAndroid App Links
本质App 自己声明的私有协议,无任何验证https 链接 + 域名所有权验证https 链接 + 域名所有权验证
安全性谁都能声明同一个 scheme,可被劫持只有域名所有者能关联 App,防劫持只有域名所有者能关联 App,防劫持
用户体验未安装时直接报错,无优雅回退未安装自动回退到网页未安装自动回退到网页
适用场景App 内部跳转、开发调试所有对外的营销外链、分享链接所有对外的营销外链、分享链接

决策表很直接:营销外链、用户间分享的邀请链接,一律用 Universal Links / App Links——因为只有它们经过域名验证,不会被劫持,且未安装时能优雅回退到网页;App 内部页面跳转(比如 H5 页面调起 App 内某个原生页)可以用自定义 Scheme,简单直接;开发调试时用自定义 Scheme 最省事,不用配域名验证文件。vibe 项目的一个典型错误是"图省事全用自定义 Scheme 做分享",等用户量上来后被恶意 App 劫持 scheme,点邀请链接打开了山寨应用——这个坑的修复成本是"全量换链",越早上 Universal Links 越便宜。

还有一个 2026 年必须知道的背景:Firebase Dynamic Links 已于 2025-08-25 正式下线,所有 page.link 域名的短链现在返回 404,SDK 也已移除。如果你是从老教程抄的方案、或者接手了一个 2024 年的老项目,先检查有没有 page.link 的残留——有的话,第六节的迁移方案就是为你写的。

二、iOS Universal Links 实战:AASA 文件的五个魔鬼细节

Universal Links 的原理一句话:iOS 发现用户点击的 https 链接域名在 App 的关联域名列表里,就会去该域名的 /.well-known/apple-app-site-association 拉一个 JSON 文件验证"这个域名确实允许这个 App 接管这些路径",验证通过才直达 App。整个链路有五个魔鬼细节,错一个就不跳转,且 iOS 不会告诉你错在哪:

细节一:文件名无 .json 后缀。路径必须是 https://example.com/.well-known/apple-app-site-association,不是 apple-app-site-association.json。这是翻车率最高的一项——直觉上 JSON 文件都该有后缀,但 Apple 的规定就是没有。

细节二:必须 HTTPS,且 Content-Type 须为 application/json。文件内容哪怕完全正确,只要响应头是 text/plain,iOS 就拒收。很多静态托管默认按扩展名给 Content-Type,而这个文件没有扩展名,最容易中招。Nginx / Next.js 都要显式配响应头(见第四节)。

细节三:appID 是 TeamID.BundleID 拼接。TeamID 是 Apple Developer 后台 10 位字符的团队标识,不是 App 名称。格式示例:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "ABCDE12345.com.example.myapp",
        "components": [
          { "/": "/invite/*", "comment": "邀请链接直达 App" },
          { "/": "/p/*", "comment": "内容页直达 App" },
          { "/": "/admin/*", "exclude": true, "comment": "后台路径不接管" }
        ]
      }
    ]
  }
}

细节四:components 匹配规则。新版 AASA 用 components(老版 paths 仍兼容,但新项目直接用 components)。每条规则按顺序匹配:/invite/* 是通配,exclude: true 是排除。注意大小写敏感——/Invite/abc 匹配不上 /invite/*。另外 ? 和 # 后面的 query/fragment 默认不参与路径匹配,如果你的路由靠 query 参数区分(比如 /?page=invite&code=abc),要用 components 的 "?": { "code": "*" } 写法显式声明。

细节五:Xcode 里声明 Associated Domains。在 Signing & Capabilities 里加 Associated Domains,条目写 applinks:example.com(不要带 https://,不要带路径)。子域名要逐个声明:applinks:example.com 管不了 m.example.com,后者要单独加一条,AASA 文件也要在 m.example.com 下单独放一份。

App 内接收入口,SwiftUI 一行即可:

// SwiftUI:系统验证通过后,URL 会直接送到这里
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL { url in
          // url = https://example.com/invite/abc123
          DeepLinkRouter.shared.handle(url) // 解析路径 → 跳邀请页并消费 code
        }
    }
  }
}

最后是 Universal Link 失效时的回退:如果用户在 Safari 里点了链接右上角的"在浏览器中打开"(iOS 会记住这个选择),之后这个域名的链接就永远走 Safari,直到用户长按链接选"在 App 中打开"才能恢复。这是 iOS 的系统行为,不是你的 bug——但你要在 FAQ 里写清楚,否则客服会被"我点了链接但没打开 App"的工单淹没。

三、Android App Links 实战:指纹配错是头号死因

Android 的原理与 iOS 对称:系统发现点击的 https 链接域名在 App 的 intent-filter 里声明了 autoVerify,就去拉 https://example.com/.well-known/assetlinks.json 验证"这个 App(包名 + 签名指纹)确实被域名所有者授权",通过才直达。

验证文件内容:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": ["AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12"]
  }
}]

AndroidManifest 里的声明:

<activity android:name=".DeepLinkActivity" android:exported="true">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="example.com" />
  </intent-filter>
</activity>

三个关键点,个个都是血泪:

  • android:autoVerify="true" 不能漏。漏了它,系统根本不会去拉 assetlinks.json,每次点击都弹"用哪个应用打开"的选择框。很多从 Stack Overflow 抄的 intent-filter 示例没有这一行。
  • debug 和 release 的签名指纹不一样,这是头号死因。开发时用 debug keystore 签的包,assetlinks.json 里配的却是 release 指纹(或反过来),结果"开发机上能跳转,线上包不行"。正确做法是把两个指纹都写进 sha256_cert_fingerprints 数组——它是个数组,就是用来放多个的。取指纹命令:keytool -list -v -keystore your.keystore,找 SHA256 那一行。
  • Android 12+ 验证收紧。从 Android 12 开始,如果域名验证没通过,系统不再自动直达,只弹应用选择框让用户手动选。以前"配错了也能用,只是多弹个框"的时代结束了——现在配错就等于功能残废。更狠的是,用户一旦在选择框里选了"浏览器",系统会记住,下次连框都不弹。

多 flavor / 多包名的项目注意:每个包名(包括 .debug、.staging 后缀的)都要在 assetlinks.json 里单独写一个 target 条目,或者给测试环境单独配一个域名。别指望一个条目覆盖所有变体。

四、域名与服务端侧:两个验证文件的托管

两个验证文件都是纯静态文件,但托管时有四个服务端侧要求,做不对前面两节全白费:

  • .well-known 路径不能被应用路由吞掉。Next.js 项目里,/.well-known/assetlinks.json 可能会被 App Router 的 catch-all 路由拦截。解法是用 rewrites 把这两个路径指向 public/.well-known/ 下的静态文件,或者直接放在 public/.well-known/ 里让 Next.js 当静态资源 serve。部署后第一件事就是 curl 一下两个 URL,确认返回的是文件内容而不是 404 页面或你的首页 HTML。
  • Content-Type 必须正确。AASA 文件(无扩展名)必须返回 application/json。Next.js 的 next.config.js 里用 headers 显式指定:
// next.config.js
async headers() {
  return [
    {
      source: '/.well-known/apple-app-site-association',
      headers: [
        { key: 'Content-Type', value: 'application/json' },
        { key: 'Cache-Control', value: 'no-store' },
      ],
    },
    {
      source: '/.well-known/assetlinks.json',
      headers: [{ key: 'Cache-Control', value: 'no-store' }],
    },
  ];
}
  • CDN 不要缓存这两个文件。这是个隐蔽但致命的坑:你更新了 AASA 文件(比如加了新路径),CDN 还在吐旧缓存,iOS 拉到的是旧文件,新路径死活不跳转。两个验证文件一律 Cache-Control: no-store,流量小到可以忽略,缓存收益为零。
  • HTTP 强制跳 HTTPS,子域名逐个声明。验证请求只走 HTTPS,http:// 的验证文件没人看,所以全站 HTTP→HTTPS 跳转是前提。子域名方面,example.com 和 www.example.com 在 Apple/Google 眼里是两个域名,AASA/assetlinks.json 要各放一份,App 里也要各声明一条。决定用哪个域名做分享链接时,全站只用一个,别混用。

五、路由与参数设计:路径即契约

验证链路通了之后,真正决定体验的是路由设计。核心原则:路径即契约——App 内外用同一套路由表。https://example.com/invite/abc123 在浏览器里打开是邀请落地页,在 App 里打开是邀请原生页,路径语义完全一致。这样做的好处是:分享链接天然就是深度链接,不需要维护两套映射;SEO 和 App 索引也能复用同一套 URL。

路由设计三条实践:第一,路径承载资源定位,query 承载一次性参数:/p/:id 定位内容,/invite/:code 定位邀请,query 只放 utm_source 这类追踪参数和签名;第二,query 参数要签名防篡改:邀请码、优惠金额这类敏感参数不能裸奔——签名方案(HMAC + 过期时间)的完整实现见《分享链接指南》的参数签名一节,本文不重复,记住"深度链接只负责送达,信任要靠签名";第三,路由表要有版本意识:老版本 App 不认识新路径时,要能优雅降级到首页而不是白屏,建议路由解析失败时统一落到首页 + toast 提示"请升级 App 查看完整内容"。

然后是"未安装"分支的智能落地页:用户没装 App,点链接先到一个中间页,按 User-Agent 分发:

// 智能落地页核心逻辑(服务端渲染或纯前端均可)
const ua = navigator.userAgent;
if (/iPhone|iPad|iPod/.test(ua)) {
  location.href = 'https://apps.apple.com/app/id123456789'; // App Store
} else if (/Android/.test(ua)) {
  location.href = 'https://play.google.com/store/apps/details?id=com.example.myapp';
} else {
  location.href = 'https://example.com/invite/abc123'; // 桌面端直接看 Web 版
}

智能落地页有三个进阶要求:第一,页面要轻——它是个中转页,首屏 1 秒内必须完成跳转决策,别放轮播图;第二,给用户"直接看网页版"的出口,别强制所有人都去下载 App,很多人只是想看一眼内容;第三,把原始链接参数带到商店——用户安装后首次打开要能还原上下文,这就是下一节延迟深度链接要解决的问题,智能落地页需要在跳转前把参数存下来(见第六节的自建方案)。

六、延迟深度链接:FDL 下线之后怎么办

延迟深度链接(Deferred Deep Link)解决的是最痛的一个场景:用户没装 App,点了邀请链接 → 跳到应用商店 → 安装 → 首次打开——这时候 App 怎么知道当初点的是哪个邀请链接?没有延迟深度链接,邀请归因就断了,拉新激励发不出去。

先说背景,因为这直接决定了你的方案选型:Firebase Dynamic Links 已于 2025-08-25 正式下线,page.link 短链现在全部 404。如果你还在用 FDL,迁移是必答题,不是选答题。三条替代路线:

路线代表成本适合谁
第三方归因全家桶Branch、AppsFlyer OneLink、Adjust按量付费,量大后不便宜有投放、有归因需求的增长型产品
自建install referrer + 设备指纹匹配开发成本 2~3 天,几乎零边际成本日新增 <500 的 vibe 项目(绝大多数)
原生直连(无延迟)纯 Universal Links / App Links零成本不需要邀请归因、只做内容直达的项目

决策线很清晰:日新增低于 500,自建完全够用,别急着给 Branch 交钱。自建方案的原理分两步:第一步,用户点链接进智能落地页时,服务端记录一条"点击事件":{ IP、UA、时间戳、链接参数、生成的 click_id },同时把 click_id 写进 cookie/localStorage;第二步,用户安装后首次启动 App,App 把"安装事件"(IP、UA、时间戳、应用市场来源)上报给服务端,服务端在点击事件里按"IP + UA + 时间窗口(比如 1 小时内)"做模糊匹配,命中就把当时的链接参数下发给 App,完成归因。

但必须把自建方案的精度上限和隐私边界讲清楚,这是很多人踩坑的地方:精度方面,同一 Wi-Fi 下的多个用户 IP 相同、UA 相近,匹配可能串——所以自建方案只适合"邀请归因"这种允许小概率误差的场景,不适合"按归因发钱"的精确结算;隐私方面,iOS 14.5+ 的 ATT 框架和 Android 的隐私沙盒都在收紧设备标识,不要碰 IDFA/IMEI 这类强标识,用"IP + UA + 时间窗口"的弱匹配是合规上限。另外,Android 的 Play Install Referrer API 是官方推荐的归因通道(用户从 Google Play 安装时,商店会把 referrer 透传给 App),有条件优先用它,比指纹匹配准一个量级。

迁移 checklist(给还在用 FDL 的项目):第一,全量扫描代码和数据库里的 page.link 链接,列出清单;第二,搭好 Universal Links / App Links 的验证链路(第二、三节);第三,选第三方或自建补上延迟归因;第四,老链接做 301 跳转到新域名下的等效路径,别直接 404——已经发出、印在物料上的二维码和链接还在外面流通。

七、中国生态特供:微信里点不开不是你的 bug

如果你的用户在中国,这节是必读——因为在国内,深度链接最大的敌人不是技术,是超级 App 的围墙花园。

微信 / QQ 内置浏览器会屏蔽 Universal Links 和 App Links 的系统级跳转。用户在微信聊天里点你的 https 链接,打开的是微信内置浏览器,iOS/Android 的系统级域名验证根本不会触发——你的 App 再怎么配都是徒劳。这是平台限制,不是你的 bug。标准解法是做一个"在浏览器中打开"引导页:检测到 UA 是 MicroMessenger/QQ 时,不直接跳应用商店,而是显示一个引导页,用箭头动画告诉用户"点击右上角···,选择在浏览器中打开"。虽然多了一步,但这是目前唯一可靠的路径。注意引导页的文案要直白:"微信里打不开是正常的,按图操作 10 秒搞定"——把锅甩给平台,反而能降低用户 frustration。

应用商店分发也有中国特色:应用宝 / 华为应用市场支持直达参数。智能落地页按 UA 分发时,国内 Android 用户应该分发到应用宝或华为应用市场(按手机品牌判断),而不是 Google Play(国内不可用)。这两个市场都支持在下载链接里带自定义参数,可以作为延迟归因的补充通道——具体参数格式看各市场的开放平台文档,每年可能有小变化,本文不展开。

企业微信 / 钉钉的分享卡片是另一个坑:它们对链接的抓取(生成卡片标题、摘要图)有自己的爬虫和缓存策略,刚上线的页面分享出去可能显示"该链接无法访问"或旧标题。解法是:分享页的 OG 标签(og:title、og:image)要服务端渲染输出,别靠客户端 JS;换了标题后,用企业微信/钉钉的"刷新分享卡片"工具(或换个带随机参数的 URL)清缓存。

最后,域名防封和二维码生成见《二维码生态指南》,本文只讲"点开之后"的路由。但有一条要提前说:分享域名和主站域名分离——用 go.example.com 这类短域名做分享跳转,即使被微信封了也不影响主站。这是国内做分享增长的基本操作,域名规划阶段就要定下来。

八、测试与排错清单:10 项

深度链接的调试是"玄学重灾区",因为链路跨了 DNS、CDN、操作系统三方,任何一方缓存都可能让你"改了但没生效"。按这个清单逐项排查:

  1. iOS 备忘录测试法:把链接粘贴到备忘录,长按——如果弹出"在 XXX App 中打开",说明 Universal Links 生效。这是最快的冒烟测试,不用打包不用 TestFlight。
  2. AASA 文件直连验证:curl -v https://example.com/.well-known/apple-app-site-association,确认三点:200(不是 404 页面的 200)、Content-Type 是 application/json、内容是你最新改的版本。
  3. 排查 AASA 被 CDN 缓存:如果 curl 到的是旧内容,检查 CDN 缓存规则,确认这两个验证文件是 no-store。iOS 拉取 AASA 有自己的缓存(App 安装/更新时拉一次),改完文件后要删 App 重装才能让系统重新拉取——这是"我改了文件但手机上还是旧行为"最常见的原因。
  4. swcutil 验证:macOS 自带的 swcutil 可以检查系统侧的域名关联状态,比如 swcutil verify -d example.com。如果系统侧显示未关联,问题在文件或签名,不在你的 App 代码。
  5. Android adb 直测:adb shell am start -a android.intent.action.VIEW -d "https://example.com/invite/abc123",看是直达 App、弹选择框还是进了浏览器。三种结果分别对应:验证通过 / 验证失败(或没配 autoVerify)/ 完全没接管。
  6. Digital Asset Links 在线验证:Google 提供了官方验证 API,拼出 URL 在浏览器打开即可看到验证结果:
    https://digitalassetlinks.googleapis.com/v1/assetlinks:check?source.android_app.package_name=com.example.myapp&source.android_app.certificate.sha256_fingerprint=AB:CD:EF:...&relation=delegate_permission/common.handle_all_urls&target.web.site=https://example.com
    返回 {"linked": true} 才算服务端侧完全 OK。
  7. debug/release 指纹对照:分别用 debug 包和 release 包跑一遍 adb 测试,确认 assetlinks.json 里两个指纹都在。只测 debug 包是自欺欺人。
  8. 微信/QQ 内置浏览器专项测试:真机微信里点链接,确认出现的是"在浏览器中打开"引导页,而不是白屏或报错。模拟器没有微信环境,必须真机。
  9. 线上监控三指标:链接点击 → App 直达成功率、回退到网页的 fallback 率、延迟归因的匹配成功率。fallback 率突然升高,八成是 AASA/assetlinks 文件出了问题(比如证书续期后 HTTPS 配置变了);归因匹配率掉,查 IP 匹配逻辑或 referrer 透传。
  10. 上线前 10 项复核:AASA 无 .json 后缀 ✓;Content-Type 正确 ✓;appID 的 TeamID 对 ✓;components 大小写 ✓;Associated Domains 逐条声明 ✓;assetlinks.json 指纹含 debug+release ✓;intent-filter 有 autoVerify ✓;CDN 不缓存验证文件 ✓;子域名各放一份 ✓;老 page.link 做了 301 ✓。

最后说一句判断:深度链接是个"配置重于代码"的功能——App 内的路由处理代码不到 100 行,真正的功夫全在域名验证文件的五个细节、Android 指纹、CDN 缓存这三处。vibe 项目做它最大的优势是船小好掉头:域名规划、短域名分离、签名指纹管理,一开始就按本文的清单做对,后面用户量涨起来时不用返工。而返工的代价(全量换链、老链接 301、应用商店参数迁移)是初次做对的十倍。先花一下午把验证链路调通,再谈增长。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

题图:数据中心机房内成排的服务器机柜与蓝色网线,象征网站边缘防护基础设施
指南
网页水印与防爬实战:保护你的内容、图片和 API 不被薅秃

爬虫 24 小时批量搬走你的内容、图片和商品数据怎么办?从 Cloudflare 四层检测、JA3/JA4 握手指纹、抓爬检测规则,到签名 URL、水印取证与投入产出清单——一份算过账的中小站长防爬实战指南。

部署上线后端工程独立开发
城市高空俯拍照片:暖色暮光下的密集街区与高楼,隐喻城市位置服务
指南
“附近的咖啡馆”怎么做:vibe 项目的位置与地图功能实战

从“附近的咖啡馆”到骑手实时位置:入库只存 WGS84、展示层再转 GCJ-02/BD-09,附近搜索走粗排精排两阶段,地图 SDK 先看合规再看价格,逆地理编码按 geohash 缓存,精确位置从采集侧就当敏感个人信息对待。

移动应用后端工程独立开发