路由机制概述 为什么需要路由? ┌─────────────────────────────────────────────────────────────────┐ │ 传统页面跳转 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ 直接依赖 ┌──────────────┐ │ │ │ 首页模块 │ ─────────────────────→ │ 商品模块 │ │ │ │ │ import ProductModule │ │ │ │ │ HomeVC │ ───────────────────────→│ ProductVC │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ 直接依赖 │ │ │ └─────────────────────┐ ┌─────────────┘ │ │ ▼ ▼ │ │ ┌──────────────┐ │ │ │ 订单模块 │ │ │ │ OrderVC │ │ │ └──────────────┘ │ │ │ │ 问题: │ │ ❌ 模块间强耦合,形成网状依赖 │ │ ❌ 无法独立编译和测试 │ │ ❌ 代码改动影响范围大 │ │ ❌ 不支持动态化和外部跳转 │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ 路由解耦后 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 首页模块 │ │ 商品模块 │ │ │ └──────┬───────┘ └───────▲──────┘ │ │ │ │ │ │ │ router.navigate("/product/123") │ │ │ │ │ │ │ ▼ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 路由中心 │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ 路由表: │ │ │ │ │ │ /home → HomeViewController │ │ │ │ │ │ /product/:id → ProductViewController │ │ │ │ │ │ /order/:id → OrderViewController │ │ │ │ │ │ /cart → CartViewController │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ 订单模块 │ │ │ └──────────────┘ │ │ │ │ 优势: │ │ ✅ 模块间完全解耦 │ │ ✅ 支持外部 DeepLink │ │ ✅ 动态化配置 │ │ ✅ 统一拦截和处理 │ └─────────────────────────────────────────────────────────────────┘主流路由方案对比 ┌──────────────────────────────────────────────────────────────────────────┐ │ iOS 主流路由方案对比 │ ├────────────────┬────────────────┬────────────────┬───────────────────────┤ │ 方案 │ 代表框架 │ 优点 │ 缺点 │ ├────────────────┼────────────────┼────────────────┼───────────────────────┤ │ URL Router │ JLRoutes │ • 解耦彻底 │ • 参数传递受限 │ │ │ MGJRouter │ • 支持DeepLink │ • 编译期无类型检查 │ │ │ HHRouter │ • 配置灵活 │ • 硬编码URL字符串 │ ├────────────────┼────────────────┼────────────────┼───────────────────────┤ │ Target-Action │ CTMediator │ • 无需注册 │ • 依赖runtime │ │ │ │ • 延迟加载 │ • 方法名硬编码 │ │ │ │ • 原生调用 │ • 缺少编译检查 │ ├────────────────┼────────────────┼────────────────┼───────────────────────┤ │ Protocol-Class │ BeeHive │ • 类型安全 │ • 需要注册协议 │ │ │ Swinject │ • 编译期检查 │ • 接口变更成本高 │ │ │ │ • IDE支持好 │ • 协议维护成本 │ ├────────────────┼────────────────┼────────────────┼───────────────────────┤ │ 混合方案 │ 自研Router │ • 各取所长 │ • 架构复杂度较高 │ │ │ │ • 适应不同场景 │ • 学习成本 │ └────────────────┴────────────────┴────────────────┴───────────────────────┘URL Scheme 机制 URL Scheme 基础 /* ═══════════════════════════════════════════════════════════════════ URL Scheme 结构 ═══════════════════════════════════════════════════════════════════ myapp://product/detail?id=123&source=home ───── ───────────── ───────────────────── │ │ │ Scheme Path Query (协议) (路径) (查询参数) 完整URL结构: ┌────────────────────────────────────────────────────────────────┐ │ scheme://user:password@host:port/path?query=value#fragment │ └────────────────────────────────────────────────────────────────┘ 常见使用场景: ┌─────────────────────────────────────────────────────────────────┐ │ 场景 │ 示例URL │ ├─────────────────────────────────────────────────────────────────┤ │ App外部打开 │ myapp://home │ │ Push通知跳转 │ myapp://order/123 │ │ 第三方分享回调 │ myapp://share/callback?code=xxx │ │ 广告投放追踪 │ myapp://product/456?utm_source=ad │ │ App间跳转 │ weixin:// │ │ Universal Links │ https://app.example.com/product/1 │ └─────────────────────────────────────────────────────────────────┘ */ Info.plist 配置 <!-- Info.plist URL Scheme 配置 --> <?xml version="1.0" encoding="UTF-8"?> <! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > < plistversion = " 1.0" > < dict> <!-- URL Types 配置 --> < key> CFBundleURLTypes</ key> < array> <!-- 主 Scheme --> < dict> < key> CFBundleURLName</ key> < string> com.yourcompany.myapp</ string> < key> CFBundleURLSchemes</ key> < array> < string> myapp</ string> </ array> < key> CFBundleTypeRole</ key> < string> Editor</ string> </ dict> <!-- 微信回调 Scheme --> < dict> < key> CFBundleURLName</ key> < string> weixin</ string> < key> CFBundleURLSchemes</ key> < array> < string> wx1234567890abcdef</ string> </ array> </ dict> <!-- 支付宝回调 Scheme --> < dict> < key> CFBundleURLName</ key> < string> alipay</ string> < key> CFBundleURLSchemes</ key> < array> < string> ap2021123456789012</ string> </ array> </ dict> </ array> <!-- 可以打开的外部 Scheme(iOS 9+需要白名单) --> < key> LSApplicationQueriesSchemes</ key> < array> < string> weixin</ string> < string> wechat</ string> < string> alipay</ string> < string> mqq</ string> < string> weibo</ string> </ array> </ dict> </ plist> Universal Links 配置 /* ═══════════════════════════════════════════════════════════════════ Universal Links ═══════════════════════════════════════════════════════════════════ 优势对比: ┌────────────────────┬─────────────────┬─────────────────────────┐ │ 特性 │ URL Scheme │ Universal Links │ ├────────────────────┼─────────────────┼─────────────────────────┤ │ 唯一性 │ ❌ 可能冲突 │ ✅ 域名唯一 │ │ 回退网页 │ ❌ 不支持 │ ✅ 可打开网页 │ │ 微信等App内打开 │ ❌ 被屏蔽 │ ✅ 可直接打开 │ │ 安全性 │ ❌ 任何App可用 │ ✅ 域名验证 │ │ 配置复杂度 │ 简单 │ 需服务端配合 │ └────────────────────┴─────────────────┴─────────────────────────┘ */ // MARK: - apple-app-site-association 文件 (放在服务器 .well-known 目录) /* { "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.yourcompany.myapp", "paths": [ "/product/*", "/order/*", "/user/*", "/share/*", "NOT /api/*", "NOT /static/*" ] } ] }, "webcredentials": { "apps": ["TEAMID.com.yourcompany.myapp"] } } */ // MARK: - Entitlements 配置 /* <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.developer.associated-domains</key> <array> <string>applinks:app.yourcompany.com</string> <string>applinks:www.yourcompany.com</string> </array> </dict> </plist> */ URL 处理入口 // MARK: - SceneDelegate (iOS 13+) class SceneDelegate : UIResponder , UIWindowSceneDelegate { var window: UIWindow ? // 冷启动时通过 URL 打开 func scene ( _ scene: UIScene , willConnectTo session: UISceneSession , options connectionOptions: UIScene . ConnectionOptions ) { // 处理 URL Scheme if let url= connectionOptions. urlContexts. first ? . url{ handleIncomingURL ( url, isLaunch: true ) } // 处理 Universal Links if let userActivity= connectionOptions. userActivities. first , userActivity. activityType== NSUserActivityTypeBrowsingWeb , let url= userActivity. webpageURL{ handleUniversalLink ( url, isLaunch: true ) } } // App 在后台时通过 URL 唤起 func scene ( _ scene: UIScene , openURLContextsURLContexts : Set < UIOpenURLContext > ) { guard let url= URLContexts . first ? . urlelse { return } handleIncomingURL ( url, isLaunch: false ) } // Universal Links func scene ( _ scene: UIScene , continue userActivity: NSUserActivity ) { guard userActivity. activityType== NSUserActivityTypeBrowsingWeb , let url= userActivity. webpageURLelse { return } handleUniversalLink ( url, isLaunch: false ) } // MARK: - URL Handling private func handleIncomingURL ( _ url: URL , isLaunch: Bool ) { print ( "📥 [URL] Incoming:\( url. absoluteString) , isLaunch:\( isLaunch) " ) // 检查是否是第三方回调 if handleThirdPartyCallback ( url) { return } // 使用路由系统处理 DispatchQueue . main. asyncAfter ( deadline: . now ( ) + ( isLaunch? 0.5 : 0 ) ) { URLRouter . shared. open ( url: url, from: self . topViewController ( ) ) } } private func handleUniversalLink ( _ url: URL , isLaunch: Bool ) { print ( "🔗 [UniversalLink] Incoming:\( url. absoluteString) " ) // 转换为内部 URL 格式 let internalURL= convertToInternalURL ( url) handleIncomingURL ( internalURL, isLaunch: isLaunch) } private func handleThirdPartyCallback ( _ url: URL ) - > Bool { let scheme= url. scheme? ? "" // 微信回调 if scheme. hasPrefix ( "wx" ) { return WXApi . handleOpen ( url, delegate: WXApiManager . shared) } // 支付宝回调 if scheme. hasPrefix ( "ap" ) { AlipaySDK . defaultService ( ) . processOrder ( withPaymentResult: url) { resultin // 处理支付结果 } return true } return false } private func convertToInternalURL ( _ url: URL ) - > URL { // https://app.yourcompany.com/product/123 -> myapp://product/123 var components= URLComponents ( url: url, resolvingAgainstBaseURL: false ) components? . scheme= "myapp" components? . host= nil return components? . url? ? url} private func topViewController ( ) - > UIViewController ? { guard let rootVC= window? . rootViewControllerelse { return nil } return findTopViewController ( from: rootVC) } private func findTopViewController ( from viewController: UIViewController ) - > UIViewController { if let nav= viewControlleras ? UINavigationController , let topVC= nav. topViewController{ return findTopViewController ( from: topVC) } if let tab= viewControlleras ? UITabBarController , let selectedVC= tab. selectedViewController{ return findTopViewController ( from: selectedVC) } if let presentedVC= viewController. presentedViewController{ return findTopViewController ( from: presentedVC) } return viewController} } // MARK: - AppDelegate (iOS 12 及以下兼容) @mainclass AppDelegate : UIResponder , UIApplicationDelegate { var window: UIWindow ? // iOS 12 及以下的 URL 处理 func application ( _ app: UIApplication , open url: URL , options: [ UIApplication . OpenURLOptionsKey : Any ] = [ : ] ) - > Bool { print ( "📥 [URL] Open:\( url. absoluteString) " ) // 获取来源App信息 let sourceApp= options[ . sourceApplication] as ? String let annotation= options[ . annotation] print ( " Source App:\( sourceApp? ? "unknown" ) " ) // 处理URL handleIncomingURL ( url) return true } // Universal Links (iOS 12 及以下) func application ( _ application: UIApplication , continue userActivity: NSUserActivity , restorationHandler: @escaping( [ UIUserActivityRestoring ] ? ) - > Void ) - > Bool { guard userActivity. activityType== NSUserActivityTypeBrowsingWeb , let url= userActivity. webpageURLelse { return false } handleUniversalLink ( url) return true } private func handleIncomingURL ( _ url: URL ) { URLRouter . shared. open ( url: url, from: nil ) } private func handleUniversalLink ( _ url: URL ) { // 转换并处理 handleIncomingURL ( url) } } URLRouter 路由方案 完整 URLRouter 实现 // MARK: - URL路由核心实现 import UIKit import Combine // ═══════════════════════════════════════════════════════════════════ // MARK: - 路由协议定义 // ═══════════════════════════════════════════════════════════════════ /// 路由结果 public enum RouteResult { case viewController ( UIViewController ) case handler ( ( ) - > Void ) case redirect ( URL ) case failed ( RouteError ) } /// 路由错误 public enum RouteError : Error , LocalizedError { case notFound ( path: String ) case invalidParameter ( String ) case requireLogincase accessDenied ( reason: String ) case custom ( Error ) public var errorDescription: String ? { switch self { case . notFound ( let path) : return "路由未找到:\( path) " case . invalidParameter ( let msg) : return "参数无效:\( msg) " case . requireLogin: return "需要登录" case . accessDenied ( let reason) : return "访问被拒绝:\( reason) " case . custom ( let error) : return error. localizedDescription} } } /// 导航类型 public enum NavigationType { case push// Push导航 case present// 模态弹出 case presentFullScreen// 全屏模态 case replace// 替换当前页 case root// 设为根控制器 case custom ( ( UIViewController , UIViewController ) - > Void ) } /// 路由选项 public struct RouteOptions { public var navigationType: NavigationType public var animated: Bool public var completion: ( ( ) - > Void ) ? public init ( navigationType: NavigationType = . push, animated: Bool = true , completion: ( ( ) - > Void ) ? = nil ) { self . navigationType= navigationTypeself . animated= animatedself . completion= completion} public static let `default `= RouteOptions ( ) public static let present= RouteOptions ( navigationType: . present) public static let presentFullScreen= RouteOptions ( navigationType: . presentFullScreen) } /// 路由处理器协议 public protocol RouteHandler { func handle ( url: URL , params: RouteParams ) - > RouteResult } /// 路由拦截器协议 public protocol RouteInterceptor { /// 拦截器优先级(数字越大越先执行) var priority: Int { get } /// 是否拦截该路由 func shouldIntercept ( url: URL , params: RouteParams ) - > Bool /// 处理拦截(返回nil表示继续路由,返回URL表示重定向) func intercept ( url: URL , params: RouteParams , completion: @escaping( InterceptResult ) - > Void ) } /// 拦截结果 public enum InterceptResult { case `continue `// 继续路由 case redirect ( URL ) // 重定向 case reject ( RouteError ) // 拒绝路由 case handled// 已处理,不需要继续 } extension RouteInterceptor { public var priority: Int { return 0 } } // ═══════════════════════════════════════════════════════════════════ // MARK: - 路由参数 // ═══════════════════════════════════════════════════════════════════ /// 路由参数容器 public struct RouteParams { /// URL中的路径参数 (如 /product/:id 中的 id) public var pathParams: [ String : String ] = [ : ] /// URL中的查询参数 (如 ?page=1&size=10) public var queryParams: [ String : String ] = [ : ] /// 额外传递的对象参数 public var extra: [ String : Any ] = [ : ] /// 来源URL public var sourceURL: URL ? /// 获取String参数 public func string ( _ key: String ) - > String ? { return pathParams[ key] ? ? queryParams[ key] ? ? extra[ key] as ? String } /// 获取Int参数 public func int ( _ key: String ) - > Int ? { if let value= string ( key) { return Int ( value) } return extra[ key] as ? Int } /// 获取Bool参数 public func bool ( _ key: String ) - > Bool { if let value= string ( key) { return [ "true" , "1" , "yes" ] . contains ( value. lowercased ( ) ) } return extra[ key] as ? Bool ? ? false } /// 获取任意类型参数 public func get < T> ( _ key: String ) - > T? { return extra[ key] as ? T} /// 合并参数 public mutating func merge ( _ other: [ String : Any ] ) { for ( key, value) in other{ if let stringValue= valueas ? String { queryParams[ key] = stringValue} else { extra[ key] = value} } } } // ═══════════════════════════════════════════════════════════════════ // MARK: - URLRouter 核心实现 // ═══════════════════════════════════════════════════════════════════ public final class URLRouter { public static let shared= URLRouter ( ) // MARK: - Properties /// 路由表:path pattern -> handler private var routeMap: [ String : ( URL , RouteParams ) - > RouteResult ] = [ : ] /// 正则路由表(支持更复杂的匹配) private var regexRouteMap: [ ( NSRegularExpression , ( URL , RouteParams ) - > RouteResult ) ] = [ ] /// 拦截器列表 private var interceptors: [ RouteInterceptor ] = [ ] /// 全局路由监听 private var globalListeners: [ ( URL , RouteParams ) - > Void ] = [ ] /// 404处理器 private var notFoundHandler: ( ( URL ) - > UIViewController ) ? /// 错误处理器 private var errorHandler: ( ( RouteError , URL ) - > Void ) ? /// URL Scheme private let scheme: String /// 线程安全锁 private let lock= NSRecursiveLock ( ) // MARK: - Initialization public init ( scheme: String = "myapp" ) { self . scheme= scheme} // ═══════════════════════════════════════════════════════════════ // MARK: - 路由注册 // ═══════════════════════════════════════════════════════════════ /// 注册页面路由(返回ViewController) @discardableResultpublic func register ( _ pattern: String , handler: @escaping( URL , RouteParams ) - > UIViewController ? ) - > Self { let normalizedPattern= normalizePattern ( pattern) lock. lock ( ) defer { lock. unlock ( ) } routeMap[ normalizedPattern] = { url, paramsin if let vc= handler ( url, params) { return . viewController ( vc) } return . failed ( . notFound ( path: pattern) ) } print ( "🛣️ [Router] Registered:\( normalizedPattern) " ) return self } /// 注册动作路由(执行闭包) @discardableResultpublic func registerAction ( _ pattern: String , handler: @escaping( URL , RouteParams ) - > Void ) - > Self { let normalizedPattern= normalizePattern ( pattern) lock. lock ( ) defer { lock. unlock ( ) } routeMap[ normalizedPattern] = { url, paramsin return . handler{ handler ( url, params) } } print ( "⚡ [Router] Registered Action:\( normalizedPattern) " ) return self } /// 注册正则路由 @discardableResultpublic func registerRegex ( _ pattern: String , handler: @escaping( URL , RouteParams ) - > UIViewController ? ) - > Self { guard let regex= try ? NSRegularExpression ( pattern: pattern, options: [ ] ) else { print ( "❌ [Router] Invalid regex pattern:\( pattern) " ) return self } lock. lock ( ) defer { lock. unlock ( ) } regexRouteMap. append ( ( regex, { url, paramsin if let vc= handler ( url, params) { return . viewController ( vc) } return . failed ( . notFound ( path: pattern) ) } ) ) print ( "🔤 [Router] Registered Regex:\( pattern) " ) return self } /// 注册重定向 @discardableResultpublic func registerRedirect ( from source: String , to destination: String ) - > Self { let normalizedSource= normalizePattern ( source) lock. lock ( ) defer { lock. unlock ( ) } routeMap[ normalizedSource] = { [ weak self ] url, paramsin guard let destURL= self ? . buildURL ( destination, params: params) else { return . failed ( . notFound ( path: destination) ) } return . redirect ( destURL) } print ( "↪️ [Router] Registered Redirect:\( source) ->\( destination) " ) return self } /// 批量注册 @discardableResultpublic func registerBatch ( _ routes: [ ( String , ( URL , RouteParams ) - > UIViewController ? ) ] ) - > Self { for ( pattern, handler) in routes{ register ( pattern, handler: handler) } return self } // ═══════════════════════════════════════════════════════════════ // MARK: - 路由导航 // ═══════════════════════════════════════════════════════════════ /// 通过URL字符串导航 public func open ( _ urlString: String , from sourceVC: UIViewController ? = nil , options: RouteOptions = . default , extra: [ String : Any ] ? = nil ) { guard let url= URL ( string: urlString) else { print ( "❌ [Router] Invalid URL:\( urlString) " ) return } open ( url: url, from: sourceVC, options: options, extra: extra) } /// 通过URL对象导航 public func open ( url: URL , from sourceVC: UIViewController ? = nil , options: RouteOptions = . default , extra: [ String : Any ] ? = nil ) { // 解析参数 var params= parseParams ( from: url) if let extra= extra{ params. merge ( extra) } params. sourceURL= urlprint ( "🔍 [Router] Opening:\( url. absoluteString) " ) // 通知全局监听器 notifyListeners ( url: url, params: params) // 执行拦截器链 runInterceptors ( url: url, params: params) { [ weak self ] resultin switch result{ case . continue : self ? . performRouting ( url: url, params: params, from: sourceVC, options: options) case . redirect ( let newURL) : self ? . open ( url: newURL, from: sourceVC, options: options) case . reject ( let error) : self ? . handleError ( error, for : url) case . handled: print ( "✅ [Router] Handled by interceptor" ) } } } /// 获取目标ViewController(不进行导航) public func viewController ( for urlString: String , extra: [ String : Any ] ? = nil ) - > UIViewController ? { guard let url= URL ( string: urlString) else { return nil } return viewController ( for : url, extra: extra) } public func viewController ( for url: URL , extra: [ String : Any ] ? = nil ) - > UIViewController ? { var params= parseParams ( from: url) if let extra= extra{ params. merge ( extra) } let result= matchRoute ( url: url, params: params) switch result{ case . viewController ( let vc) : return vcdefault : return nil } } /// 检查URL是否可以被处理 public func canOpen ( _ urlString: String ) - > Bool { guard let url= URL ( string: urlString) else { return false } return canOpen ( url: url) } public func canOpen ( url: URL ) - > Bool { let params= parseParams ( from: url) let result= matchRoute ( url: url, params: params) if case . failed= result{ return false } return true } // ═══════════════════════════════════════════════════════════════ // MARK: - 拦截器管理 // ═══════════════════════════════════════════════════════════════ /// 添加拦截器 public func addInterceptor ( _ interceptor: RouteInterceptor ) { lock. lock ( ) defer { lock. unlock ( ) } interceptors. append ( interceptor) // 按优先级排序 interceptors. sort { $0 . priority> $1 . priority} } /// 移除拦截器 public func removeInterceptor< T: RouteInterceptor > ( _ type: T. Type ) { lock. lock ( ) defer { lock. unlock ( ) } interceptors. removeAll{ $0 is T} } // ═══════════════════════════════════════════════════════════════ // MARK: - 配置 // ═══════════════════════════════════════════════════════════════ /// 设置404处理器 public func setNotFoundHandler ( _ handler: @escaping( URL ) - > UIViewController ) { notFoundHandler= handler} /// 设置错误处理器 public func setErrorHandler ( _ handler: @escaping( RouteError , URL ) - > Void ) { errorHandler= handler} /// 添加全局路由监听 public func addListener ( _ listener: @escaping( URL , RouteParams ) - > Void ) { lock. lock ( ) defer { lock. unlock ( ) } globalListeners. append ( listener) } // ═══════════════════════════════════════════════════════════════ // MARK: - Private Methods // ═══════════════════════════════════════════════════════════════ /// 标准化路由模式 private func normalizePattern ( _ pattern: String ) - > String { var result= pattern. lowercased ( ) if ! result. hasPrefix ( "/" ) { result= "/" + result} if result. hasSuffix ( "/" ) && result. count > 1 { result. removeLast ( ) } return result} /// 解析URL参数 private func