capacitor-deep-linking
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDeep Linking in Capacitor
Capacitor中的深度链接
Implement deep links, universal links, and app links in Capacitor apps.
在Capacitor应用中实现深度链接、通用链接和App Links。
When to Use This Skill
何时使用此技能
- User wants deep links
- User needs universal links
- User asks about URL schemes
- User wants to open app from links
- User needs share links
- 用户需要深度链接
- 用户需要通用链接
- 用户询问URL方案相关问题
- 用户希望通过链接打开应用
- 用户需要分享链接
Types of Deep Links
深度链接的类型
| Type | Platform | Format | Requires Server |
|---|---|---|---|
| Custom URL Scheme | Both | | No |
| Universal Links | iOS | | Yes |
| App Links | Android | | Yes |
| 类型 | 平台 | 格式 | 是否需要服务器 |
|---|---|---|---|
| 自定义URL方案 | 双平台 | | 否 |
| Universal Links | iOS | | 是 |
| App Links | Android | | 是 |
Quick Start
快速开始
Install Plugin
安装插件
bash
bun add @capacitor/app
bunx cap syncbash
bun add @capacitor/app
bunx cap syncHandle Deep Links
处理深度链接
typescript
import { App } from '@capacitor/app';
// Listen for deep link opens
App.addListener('appUrlOpen', (event) => {
console.log('App opened with URL:', event.url);
// Parse and navigate
const url = new URL(event.url);
handleDeepLink(url);
});
function handleDeepLink(url: URL) {
// Custom scheme: myapp://product/123
// Universal link: https://myapp.com/product/123
const path = url.pathname || url.host + url.pathname;
// Route based on path
if (path.startsWith('/product/')) {
const productId = path.split('/')[2];
navigateTo(`/product/${productId}`);
} else if (path.startsWith('/user/')) {
const userId = path.split('/')[2];
navigateTo(`/profile/${userId}`);
} else if (path === '/login') {
navigateTo('/login');
} else {
navigateTo('/');
}
}typescript
import { App } from '@capacitor/app';
// 监听深度链接打开事件
App.addListener('appUrlOpen', (event) => {
console.log('应用通过以下URL打开:', event.url);
// 解析并导航
const url = new URL(event.url);
handleDeepLink(url);
});
function handleDeepLink(url: URL) {
// 自定义方案: myapp://product/123
// 通用链接: https://myapp.com/product/123
const path = url.pathname || url.host + url.pathname;
// 根据路径路由
if (path.startsWith('/product/')) {
const productId = path.split('/')[2];
navigateTo(`/product/${productId}`);
} else if (path.startsWith('/user/')) {
const userId = path.split('/')[2];
navigateTo(`/profile/${userId}`);
} else if (path === '/login') {
navigateTo('/login');
} else {
navigateTo('/');
}
}Custom URL Scheme
自定义URL方案
iOS Configuration
iOS配置
xml
<!-- ios/App/App/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
<string>myapp-dev</string>
</array>
</dict>
</array>xml
<!-- ios/App/App/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
<string>myapp-dev</string>
</array>
</dict>
</array>Android Configuration
Android配置
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- Deep link intent filter -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- 深度链接意图过滤器 -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
</activity>Test Custom Scheme
测试自定义方案
bash
undefinedbash
undefinediOS Simulator
iOS模拟器
xcrun simctl openurl booted "myapp://product/123"
xcrun simctl openurl booted "myapp://product/123"
Android
Android
adb shell am start -a android.intent.action.VIEW -d "myapp://product/123"
undefinedadb shell am start -a android.intent.action.VIEW -d "myapp://product/123"
undefinedUniversal Links (iOS)
Universal Links(iOS)
1. Enable Associated Domains
1. 启用关联域名
In Xcode:
- Select App target
- Signing & Capabilities
-
- Capability > Associated Domains
- Add:
applinks:myapp.com
在Xcode中:
- 选择应用目标
- 进入Signing & Capabilities
-
- Capability > Associated Domains
- 添加:
applinks:myapp.com
2. Create apple-app-site-association
2. 创建apple-app-site-association文件
Host at :
https://myapp.com/.well-known/apple-app-site-associationjson
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.yourcompany.yourapp",
"paths": [
"/product/*",
"/user/*",
"/invite/*",
"NOT /api/*"
]
}
]
}
}Requirements:
- Served over HTTPS
- Content-Type:
application/json - No redirects
- File at root domain
托管在:
https://myapp.com/.well-known/apple-app-site-associationjson
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.yourcompany.yourapp",
"paths": [
"/product/*",
"/user/*",
"/invite/*",
"NOT /api/*"
]
}
]
}
}要求:
- 通过HTTPS提供服务
- Content-Type:
application/json - 无重定向
- 文件位于根域名下
3. Info.plist
3. Info.plist配置
xml
<!-- ios/App/App/Info.plist -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.com</string>
<string>applinks:www.myapp.com</string>
</array>xml
<!-- ios/App/App/Info.plist -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.com</string>
<string>applinks:www.myapp.com</string>
</array>Verify Universal Links
验证Universal Links
bash
undefinedbash
undefinedValidate AASA file
验证AASA文件
Check Apple CDN cache
检查Apple CDN缓存
undefinedundefinedApp Links (Android)
App Links(Android)
1. Create assetlinks.json
1. 创建assetlinks.json文件
Host at :
https://myapp.com/.well-known/assetlinks.jsonjson
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]托管在:
https://myapp.com/.well-known/assetlinks.jsonjson
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"
]
}
}
]Get SHA256 Fingerprint
获取SHA256指纹
bash
undefinedbash
undefinedDebug keystore
调试密钥库
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Release keystore
发布密钥库
keytool -list -v -keystore release.keystore -alias your-alias
keytool -list -v -keystore release.keystore -alias your-alias
From APK
从APK获取
keytool -printcert -jarfile app-release.apk
undefinedkeytool -printcert -jarfile app-release.apk
undefined2. AndroidManifest.xml
2. AndroidManifest.xml配置
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- App Links intent filter -->
<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" />
<data android:host="myapp.com" />
<data android:pathPrefix="/product" />
<data android:pathPrefix="/user" />
<data android:pathPrefix="/invite" />
</intent-filter>
</activity>xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
<!-- App Links意图过滤器 -->
<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" />
<data android:host="myapp.com" />
<data android:pathPrefix="/product" />
<data android:pathPrefix="/user" />
<data android:pathPrefix="/invite" />
</intent-filter>
</activity>Verify App Links
验证App Links
bash
undefinedbash
undefinedValidate assetlinks.json
验证assetlinks.json
Use Google's validator
使用Google验证工具
Check link handling on device
在设备上检查链接处理情况
adb shell pm get-app-links com.yourcompany.yourapp
undefinedadb shell pm get-app-links com.yourcompany.yourapp
undefinedAdvanced Routing
高级路由
React Router Integration
React Router集成
typescript
import { App } from '@capacitor/app';
import { useHistory } from 'react-router-dom';
import { useEffect } from 'react';
function DeepLinkHandler() {
const history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (event) => {
const url = new URL(event.url);
const path = getPathFromUrl(url);
// Navigate using React Router
history.push(path);
});
// Check if app was opened with URL
App.getLaunchUrl().then((result) => {
if (result?.url) {
const url = new URL(result.url);
const path = getPathFromUrl(url);
history.push(path);
}
});
}, []);
return null;
}
function getPathFromUrl(url: URL): string {
// Handle both custom scheme and https
if (url.protocol === 'myapp:') {
return '/' + url.host + url.pathname;
}
return url.pathname + url.search;
}typescript
import { App } from '@capacitor/app';
import { useHistory } from 'react-router-dom';
import { useEffect } from 'react';
function DeepLinkHandler() {
const history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (event) => {
const url = new URL(event.url);
const path = getPathFromUrl(url);
// 使用React Router导航
history.push(path);
});
// 检查应用是否通过URL打开
App.getLaunchUrl().then((result) => {
if (result?.url) {
const url = new URL(result.url);
const path = getPathFromUrl(url);
history.push(path);
}
});
}, []);
return null;
}
function getPathFromUrl(url: URL): string {
// 处理自定义方案和https链接
if (url.protocol === 'myapp:') {
return '/' + url.host + url.pathname;
}
return url.pathname + url.search;
}Vue Router Integration
Vue Router集成
typescript
import { App } from '@capacitor/app';
import { useRouter } from 'vue-router';
import { onMounted } from 'vue';
export function useDeepLinks() {
const router = useRouter();
onMounted(async () => {
App.addListener('appUrlOpen', (event) => {
const path = parseDeepLink(event.url);
router.push(path);
});
const launchUrl = await App.getLaunchUrl();
if (launchUrl?.url) {
const path = parseDeepLink(launchUrl.url);
router.push(path);
}
});
}typescript
import { App } from '@capacitor/app';
import { useRouter } from 'vue-router';
import { onMounted } from 'vue';
export function useDeepLinks() {
const router = useRouter();
onMounted(async () => {
App.addListener('appUrlOpen', (event) => {
const path = parseDeepLink(event.url);
router.push(path);
});
const launchUrl = await App.getLaunchUrl();
if (launchUrl?.url) {
const path = parseDeepLink(launchUrl.url);
router.push(path);
}
});
}Deferred Deep Links
延迟深度链接
Handle links when app wasn't installed:
typescript
import { App } from '@capacitor/app';
import { Preferences } from '@capacitor/preferences';
// On first launch, check for deferred link
async function checkDeferredDeepLink() {
const { value: isFirstLaunch } = await Preferences.get({ key: 'firstLaunch' });
if (isFirstLaunch !== 'false') {
await Preferences.set({ key: 'firstLaunch', value: 'false' });
// Check with your attribution service
const deferredLink = await fetchDeferredLink();
if (deferredLink) {
handleDeepLink(new URL(deferredLink));
}
}
}处理应用未安装时的链接:
typescript
import { App } from '@capacitor/app';
import { Preferences } from '@capacitor/preferences';
// 首次启动时,检查延迟链接
async function checkDeferredDeepLink() {
const { value: isFirstLaunch } = await Preferences.get({ key: 'firstLaunch' });
if (isFirstLaunch !== 'false') {
await Preferences.set({ key: 'firstLaunch', value: 'false' });
// 调用归因服务检查
const deferredLink = await fetchDeferredLink();
if (deferredLink) {
handleDeepLink(new URL(deferredLink));
}
}
}Query Parameters
查询参数
typescript
App.addListener('appUrlOpen', (event) => {
const url = new URL(event.url);
// Get query parameters
const source = url.searchParams.get('source');
const campaign = url.searchParams.get('campaign');
const referrer = url.searchParams.get('ref');
// Track attribution
analytics.logEvent('deep_link_open', {
path: url.pathname,
source,
campaign,
referrer,
});
// Navigate with state
navigateTo(url.pathname, {
state: { source, campaign, referrer },
});
});typescript
App.addListener('appUrlOpen', (event) => {
const url = new URL(event.url);
// 获取查询参数
const source = url.searchParams.get('source');
const campaign = url.searchParams.get('campaign');
const referrer = url.searchParams.get('ref');
// 跟踪归因
analytics.logEvent('deep_link_open', {
path: url.pathname,
source,
campaign,
referrer,
});
// 携带状态导航
navigateTo(url.pathname, {
state: { source, campaign, referrer },
});
});OAuth Callback Handling
OAuth回调处理
typescript
// Handle OAuth redirect
App.addListener('appUrlOpen', async (event) => {
const url = new URL(event.url);
if (url.pathname === '/oauth/callback') {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const error = url.searchParams.get('error');
if (error) {
handleOAuthError(error);
return;
}
if (code && validateState(state)) {
await exchangeCodeForToken(code);
navigateTo('/home');
}
}
});typescript
// 处理OAuth重定向
App.addListener('appUrlOpen', async (event) => {
const url = new URL(event.url);
if (url.pathname === '/oauth/callback') {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const error = url.searchParams.get('error');
if (error) {
handleOAuthError(error);
return;
}
if (code && validateState(state)) {
await exchangeCodeForToken(code);
navigateTo('/home');
}
}
});Testing
测试
Test Matrix
测试矩阵
| Scenario | Command |
|---|---|
| Custom scheme | |
| Universal link cold start | Tap link with app closed |
| Universal link warm start | Tap link with app in background |
| Universal link in Safari | Type URL in Safari |
| App link cold start | Tap link with app closed |
| App link in Chrome | Tap link in Chrome |
| 场景 | 操作/命令 |
|---|---|
| 自定义方案 | |
| Universal Links冷启动 | 应用关闭时点击链接 |
| Universal Links热启动 | 应用在后台时点击链接 |
| Safari中的Universal Links | 在Safari中输入URL |
| App Links冷启动 | 应用关闭时点击链接 |
| Chrome中的App Links | 在Chrome中点击链接 |
Debug Tools
调试工具
bash
undefinedbash
undefinediOS: Check associated domains entitlement
iOS: 检查关联域名权限
codesign -d --entitlements - App.app | grep associated-domains
codesign -d --entitlements - App.app | grep associated-domains
iOS: Reset Universal Links cache
iOS: 重置Universal Links缓存
xcrun simctl erase all
xcrun simctl erase all
Android: Check verified links
Android: 检查已验证链接
adb shell dumpsys package d | grep -A5 "Package: com.yourcompany.yourapp"
undefinedadb shell dumpsys package d | grep -A5 "Package: com.yourcompany.yourapp"
undefinedCommon Issues
常见问题
| Issue | Solution |
|---|---|
| Universal Links not working | Check AASA file, SSL, entitlements |
| App Links not verified | Check assetlinks.json, fingerprint |
| Links open in browser | Check intent-filter, autoVerify |
| Cold start not handled | Use |
| Simulator issues | Reset simulator, rebuild app |
| 问题 | 解决方案 |
|---|---|
| Universal Links无法工作 | 检查AASA文件、SSL证书、权限配置 |
| App Links未验证 | 检查assetlinks.json、指纹配置 |
| 链接在浏览器中打开 | 检查意图过滤器、autoVerify配置 |
| 冷启动未处理 | 使用 |
| 模拟器问题 | 重置模拟器、重新构建应用 |
Resources
资源
- Capacitor App Plugin: https://capacitorjs.com/docs/apis/app
- Universal Links Guide: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
- Android App Links: https://developer.android.com/training/app-links
- Digital Asset Links Validator: https://developers.google.com/digital-asset-links/tools/generator
- Capacitor App插件: https://capacitorjs.com/docs/apis/app
- Universal Links指南: https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app
- Android App Links: https://developer.android.com/training/app-links
- 数字资产链接验证工具: https://developers.google.com/digital-asset-links/tools/generator