02-超级App软件平台@路由规则设计-【URL-Scheme】与【Intent-Filter】详解

9 阅读2分钟

本专题为软件平台路由规则设计(含移动端、Web、电脑、手表与手环等智能穿戴)。本文对应「URL Scheme / Intent Filter」阶段,介绍自定义协议深链接在 iOSAndroidHarmonyOSFlutterMacOSWinOSWebAppReactNativeWatchOS 及穿戴设备上的配置、解析与统一路由处理,含多端代码示例与流程图。软件体系见 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@mainWindowGroup 上层接收 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 相同,配置 CFBundleURLTypesmyapp

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 / 等价机制配置位置接收入口
iOSURL SchemeInfo.plist CFBundleURLTypesAppDelegate openURL / Scene openURLContexts
AndroidIntent Filter data scheme/host/pathAndroidManifest intent-filterActivity intent.data
HarmonyOSWant uri / schememodule.json5 abilitiesAbility 启动参数
Flutter依赖宿主 iOS/Android 配置同上 + app_linksgetInitialLink / uriLinkStream
MacOSURL SchemeInfo.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 内路由与简单外链。
  • iOSCFBundleURLTypes + application:openURL: 或 Scene 的 openURLContextsAndroidintent-filter + Intent.getData()Flutterapp_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.