路由机制概述
为什么需要路由?
┌─────────────────────────────────────────────────────────────────┐ │ 传统页面跳转 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ 直接依赖 ┌──────────────┐ │ │ │ 首页模块 │ ─────────────────────→ │ 商品模块 │ │ │ │ │ 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"?><!DOCTYPEplistPUBLIC"-//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+)classSceneDelegate:UIResponder,UIWindowSceneDelegate{varwindow:UIWindow?// 冷启动时通过 URL 打开funcscene(_scene:UIScene,willConnectTo session:UISceneSession,options connectionOptions:UIScene.ConnectionOptions){// 处理 URL Schemeifleturl=connectionOptions.urlContexts.first?.url{handleIncomingURL(url,isLaunch:true)}// 处理 Universal LinksifletuserActivity=connectionOptions.userActivities.first,userActivity.activityType==NSUserActivityTypeBrowsingWeb,leturl=userActivity.webpageURL{handleUniversalLink(url,isLaunch:true)}}// App 在后台时通过 URL 唤起funcscene(_scene:UIScene,openURLContextsURLContexts:Set<UIOpenURLContext>){guardleturl=URLContexts.first?.urlelse{return}handleIncomingURL(url,isLaunch:false)}// Universal Linksfuncscene(_scene:UIScene,continueuserActivity:NSUserActivity){guarduserActivity.activityType==NSUserActivityTypeBrowsingWeb,leturl=userActivity.webpageURLelse{return}handleUniversalLink(url,isLaunch:false)}// MARK: - URL HandlingprivatefunchandleIncomingURL(_url:URL,isLaunch:Bool){print("📥 [URL] Incoming:\(url.absoluteString), isLaunch:\(isLaunch)")// 检查是否是第三方回调ifhandleThirdPartyCallback(url){return}// 使用路由系统处理DispatchQueue.main.asyncAfter(deadline:.now()+(isLaunch?0.5:0)){URLRouter.shared.open(url:url,from:self.topViewController())}}privatefunchandleUniversalLink(_url:URL,isLaunch:Bool){print("🔗 [UniversalLink] Incoming:\(url.absoluteString)")// 转换为内部 URL 格式letinternalURL=convertToInternalURL(url)handleIncomingURL(internalURL,isLaunch:isLaunch)}privatefunchandleThirdPartyCallback(_url:URL)->Bool{letscheme=url.scheme??""// 微信回调ifscheme.hasPrefix("wx"){returnWXApi.handleOpen(url,delegate:WXApiManager.shared)}// 支付宝回调ifscheme.hasPrefix("ap"){AlipaySDK.defaultService().processOrder(withPaymentResult:url){resultin// 处理支付结果}returntrue}returnfalse}privatefuncconvertToInternalURL(_url:URL)->URL{// https://app.yourcompany.com/product/123 -> myapp://product/123varcomponents=URLComponents(url:url,resolvingAgainstBaseURL:false)components?.scheme="myapp"components?.host=nilreturncomponents?.url??url}privatefunctopViewController()->UIViewController?{guardletrootVC=window?.rootViewControllerelse{returnnil}returnfindTopViewController(from:rootVC)}privatefuncfindTopViewController(from viewController:UIViewController)->UIViewController{ifletnav=viewControlleras?UINavigationController,lettopVC=nav.topViewController{returnfindTopViewController(from:topVC)}iflettab=viewControlleras?UITabBarController,letselectedVC=tab.selectedViewController{returnfindTopViewController(from:selectedVC)}ifletpresentedVC=viewController.presentedViewController{returnfindTopViewController(from:presentedVC)}returnviewController}}// MARK: - AppDelegate (iOS 12 及以下兼容)@mainclassAppDelegate:UIResponder,UIApplicationDelegate{varwindow:UIWindow?// iOS 12 及以下的 URL 处理funcapplication(_app:UIApplication,open url:URL,options:[UIApplication.OpenURLOptionsKey:Any]=[:])->Bool{print("📥 [URL] Open:\(url.absoluteString)")// 获取来源App信息letsourceApp=options[.sourceApplication]as?Stringletannotation=options[.annotation]print(" Source App:\(sourceApp??"unknown")")// 处理URLhandleIncomingURL(url)returntrue}// Universal Links (iOS 12 及以下)funcapplication(_application:UIApplication,continueuserActivity:NSUserActivity,restorationHandler:@escaping([UIUserActivityRestoring]?)->Void)->Bool{guarduserActivity.activityType==NSUserActivityTypeBrowsingWeb,leturl=userActivity.webpageURLelse{returnfalse}handleUniversalLink(url)returntrue}privatefunchandleIncomingURL(_url:URL){URLRouter.shared.open(url:url,from:nil)}privatefunchandleUniversalLink(_url:URL){// 转换并处理handleIncomingURL(url)}}
URLRouter 路由方案
完整 URLRouter 实现
// MARK: - URL路由核心实现importUIKitimportCombine// ═══════════════════════════════════════════════════════════════════// MARK: - 路由协议定义// ═══════════════════════════════════════════════════════════════════/// 路由结果publicenumRouteResult{caseviewController(UIViewController)casehandler(()->Void)caseredirect(URL)casefailed(RouteError)}/// 路由错误publicenumRouteError:Error,LocalizedError{casenotFound(path:String)caseinvalidParameter(String)caserequireLogincaseaccessDenied(reason:String)casecustom(Error)publicvarerrorDescription:String?{switchself{case.notFound(letpath):return"路由未找到:\(path)"case.invalidParameter(letmsg):return"参数无效:\(msg)"case.requireLogin:return"需要登录"case.accessDenied(letreason):return"访问被拒绝:\(reason)"case.custom(leterror):returnerror.localizedDescription}}}/// 导航类型publicenumNavigationType{casepush// Push导航casepresent// 模态弹出casepresentFullScreen// 全屏模态casereplace// 替换当前页caseroot// 设为根控制器casecustom((UIViewController,UIViewController)->Void)}/// 路由选项publicstructRouteOptions{publicvarnavigationType:NavigationTypepublicvaranimated:Boolpublicvarcompletion:(()->Void)?publicinit(navigationType:NavigationType=.push,animated:Bool=true,completion:(()->Void)?=nil){self.navigationType=navigationTypeself.animated=animatedself.completion=completion}publicstaticlet`default`=RouteOptions()publicstaticletpresent=RouteOptions(navigationType:.present)publicstaticletpresentFullScreen=RouteOptions(navigationType:.presentFullScreen)}/// 路由处理器协议publicprotocolRouteHandler{funchandle(url:URL,params:RouteParams)->RouteResult}/// 路由拦截器协议publicprotocolRouteInterceptor{/// 拦截器优先级(数字越大越先执行)varpriority:Int{get}/// 是否拦截该路由funcshouldIntercept(url:URL,params:RouteParams)->Bool/// 处理拦截(返回nil表示继续路由,返回URL表示重定向)funcintercept(url:URL,params:RouteParams,completion:@escaping(InterceptResult)->Void)}/// 拦截结果publicenumInterceptResult{case`continue`// 继续路由caseredirect(URL)// 重定向casereject(RouteError)// 拒绝路由casehandled// 已处理,不需要继续}extensionRouteInterceptor{publicvarpriority:Int{return0}}// ═══════════════════════════════════════════════════════════════════// MARK: - 路由参数// ═══════════════════════════════════════════════════════════════════/// 路由参数容器publicstructRouteParams{/// URL中的路径参数 (如 /product/:id 中的 id)publicvarpathParams:[String:String]=[:]/// URL中的查询参数 (如 ?page=1&size=10)publicvarqueryParams:[String:String]=[:]/// 额外传递的对象参数publicvarextra:[String:Any]=[:]/// 来源URLpublicvarsourceURL:URL?/// 获取String参数publicfuncstring(_key:String)->String?{returnpathParams[key]??queryParams[key]??extra[key]as?String}/// 获取Int参数publicfuncint(_key:String)->Int?{ifletvalue=string(key){returnInt(value)}returnextra[key]as?Int}/// 获取Bool参数publicfuncbool(_key:String)->Bool{ifletvalue=string(key){return["true","1","yes"].contains(value.lowercased())}returnextra[key]as?Bool??false}/// 获取任意类型参数publicfuncget<T>(_key:String)->T?{returnextra[key]as?T}/// 合并参数publicmutatingfuncmerge(_other:[String:Any]){for(key,value)inother{ifletstringValue=valueas?String{queryParams[key]=stringValue}else{extra[key]=value}}}}// ═══════════════════════════════════════════════════════════════════// MARK: - URLRouter 核心实现// ═══════════════════════════════════════════════════════════════════publicfinalclassURLRouter{publicstaticletshared=URLRouter()// MARK: - Properties/// 路由表:path pattern -> handlerprivatevarrouteMap:[String:(URL,RouteParams)->RouteResult]=[:]/// 正则路由表(支持更复杂的匹配)privatevarregexRouteMap:[(NSRegularExpression,(URL,RouteParams)->RouteResult)]=[]/// 拦截器列表privatevarinterceptors:[RouteInterceptor]=[]/// 全局路由监听privatevarglobalListeners:[(URL,RouteParams)->Void]=[]/// 404处理器privatevarnotFoundHandler:((URL)->UIViewController)?/// 错误处理器privatevarerrorHandler:((RouteError,URL)->Void)?/// URL Schemeprivateletscheme:String/// 线程安全锁privateletlock=NSRecursiveLock()// MARK: - Initializationpublicinit(scheme:String="myapp"){self.scheme=scheme}// ═══════════════════════════════════════════════════════════════// MARK: - 路由注册// ═══════════════════════════════════════════════════════════════/// 注册页面路由(返回ViewController)@discardableResultpublicfuncregister(_pattern:String,handler:@escaping(URL,RouteParams)->UIViewController?)->Self{letnormalizedPattern=normalizePattern(pattern)lock.lock()defer{lock.unlock()}routeMap[normalizedPattern]={url,paramsinifletvc=handler(url,params){return.viewController(vc)}return.failed(.notFound(path:pattern))}print("🛣️ [Router] Registered:\(normalizedPattern)")returnself}/// 注册动作路由(执行闭包)@discardableResultpublicfuncregisterAction(_pattern:String,handler:@escaping(URL,RouteParams)->Void)->Self{letnormalizedPattern=normalizePattern(pattern)lock.lock()defer{lock.unlock()}routeMap[normalizedPattern]={url,paramsinreturn.handler{handler(url,params)}}print("⚡ [Router] Registered Action:\(normalizedPattern)")returnself}/// 注册正则路由@discardableResultpublicfuncregisterRegex(_pattern:String,handler:@escaping(URL,RouteParams)->UIViewController?)->Self{guardletregex=try?NSRegularExpression(pattern:pattern,options:[])else{print("❌ [Router] Invalid regex pattern:\(pattern)")returnself}lock.lock()defer{lock.unlock()}regexRouteMap.append((regex,{url,paramsinifletvc=handler(url,params){return.viewController(vc)}return.failed(.notFound(path:pattern))}))print("🔤 [Router] Registered Regex:\(pattern)")returnself}/// 注册重定向@discardableResultpublicfuncregisterRedirect(from source:String,to destination:String)->Self{letnormalizedSource=normalizePattern(source)lock.lock()defer{lock.unlock()}routeMap[normalizedSource]={[weakself]url,paramsinguardletdestURL=self?.buildURL(destination,params:params)else{return.failed(.notFound(path:destination))}return.redirect(destURL)}print("↪️ [Router] Registered Redirect:\(source)->\(destination)")returnself}/// 批量注册@discardableResultpublicfuncregisterBatch(_routes:[(String,(URL,RouteParams)->UIViewController?)])->Self{for(pattern,handler)inroutes{register(pattern,handler:handler)}returnself}// ═══════════════════════════════════════════════════════════════// MARK: - 路由导航// ═══════════════════════════════════════════════════════════════/// 通过URL字符串导航publicfuncopen(_urlString:String,from sourceVC:UIViewController?=nil,options:RouteOptions=.default,extra:[String:Any]?=nil){guardleturl=URL(string:urlString)else{print("❌ [Router] Invalid URL:\(urlString)")return}open(url:url,from:sourceVC,options:options,extra:extra)}/// 通过URL对象导航publicfuncopen(url:URL,from sourceVC:UIViewController?=nil,options:RouteOptions=.default,extra:[String:Any]?=nil){// 解析参数varparams=parseParams(from:url)ifletextra=extra{params.merge(extra)}params.sourceURL=urlprint("🔍 [Router] Opening:\(url.absoluteString)")// 通知全局监听器notifyListeners(url:url,params:params)// 执行拦截器链runInterceptors(url:url,params:params){[weakself]resultinswitchresult{case.continue:self?.performRouting(url:url,params:params,from:sourceVC,options:options)case.redirect(letnewURL):self?.open(url:newURL,from:sourceVC,options:options)case.reject(leterror):self?.handleError(error,for:url)case.handled:print("✅ [Router] Handled by interceptor")}}}/// 获取目标ViewController(不进行导航)publicfuncviewController(forurlString:String,extra:[String:Any]?=nil)->UIViewController?{guardleturl=URL(string:urlString)else{returnnil}returnviewController(for:url,extra:extra)}publicfuncviewController(forurl:URL,extra:[String:Any]?=nil)->UIViewController?{varparams=parseParams(from:url)ifletextra=extra{params.merge(extra)}letresult=matchRoute(url:url,params:params)switchresult{case.viewController(letvc):returnvcdefault:returnnil}}/// 检查URL是否可以被处理publicfunccanOpen(_urlString:String)->Bool{guardleturl=URL(string:urlString)else{returnfalse}returncanOpen(url:url)}publicfunccanOpen(url:URL)->Bool{letparams=parseParams(from:url)letresult=matchRoute(url:url,params:params)ifcase.failed=result{returnfalse}returntrue}// ═══════════════════════════════════════════════════════════════// MARK: - 拦截器管理// ═══════════════════════════════════════════════════════════════/// 添加拦截器publicfuncaddInterceptor(_interceptor:RouteInterceptor){lock.lock()defer{lock.unlock()}interceptors.append(interceptor)// 按优先级排序interceptors.sort{$0.priority>$1.priority}}/// 移除拦截器publicfuncremoveInterceptor<T:RouteInterceptor>(_type:T.Type){lock.lock()defer{lock.unlock()}interceptors.removeAll{$0isT}}// ═══════════════════════════════════════════════════════════════// MARK: - 配置// ═══════════════════════════════════════════════════════════════/// 设置404处理器publicfuncsetNotFoundHandler(_handler:@escaping(URL)->UIViewController){notFoundHandler=handler}/// 设置错误处理器publicfuncsetErrorHandler(_handler:@escaping(RouteError,URL)->Void){errorHandler=handler}/// 添加全局路由监听publicfuncaddListener(_listener:@escaping(URL,RouteParams)->Void){lock.lock()defer{lock.unlock()}globalListeners.append(listener)}// ═══════════════════════════════════════════════════════════════// MARK: - Private Methods// ═══════════════════════════════════════════════════════════════/// 标准化路由模式privatefuncnormalizePattern(_pattern:String)->String{varresult=pattern.lowercased()if!result.hasPrefix("/"){result="/"+result}ifresult.hasSuffix("/")&&result.count>1{result.removeLast()}returnresult}/// 解析URL参数privatefunc