本专题为软件平台路由规则设计(含移动端、Web、电脑、手表与手环等智能穿戴)。本文对应「URL Scheme / Intent Filter」阶段,介绍自定义协议深链接在 iOS、Android、HarmonyOS、Flutter、MacOS、WinOS、WebApp、ReactNative、WatchOS 及穿戴设备上的配置、解析与统一路由处理,含多端代码示例与流程图。软件体系见 08-软件体系与多平台路由对照。
一、概念与定位
1.1 什么是 URL Scheme / Intent Filter
- URL Scheme(iOS):应用在系统注册的自定义协议头,格式为
scheme://host/path?query。当系统或其他应用打开该 URL 时,可唤起本 App 并传入完整 URL,由 App 内部分发到对应页面或能力。 - Intent Filter(Android):在
AndroidManifest.xml中通过<intent-filter>声明应用能处理的 Intent(如ACTION_VIEW+ 指定data的 scheme/host/path),实现「链接 → 打开本 App」的深链接能力。
二者都是系统级的「链接 → 打开指定 App」的机制,不依赖第三方服务,是深链接的雏形与基础。
1.2 知识结构(思维导图)
mindmap
root((URL Scheme / Intent Filter))
概念
Scheme 协议头
Host / Path / Query
系统唤起与参数传递
移动原生
iOS Info.plist AppDelegate
Android intent-filter Intent.getData
HarmonyOS Want / scheme
跨平台与桌面
Flutter app_links 平台通道
ReactNative Linking + 原生配置
MacOS 同 iOS
WinOS 协议关联
Web 与可穿戴
WebApp 不适用 用 URL 路由
WatchOS 简化 Scheme 与 iPhone 协同
局限与注意
Scheme 冲突 容器拦截
未安装无反馈
二、整体流程(泳道图)
从「用户点击链接」到「App 内目标页打开」的职责划分如下。
flowchart TB
subgraph 用户与系统
A[用户点击 myapp://page/detail?id=1]
B[系统解析 URL]
C{已安装对应 App?}
end
subgraph iOS
D[iOS 根据 CFBundleURLSchemes 匹配]
E[调用 application:openURL:options:]
end
subgraph Android
F[根据 intent-filter 匹配]
G[启动 Activity 并传入 Intent]
end
subgraph App 内
H[接收 URL / Intent]
I[解析 path + query]
J[路由表查找]
K[拦截器]
L[打开目标页]
end
A --> B --> C
C -->|是| D
C -->|否| N[无反应或打开失败页]
D --> E --> H
F --> G --> H
B --> F
H --> I --> J --> K --> L
三、iOS 实现详解
3.1 Info.plist 配置
在 Xcode 中为 Target 添加 URL Types,或直接在 Info.plist 中增加:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
对应 URL 示例:myapp://page/detail?id=1,其中 myapp 即 Scheme。
3.2 AppDelegate / SceneDelegate 接收
UIKit(AppDelegate):
// AppDelegate.swift
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
// 将 URL 交给统一路由层处理
return AppRouter.shared.handleOpenURL(url)
}
SwiftUI + Scene 生命周期:在 SceneDelegate 或 @main 的 WindowGroup 上层接收 URL,需在 Info.plist 中配置 UIApplicationSceneManifest 并实现 UISceneDelegate:
// SceneDelegate.swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
AppRouter.shared.handleOpenURL(url)
}
3.3 统一路由处理(Swift 示例)
// AppRouter.swift
final class AppRouter {
static let shared = AppRouter()
private let routeTable: [String: RouteHandler] = [
"/page/detail": { params in DetailViewController(id: params["id"]) },
"/page/list": { _ in ListViewController() }
]
func handleOpenURL(_ url: URL) -> Bool {
guard url.scheme == "myapp", url.host == "page" else { return false }
let path = "/" + (url.pathComponents.dropFirst().joined(separator: "/"))
var params = url.queryItems ?? [:]
if let pathId = url.pathComponents.last, path.contains("detail") {
params["id"] = pathId
}
return navigate(path: path, params: params)
}
private func navigate(path: String, params: [String: String]) -> Bool {
guard let handler = routeTable[path] else { return false }
let vc = handler(params)
topViewController()?.show(vc, sender: nil)
return true
}
}
extension URL {
var queryItems: [String: String]? {
URLComponents(url: self, resolvingAgainstBaseURL: false)?
.queryItems?
.reduce(into: [String: String]()) { $0[$1.name] = $1.value }
}
}
四、Android 实现详解
4.1 AndroidManifest 中 intent-filter 配置
在要接收链接的 Activity(通常为主 Activity 或专门用于深链接的 Activity)下声明:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<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"
android:host="page"
android:pathPrefix="/detail"
android:pathPattern="/list/.*" />
</intent-filter>
</activity>
singleTask常用于避免重复栈;若希望每次打开新实例可改为standard。- 多条
<data>可共存,表示多种 path 均由该 Activity 接收,再在代码里根据 path 分发。
4.2 Activity 中解析 Intent
// MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intent?.let { handleDeepLink(it) }
}
private fun handleDeepLink(intent: Intent?) {
val data: Uri? = intent?.data ?: return
if (data.scheme != "myapp" || data.host != "page") return
val path = data.path ?: return
val params = data.queryParameterNames.associateWith { data.getQueryParameter(it) ?: "" }
AppRouter.handle(this, path, params)
}
4.3 统一路由处理(Kotlin 示例)
// AppRouter.kt
object AppRouter {
private val routeTable = mapOf(
"/detail" to { ctx: Context, params: Map<String, String> ->
Intent(ctx, DetailActivity::class.java).apply {
params["id"]?.let { putExtra("id", it) }
}
},
"/list" to { ctx: Context, _: Map<String, String> ->
Intent(ctx, ListActivity::class.java)
}
)
fun handle(context: Context, path: String, params: Map<String, String>) {
val intentBuilder = routeTable[path] ?: return
context.startActivity(intentBuilder(context, params))
}
}
五、Flutter 实现详解
5.1 依赖与配置
pubspec.yaml:
dependencies:
app_links: ^6.3.0 # 统一处理 deep link / app link
# 或仅需 Scheme:uni_links: ^0.5.1
iOS Info.plist:与原生 iOS 相同,配置 CFBundleURLTypes 的 myapp。
Android AndroidManifest.xml:与原生 Android 相同,在对应 Activity 下配置 intent-filter(scheme=myapp)。
5.2 监听 Scheme 并交给路由
// lib/deep_link_handler.dart
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
class DeepLinkHandler {
static final AppLinks _appLinks = AppLinks();
static Future<void> init() async {
// 冷启动:App 未运行时通过链接打开
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_routeFromUri(initialUri);
}
// 热启动:App 在后台时点击链接
_appLinks.uriLinkStream.listen((Uri uri) {
_routeFromUri(uri);
});
}
static void _routeFromUri(Uri uri) {
if (uri.scheme != 'myapp' || uri.host != 'page') return;
final path = uri.path;
final params = uri.queryParameters;
// 交给 go_router 或 Navigator
GlobalRouter.navigate(path, params: params);
}
}
在 main.dart 中:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await DeepLinkHandler.init();
runApp(MyApp());
}
5.3 与 go_router 结合示例
// 若 path 与 go_router 的 path 一致,可直接 go
void GlobalRouter.navigate(String path, {Map<String, String>? params}) {
final query = params?.entries.map((e) => '${e.key}=${e.value}').join('&');
final fullPath = query != null && query.isNotEmpty ? '$path?$query' : path;
goRouter.go(fullPath);
}
六、流程时序图(从点击到打开页面)
sequenceDiagram
participant U as 用户
participant S as 系统/浏览器
participant OS as 系统层
participant App as App 进程
participant R as 路由层
participant P as 目标页
U->>S: 点击 myapp://page/detail?id=1
S->>OS: 请求打开 URL
OS->>OS: 根据 Scheme 查找已安装 App
alt 已安装
OS->>App: 唤起 App 并传入 URL / Intent
App->>R: handleOpenURL(url) / handleDeepLink(intent)
R->>R: 解析 path、query
R->>R: 查路由表
R->>R: 执行拦截器
R->>P: 打开目标页并传入 params
else 未安装
OS-->>U: 无反应或提示
end
七、URL 解析与路由分发(伪代码)
将「Scheme URL → path + params」的解析与「path → 目标」的分发拆开,便于与统一路由表对接。
函数 parseSchemeURL(url):
输入: url (字符串或 Uri)
输出: (path: string, params: Map<string, string>) 或 null
1. 解析 url 得到 scheme, host, pathStr, queryStr
2. 若 scheme 非本 App 约定(如 "myapp"),返回 null
3. path = 若 pathStr 不以 '/' 开头则 '/' + pathStr,否则 pathStr
4. params = 解析 queryStr 为键值对(如 "id=1&from=home" → { id: "1", from: "home" })
5. 若 host 有意义,可将 host 并入 path,如 path = "/" + host + path
6. 返回 (path, params)
函数 dispatchByPath(path, params):
1. routeMeta = RouteTable.lookup(path)
2. 若 routeMeta 为 null,跳转兜底页或返回 false
3. 执行拦截器链
4. 根据 routeMeta 执行打开页面(见 01 总纲 navigate 伪代码)
八、九大平台 Scheme/等价机制速查
| 平台 | Scheme / 等价机制 | 配置位置 | 接收入口 |
|---|---|---|---|
| iOS | URL Scheme | Info.plist CFBundleURLTypes | AppDelegate openURL / Scene openURLContexts |
| Android | Intent Filter data scheme/host/path | AndroidManifest intent-filter | Activity intent.data |
| HarmonyOS | Want uri / scheme | module.json5 abilities | Ability 启动参数 |
| Flutter | 依赖宿主 iOS/Android 配置 | 同上 + app_links | getInitialLink / uriLinkStream |
| MacOS | URL Scheme | Info.plist(同 iOS) | NSApplicationDelegate openURL |
| WinOS | 协议关联 | 包清单 / 注册表 | 启动参数 / OnActivated |
| WebApp | 不适用 | — | location.pathname + search |
| ReactNative | 使用原生 iOS/Android | 同上 | Linking.getInitialURL / addEventListener |
| WatchOS | 简化 Scheme | 与 iPhone App 协同 | 简化处理或 WKConnectivity |
九、局限与注意事项
| 问题 | 说明 | 建议 |
|---|---|---|
| Scheme 冲突 | 不同 App 可能使用相同 Scheme | 使用逆序域名如 comYourapp,或后续用 Universal Links / App Links |
| 容器拦截 | 微信等容器内可能禁止跳转自定义 Scheme | 引导在浏览器打开或使用 Universal Links |
| 未安装无反馈 | 用户未安装时点击可能无反应 | 落地页检测并引导下载,安装后通过 deferred deep link 还原 |
| iOS 确认框 | 首次跳转可能弹出「是否打开」 | 无法消除,Universal Links 可减少弹窗 |
十、小结
- URL Scheme / Intent Filter 是系统提供的、不依赖后端的深链接方案,配置简单,适合 App 内路由与简单外链。
- iOS:
CFBundleURLTypes+application:openURL:或 Scene 的openURLContexts;Android:intent-filter+Intent.getData();Flutter:app_links/uni_links+ 统一路由层。 - 建议在 App 内统一入口解析 URL(path + query),再查路由表、走拦截器,最终打开目标页,便于与后续「Universal Links / App Links」「组件化路由」复用同一套路由规则。
十一、Scheme 路由管理工具类示例(Swift 精简版)
将「接收 URL → 解析 → 查表 → 跳转」封装成单一工具类,便于在 AppDelegate/SceneDelegate 中一行调用。
// SchemeRouter.swift:仅负责 Scheme URL 的解析与转发,内部调用统一 Router
import UIKit
final class SchemeRouter {
static let shared = SchemeRouter()
var allowedSchemes: Set<String> = ["myapp"]
var defaultHost: String = "page"
/// 处理从系统传入的 URL,解析后交给 AppRouter
func handleOpenURL(_ url: URL) -> Bool {
guard allowedSchemes.contains(url.scheme ?? "") else { return false }
let path = buildPath(host: url.host ?? defaultHost, path: url.path)
let params = url.queryItems ?? [:]
return AppRouter.shared.navigate(path: path, params: params)
}
private func buildPath(host: String, path: String) -> String {
let p = path.hasPrefix("/") ? path : "/" + path
return "/" + host + p
}
}
extension URL {
var queryItems: [String: String]? {
URLComponents(url: self, resolvingAgainstBaseURL: false)?
.queryItems?.reduce(into: [String: String]()) { $0[$1.name] = $1.value }
}
}
十二、参考文献
- Apple. Defining a Custom URL Scheme for Your App.
- Android. Create deep links to app content.
- Flutter app_links / uni_links 官方文档.
- 《01-软件平台路由规则设计-总纲》§2.2、§6.