当前位置:首页>安卓APP>支持鸿蒙、安卓、iOS 的 KMP/CMP 跨平台路由框架

支持鸿蒙、安卓、iOS 的 KMP/CMP 跨平台路由框架

  • 2026-06-26 23:40:29
支持鸿蒙、安卓、iOS 的 KMP/CMP 跨平台路由框架

在 2026 年 6 月 13 日华为开发者大会上,华为宣布 KMP/CMP 正式支持鸿蒙。

在这之前,我就实现了一个支持安卓和 iOS 的 KMP/CMP 跨平台路由框架,KMP/CMP 支持鸿蒙后,我也在第一时间让我的路由框架支持了鸿蒙。

天衢(Github地址:https://github.com/peiyunfei/TianQu)是一个专为 KMP/CMP 打造的纯 Kotlin + 协程驱动的现代路由框架,支持鸿蒙、Android、iOS。

它彻底摆脱了传统 Android 路由框架对 JVM ASM 字节码插桩的依赖与深层嵌套的回调地狱,全面拥抱挂起函数,为您提供无侵入、强解耦、类型安全以及功能极其丰富的跨模块导航与服务发现解决方案。

天衢(tiān qú)出自《楚辞·九思·遭厄》:“蹑天衢兮长驱,踵九阳兮戏荡。”意为天上的大道,宽阔的通衢。寓意本框架为 KMP/CMP 跨平台开发提供一条平坦、宽广、畅通无阻的路由大道。

📱 平台支持

平台
状态
鸿蒙
✅ 支持
Android
✅ 支持
iOS
✅ 支持

🌟 核心特性概览

天衢路由最大的亮点在于对 Kotlin 协程的极致拥抱。我们在核心架构的每一层都去除了传统的回调接口,采用挂起函数重构了复杂的异步流,带来了前所未有的开发体验。

1.🚀 纯协程驱动的路由链路
  • 挂起式页面通信:通过 awaitNavigateForResult 无阻塞等待下一个页面的返回值,彻底消灭 onActivityResult 的回调嵌套。

  • 挂起式路由守卫RouterGuard 的拦截方法为挂起方法,您可以在守卫中自由发起网络请求、下载动态模块或弹窗请求权限,无需额外的线程切换与闭包等待。

  • 挂起式全局降级RouterHandler 的 404 页面未找到或外部路由处理均为协程环境,轻松实现异步容错与重定向。

2.纯 KSP 编译期扫描:完全无侵入,无需手动注册,支持增量编译。
3.全自动跨模块聚合:业务模块按需生成子路由表,App 主模块自动聚合所有子模块,彻底解决多模块组件化难题。

4.强大的传参机制:支持 URL Path 变量 (/user/{id})、Query 参数 (?id=1)、复杂大对象 (extra) 以及基于数据类的强类型安全传递

5.跨模块服务发现:轻松实现接口下沉与依赖反转,提供优雅的 rememberService<T>() 协程安全挂起加载,确保服务单例的线程与协程安全。

6.生命周期与 ViewModel 绑定:内置专属页面作用域,提供 tianquViewModel<T>()。支持跨平台无反射的 @InjectViewModel 自动依赖注入 tianQuViewModelInject<T>(),当页面出栈时,不仅自动销毁 ViewModel,还会自动取消其绑定的所有协程任务,彻底告别内存泄漏与空指针。

7.多类型路由支持:支持配置页面为全屏页面(RouteType.SCREEN)或悬浮弹窗(RouteType.DIALOG),通过同一套 Navigator 接口无缝调起和传参,甚至支持弹窗返回结果给底层全屏页面。

8.并发预加载引擎:内置基于 CompletableDeferred 的非阻塞预加载器 (RoutePreloader)。在转场动画播放的同时,后台协程高并发加载目标页数据,实现“瞬开”体验。

9.离线 DeepLink 意图缓存:内置基于协程无界 Channel 的意图队列。冷启动或多线程高频触发外部唤起时,底层协程会自动缓冲并按序消费路由,绝不丢失任何一次跳转意图。

10.高级导航表现:内置单例模式控制 (SINGLE_TOPSINGLE_TASK) 且支持零重组代价的参数复用更新、多返回栈(无缝衔接底层 Tab 栏)、自定义动画过渡、Compose 共享元素动画及 404 全局降级。

📦 引入与配置

目前可通过 Maven Central 引入最新版天衢路由依赖。

1. 添加仓库

⚠️ 注意:需要在 settings.gradle.kts 的 pluginManagement 和 dependencyResolutionManagement 中,将以下仓库添加到第一位(鸿蒙所依赖的 Kotlin/CMP fork 版本仅存于此仓库):

// settings.gradle.ktspluginManagement {    repositories {        // 鸿蒙所依赖的 Kotlin/CMP 仅存于此仓库        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/"// 必须放第一位        google()        mavenCentral()        gradlePluginPortal()    }}dependencyResolutionManagement {    repositories {        // 鸿蒙所依赖的 Kotlin/CMP 仅存于此仓库        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/"// 必须放第一位        google()        mavenCentral()    }}

在项目根目录或 gradle/libs.versions.toml 中配置:

[versions]tianqu-router-annotations = "1.0.8" # 替换为最新版本号tianqu-router-processor = "1.0.8"tianqu-router-runtime = "1.0.8"ksp = "2.2.21-2.0.4"[libraries]tianqu-router-annotations = { module = "io.gitee.zhongte:tianqu-router-annotations", version.ref = "tianqu-router-annotations" }tianqu-router-processor = { module = "io.gitee.zhongte:tianqu-router-processor", version.ref = "tianqu-router-processor" }tianqu-router-runtime = { module = "io.gitee.zhongte:tianqu-router-runtime", version.ref = "tianqu-router-runtime" }[plugins]ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

2. 业务子模块配置 (Feature Module)

子模块需要依赖 annotations 注解库和运行时库,并应用 KSP 处理器来生成自身的路由表。同时可以通过 KSP 参数 tianqu.moduleName 指定生成的类名前缀。注意:必须配置 Java 17 并在 commonMain 中添加 KSP 生成的代码目录,否则会导致编译时找不到生成的类。

// feature-a/build.gradle.ktsplugins {    alias(libs.plugins.kotlinMultiplatform)    alias(libs.plugins.composeMultiplatform)    alias(libs.plugins.ksp) // 引入 KSP}kotlin {    // 确保编译目标使用 Java 17    jvmToolchain(17    // 或者在 androidTarget/jvm 中显式指定 jvmTarget = "17"    sourceSets {        commonMain.dependencies {            implementation(libs.tianqu.router.annotations) // 注解库            implementation(libs.tianqu.router.runtime) // 运行时库        }    }}// 【必填项】:为该模块生成的 Router 注册类指定模块前缀,建议直接使用 project.name 动态获取ksp {    arg("tianqu.moduleName", project.name)}dependencies {    add("kspCommonMainMetadata", libs.tianqu.router.processor)}// 【必填项】:将 KSP 生成的代码目录添加到 commonMain 的 source sets 中kotlin.sourceSets.commonMain {    kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")}// 【必填项】:确保所有 Kotlin 编译任务在 KSP 生成 metadata 之后执行tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {    if (name != "kspCommonMainKotlinMetadata") {        dependsOn("kspCommonMainKotlinMetadata")    }}

3. 主工程模块配置 (App Module) 【极其重要❗】

主工程除了依赖 runtime 核心库外,必须在 KSP 配置中声明自己为 App 模块,并且同样需要配置 Java 17 和 KSP 代码生成目录关联。

// composeApp/build.gradle.ktsplugins {    alias(libs.plugins.ksp)}// 【必填项】:告知处理器当前模块为主工程,需要聚合全工程路由表ksp {    arg("tianqu.moduleName", project.name)    arg("tianqu.isApp""true"}kotlin {    jvmToolchain(17)    sourceSets {        commonMain.dependencies {            implementation(libs.tianqu.router.annotations)            implementation(libs.tianqu.router.runtime) // 必须引入 Runtime            // 依赖你的所有业务模块            implementation(project(":feature-a"))            implementation(project(":feature-b"))        }    }}dependencies {    add("kspCommonMainMetadata", libs.tianqu.router.processor)}// 【必填项】:将 KSP 生成的聚合路由表代码目录添加到 commonMain 的 source sets 中kotlin.sourceSets.commonMain {    kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin")}// 【必填项】:确保 KSP 任务优先执行tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {    if (name != "kspCommonMainKotlinMetadata") {        dependsOn("kspCommonMainKotlinMetadata")    }}

🚀 基础路由导航与全局初始化

1. 全局初始化 RouterHost

在 Compose Multiplatform 的共享 UI 层 (App.kt) 根节点,调用 rememberAppTianQuState 完成框架装配,并使用 RouterHost 承载。

框架会自动完成跨模块服务初始化、路由事件监听和返回键拦截,业务只需声明自己的策略。

// App.kt@ComposablefunApp() {    // 1. 定义路由拦截器(只有业务策略,框架装配已内部处理)    val guards = remember {        listOf(            object : RouterGuard {                override funmatches(context: RouterContext)Boolean {                    return context.url.startsWith("/user")                }                override suspend funcanActivate(context: RouterContext, chain: GuardChain)Boolean {                    println("🚀 [局部拦截器] 进入 User 模块,URL: ${context.url}")                    return chain.proceed(context)                }            }        )    }    // 2. 一行完成框架装配:    //   - 自动注入 GlobalRouteAggregator.routers 和 services(无需手动传)    //   - 自动初始化跨模块服务、监听路由事件、挂载返回键拦截    val navigator = rememberTianQuApp {       // 自动注入 KSP 生成的产物       routes = GlobalRouteAggregator.routers       serviceProviders = GlobalRouteAggregator.services        // 传入第一个页面的路径        startRoute = "/main_tab"        this.guards = guards        onRouteEvent = { event, nav ->            when (event) {                is RouterEvent.NotFound -> nav.navigateTo("/main_tab"// 404 降级,如果找不到目标页面,跳转到/main_tab                is RouterEvent.Navigated -> println("✅ 跳转成功: ${event.url}")            }        }    }    MaterialTheme {        // 3. RouterHost 接管全部路由渲染,Navigator 可随时通过 LocalNavigator.current 获取        RouterHost(navigator = navigator)    }}

2. 获取 Navigator 的多种方式

页面跳转需要先拿到 Navigator 对象,然后调用 navigateTo。

获取 Navigator 对象,除了通过 CompositionLocal 获取,如果您处于协程作用域内,还可以直接从上下文中提取!

import shijing.tianqu.router.Routerimport shijing.tianqu.router.RouterContextimport shijing.tianqu.runtime.LocalNavigatorimport shijing.tianqu.runtime.rememberRouterScopeimport shijing.tianqu.runtime.Navigatorimport kotlinx.coroutines.launch@Router(path = "/home", transition = "Fade") // 可选设置动画@ComposablefunHomeScreen(context: RouterContext) {    // 方式一:在 Compose 作用域内获取 (最常用)    val navigator = LocalNavigator.current    // 方式二:在挂起函数 / 专属协程作用域内获取 (极客玩法)    val routerScope = rememberRouterScope()    Button(onClick = {        routerScope.launch {            // 通过协程上下文直接提取当前的 Navigator            val nav = coroutineContext[Navigator] ?: return@launch            // 跳转到目标页面            nav.navigateTo("/profile")        }    }) {        Text("跳转个人资料页")    }}

3. 页面状态保存(remember vs rememberSaveable)【重要】

天衢底层的 RouterHost 通过 rememberSaveableStateHolder 管理返回栈 —— 当 A 跳转到 B 时,A 会被移出组合树;从 B 返回时,A 重新进入组合树并恢复状态。

框架已帮你处理了底层的 SaveableStateHolder业务侧只需要用 rememberSaveable 声明需要跨页面保留的 UI 状态即可

@Router("/home")@ComposablefunHomeScreen(context: RouterContext) {    // ❌ 错误写法:跳转后返回,该状态会丢失(重置为 0)    // var testCounter by remember { mutableStateOf(0) }    // ✅ 正确写法:跳转后返回,状态依然保留    var testCounter by rememberSaveable { mutableStateOf(0) }    Button(onClick = { testCounter++ }) {        Text("计数值: $testCounter")    }}

4. 物理返回键/侧滑手势返回拦截 (Android/iOS)

在 Android 上,通常需要拦截系统的物理返回键;在 iOS 上,则是侧滑返回。

天衢框架已在 router-runtime 内部内置了跨平台 BackHandler 实现,并由 rememberAppTianQuState 默认自动挂载,无需业务层手动编写任何 expect/actual 桥接代码

默认行为:当导航栈中多于 1 个页面时,物理返回键/侧滑手势会自动触发出栈。

如需禁用默认的全局返回拦截(例如某些特殊场景需要完全自定义),可通过配置关闭:

val navigator = rememberAppTianQuState {    startRoute = “/main_tab”    enableBackHandler = false // 关闭框架默认的返回键拦截}

⚠️ 注意:对于鸿蒙来说,需要在 harmonyApp/entry/src/main/ets/pages/Index.ets 重写 onBackPress ,不是onBackPressed,方法名别写错。

一般来说,Index.ets是在创建项目的时候自动生成的,但自动生成的onBackPress方法可能会出错,可能会生成onBackPressed,多加了ed,一定要检查onBackPress方法是否正确。

// 这是正确的方法onBackPress(): boolean | void {    return this.controller ? this.controller.onBackPress() : false;}// 这是错误的,编译器自动生成的可能会是onBackPressedonBackPressed(): boolean {    return this.controller ? this.controller.onBackPress() : false;  }

如需在某个深层子页面拦截返回(例如表单页阻止意外返回),直接在该页面调用 BackHandler 即可,它会优先于全局拦截消费返回事件:

@Router(path = “/edit_form”)@ComposablefunEditFormScreen(context: RouterContext) {    val navigator = LocalNavigator.current    var hasUnsavedChanges by remember { mutableStateOf(true) }    // 子页面的 BackHandler 优先消费,不会与全局冲突    BackHandler(enabled = hasUnsavedChanges) {        // 弹窗提示用户有未保存的内容        println(“⚠️ 有未保存的内容,请先保存再退出”)    }}

💡 多层嵌套拦截提示:Compose 的 BackHandler 遵循”就近拦截(子优先)”原则。子页面的 BackHandler 会优先消费返回事件,完美覆盖全局的默认退栈逻辑,不会产生冲突。

5. 路由启动模式(LaunchMode):栈顶复用与栈内复用

天衢路由全面支持了类似 Android Activity 的高级启动模式 (LaunchMode),并且由于采用纯 Compose 状态驱动,复用页面时只有在参数发生实质性变化时才会触发重组,实现了真正的“零开销复用”。

如何使用:在 @Router 注解中指定 launchMode 即可。目前支持三种模式:

  • LaunchMode.STANDARD (默认):每次跳转都创建新页面并入栈。

  • LaunchMode.SINGLE_TOP:栈顶复用。如果当前栈顶已经是该页面,则复用栈顶页面,不创建新实例,同时将新的参数传递给旧页面。

  • LaunchMode.SINGLE_TASK:栈内复用。如果导航栈中已经存在该页面,则将其上的所有页面弹出栈,使该页面重新回到栈顶并复用,同时传递新参数。

import shijing.tianqu.router.LaunchModeimport shijing.tianqu.router.Router// 示例:设置栈内复用模式。// 假设栈为 [Home, Task, Detail],此时再跳到 /task,Detail 会被出栈,回到 [Home, Task]。@Router(path = "/task", launchMode = LaunchMode.SINGLE_TASK)@ComposablefunSingleTaskScreen(context: RouterContext) {    // context 会自动更新为最新传递进来的参数,并按需触发 Compose 重组。    // 如果传递的新参数与旧参数完全相同(实质内容一样),底层引擎会拦截赋值,不会引发任何 UI 重组!    Text("Task Page: ${context.queryParams["id"]}")}

💡 最佳实践:得益于 Compose 的 SaveableStateHolder,在 SINGLE_TASK 触发上层页面退栈而使复用页面重新展示时,原页面的滚动位置、输入框状态等均会原样保留且无缝衔接动画,完美还原 Android 的 onNewIntent 体验!

6. 自定义页面切换动画

天衢路由内置了常见的转场动画(如 FadeSlideScaleNone),默认使用的是Slide,这是安卓上经典的页面切换动画。

如果您需要极其炫酷的自定义动画,可以直接使用 @Transition 注解和 BaseTransitionStrategy 轻松实现,且完全支持 Compose 官方的动画 API!

实现步骤:

  • 继承 BaseTransitionStrategy 并重写入场/出场动画。
  • 使用 @Transition 注解标记,并起一个引用名称。
  • 在目标页面的 @Router 注解中使用这个名称。
import androidx.compose.animation.*import androidx.compose.animation.core.tweenimport shijing.tianqu.router.Routerimport shijing.tianqu.router.Transitionimport shijing.tianqu.runtime.transition.BaseTransitionStrategy// 1. 定义自定义动画策略@Transition(name = "RotateScale")class RotateScaleTransitionStrategy : BaseTransitionStrategy() {    // 入场动画:缩放并淡入    override fungetEnterTransition(        scope: AnimatedContentTransitionScope<StackEntry>,        initial: StackEntry,        target: StackEntry,        isPop: Boolean    ): EnterTransition {        return scaleIn(initialScale = 0.5f, animationSpec = tween(500)) +                fadeIn(animationSpec = tween(500))    }    // 出场动画:缩放并淡出    override fungetExitTransition(        scope: AnimatedContentTransitionScope<StackEntry>,        initial: StackEntry,        target: StackEntry,        isPop: Boolean    ): ExitTransition {        return scaleOut(targetScale = 1.5f, animationSpec = tween(500)) +                fadeOut(animationSpec = tween(500))    }}// 2. 在页面路由中引用它@Router(path = "/demo_anim", transition = "RotateScale")@ComposablefunDemoAnimScreen(context: RouterContext) {    // 页面内容...}

KSP 会自动将您的自定义动画收集到聚合器中,跳转时无缝衔接。

🎯 传参大满贯:各种传参场景与代码示例

天衢路由全面覆盖了不同场景的数据传递需求,不再有传统框架传参类型容易丢失的问题。

场景一:基于 URL 的基础类型传参 (Path & Query)

适用于深层链接 (DeepLink) 直接唤起 App,或者简单的 ID、状态位传递。

发起跳转:

// 跳转并拼接 Path 与 Query 参数navigator.navigateTo("app://shijing.tianqu/user/1001?source=home_banner&vip=true")

目标页面解析参数:

@Router(path = "/user/{id}")@ComposablefunUserDetailScreen(context: RouterContext) {    // 1. 从 context.pathParams 获取路径里的 {id}    val userId = context.pathParams["id"]    // 2. 从 context.queryParams 获取问号后的参数 (注意是 List,因为可能存在多个同名 key)    val source = context.queryParams["source"]?.firstOrNull()    val isVip = context.queryParams["vip"]?.firstOrNull()?.toBoolean() ?: false    Text("用户ID: $userId, 来源: $source, VIP: $isVip")}

场景二:传递内存级别复杂大对象 (Extra)

对于非序列化、直接传递的大对象(例如 Bitmap,或某些体积巨大、不方便格式化的类实例),可以直接通过 extra 字段传递。

发起跳转:

// 定义任意业务模型data class UserProfile(val name: String, val age: Intval isVip: Boolean)// 使用 extra 传递对象val profile = UserProfile(name = "Kotlin开发者", age = 25, isVip = true)navigator.navigateTo("/profile", extra = profile)

目标页面安全解包:

@Router(path = "/profile")@ComposablefunProfileScreen(context: RouterContext) {    // 使用 as? 进行安全的类型强转    val profileData = context.extra as? UserProfile    if (profileData != null) {        Text("你好,${profileData.name},年龄 ${profileData.age}")    } else {        Text("未接收到复杂参数对象")    }}

场景三:基于 Kotlinx.serialization 的强类型安全传参

如果希望享受绝对的类型安全检查,支持序列化并严防类型错误,请使用强类型传参。

前置准备:引入序列化插件与依赖

在项目根目录或 gradle/libs.versions.toml 中配置:

[versions]kotlinx-serialization = "1.9.1-0.3.0"[libraries]# 序列化kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }[plugins]ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }# 序列化插件kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

在需要使用强类型传参的模块的 build.gradle.kts 中,引入 Kotlin 官方的序列化插件及运行库:

plugins {   alias(libs.plugins.kotlinMultiplatform)   alias(libs.plugins.androidApplication)   alias(libs.plugins.composeMultiplatform)   alias(libs.plugins.composeCompiler)   alias(libs.plugins.ksp)    // 引入序列化插件   alias(libs.plugins.kotlinSerialization)}kotlin {    sourceSets {        commonMain.dependencies {           implementation(libs.tianqu.router.annotations)           implementation(libs.tianqu.router.runtime)           // 添加依赖           implementation(libs.kotlinx.serialization.json)        }    }}

步骤 1:定义可序列化的数据类

import kotlinx.serialization.Serializable@Serializabledata class UserDetailArgs(    val userId: Long,    val username: String,    val isVip: Boolean,    val scores: List<Int>)

步骤 2:发起强类型安全跳转 (navigateArgs)

// 使用 navigateArgs 方法navigator.navigateArgs(    path = "/typesafe_demo",    args = UserDetailArgs(        userId = 999L        username = "天衢路由"        isVip = true        scores = listOf(10098)    ))

步骤 3:目标页面强类型提取 (getTypedArgs<T>())

import shijing.tianqu.runtime.getTypedArgs@Router(path = "/typesafe_demo")@ComposablefunTypeSafeScreen(context: RouterContext) {    // 根据泛型类型自动反序列化,如果解析失败或缺失则为 null    val args = context.getTypedArgs<UserDetailArgs>()    if (args != null) {        Text("✅ 类型安全解析成功!用户名: ${args.username}")    }}

⚡️ 极致性能:非阻塞并发数据预加载 (Route Pre-fetching)

传统的页面开发模式往往面临两难选择:要么先跳转再请求(进入页面后有一段骨架屏/Loading 空白期等待数据),要么先请求再跳转(用户点击按钮后界面卡住无响应,等数据回来了才突然跳转)。

天衢路由作为一款基于协程驱动的现代框架,彻底解决了这个问题!我们提供了页面跳转与数据加载并发执行的预加载能力:在执行 navigateTo 的毫秒级瞬间,底层会通过 async 启动预加载协程,并在同时播放页面的 Compose 转场动画。

我们完美利用了动画过渡的 300~500 毫秒“空窗期”,等动画播完时,数据往往已经加载完毕,实现真正的“零秒开启、无缝平滑”极致体验。完全不阻塞主线程!

步骤 1:定义你的预加载器 (实现 RoutePreloader)

class UserDetailPreloader : RoutePreloader {    override suspend funpreload(context: RouterContext): Any? {        val userId = context.pathParams["id"] ?: return null        println("🚀 [后台线程] 开始预加载用户 $userId 的详细数据...")        // 模拟耗时网络请求(此挂起不会阻塞 UI 线程与转场动画!)        delay(500)        return UserDetailArgs(userId = userId.toLong(), username = "预加载大神", isVip = true, scores = emptyList())    }}

步骤 2:注册到 Navigator 初始化中

// 一定要先创建对象val preloaders = remember { mapOf("/demo_preload" to UserDetailPreloader()) }val navigator = rememberAppTianQuState {    startRoute = "/home"    this.preloaders = preloaders // 绑定路由路径与对应的预加载器}

请注意,不能通过下面的代码传递预加载器。直接写 preloaders = mapOf(...),这意味着每次重组时都会创建一个新的 Map 对象。rememberAppTianQuState 会根据参数(包括 preloaders)来 remember 导航器。因为传进去的 Map 对象一直在变,导致 Compose 认为参数变了,从而不断地重新创建 Navigator 对象并清空页面栈,最终表现出来的就是不断刷新的“白屏”。

// 错误示例val navigator = rememberAppTianQuState {    startRoute = "/home"    this.preloaders = mapOf("/demo_preload" to UserDetailPreloader()// 这是错误的写法}

如果不想在启动的时候就传递预加载器,可以通过下面的方式:

// 在跳转之前传递预加载器navigator.registerPreloader("/demo_preload"UserDetailPreloader())navigator.navigateTo("/demo_preload")

步骤 3:在目标页面一键提取数据 (rememberPreloadData<T>())无需关心复杂的协程等待逻辑,框架在底层已经将获取到的 Deferred 无缝传递给了页面,使用即可:

@Router(path = "/user_detail/{id}")@ComposablefunUserDetailScreen(context: RouterContext) {    // 🌟 在 Compose 中非阻塞地挂起等待预加载结果    val preloadedData = rememberPreloadData<UserDetailArgs>()    if (preloadedData == null) {        // 动画期间,由于数据还在飞奔请求中,可能会短暂显示 Loading        CircularProgressIndicator()    } else {        // 数据到达,直接渲染!此时转场动画可能刚巧播完,完美衔接!        Text("加载完毕,用户:${preloadedData.username}")    }}

🔄 页面结果回传 (页面间双向通信)

处理“选择联系人”、“修改设置后返回数据”等需求时,天衢提供了极为优雅的挂起式协程解决方案。

1. 目标页:如何发送返回值并返回?

无论调用方采用哪种方式,目标页面只需要调用 popBackStack(result) 即可!它支持传递任何类型的对象。

@Router(path = "/settings")@ComposablefunSettingsScreen(context: RouterContext) {    val navigator = LocalNavigator.current    Button(onClick = {        // 关闭当前页面,并将字符串对象作为结果弹回上一个页面        navigator.popBackStack(result = "这里是修改后的最新设置数据")    }) {        Text("保存并返回")    }}

2. 调用方做法一:协程挂起式 (awaitNavigateForResult) 【⭐ 强烈推荐】

告别嵌套“地狱回调”,像写同步代码一样写异步等待!(⚠️ 警告:请务必在与页面生命周期绑定的 navigator.coroutineScope 内启动协程,否则可能会因为屏幕重组导致协程取消!)

⚠️ 结果展示状态必须使用 rememberSaveable如果你要把 awaitNavigateForResult() 得到的返回值展示在当前页面 UI 上,承接结果的状态必须写成 rememberSaveable,不要写成普通 remember

因为从目标页返回后,当前页可能发生状态恢复;此时如果使用 remember,即使已经拿到结果,界面状态也可能被重置,表现为“拿不到返回值”或“返回值一闪而过”。

val navigator = LocalNavigator.current// 正确写法:用 rememberSaveable 保存页面恢复后的结果展示状态var returnedResult by rememberSaveable { mutableStateOf<String?>(null) }Button(onClick = {    // 启动伴随页面的安全协程    navigator.coroutineScope.launch {        // 🌟 代码在此处挂起!直到用户从设置页返回才会继续往下执行,非阻塞主线程        val result = navigator.awaitNavigateForResult("/settings")        // 检查返回值类型并消费        if (result is String) {            returnedResult = "协程获取: $result"        }    }}) {    Text("前往设置页 (等待结果)")}if (returnedResult != null) {    Text("收到返回值: $returnedResult")}

3. 调用方做法二:经典回调监听 (navigateWithResult)

如果您处于非协程环境(如对接特定的 Swift 闭包),或个人偏爱经典回调:

Button(onClick = {    navigator.navigateWithResult("/settings").onResult { result ->        if (result is String) {            returnedResult = "回调获取: $result"        }    }}) {    Text("前往设置页 (回调获取结果)")}

🔌 跨模块解耦:服务发现 (Service Discovery)

路由不仅为了页面跳转,也是为了跨模块间的接口调用与反向依赖注入。上层 app 可以调用底层 feature 模块的具体实现,而无需互相强依赖。

1. 定义接口 (放在基础 Common 模块)

interface UserService {    suspend fungetUserName(): String}

2. 具体实现 (放在具体的 Feature 模块)

添加 @Service 注解,KSP 处理器会自动将其注册到服务表中。

import shijing.tianqu.router.Service@Serviceclass UserServiceImpl : UserService {    override suspend fungetUserName(): String {        return "天衢注册用户"    }}

3. 在任意位置获取服务 (支持挂起与普通获取)

天衢提供了两种灵活的方式来获取服务实例:

方式一:在 Compose 中基于协程挂起获取 (rememberService)结合 Compose 最佳实践,无阻塞安全获取。

import shijing.tianqu.runtime.utils.rememberService@ComposablefunHomeScreen(context: RouterContext) {    // 🔥 自动安全挂起加载,支持自动重组    val userService = rememberService<UserService>()    // 因为服务加载可能需要时间,使用安全调用符处理空状态    val userName = userService?.getUserName() ?: "正在异步加载..."    Text("欢迎您, $userName")}

方式二:在任意非 Compose 代码中普通获取 (ServiceManager.getService)如果您在 ViewModel、Repository 或是普通的方法中,希望直接拿到服务实例:

import shijing.tianqu.runtime.service.ServiceManagerfunfetchUserData() {    // 返回对应的实现类,如果不存在则抛出异常或返回null (具体看您的封装)    val userService = ServiceManager.getService<UserService>()    // 如果您在普通协程里,也有挂起函数版本:    // val userService = ServiceManager.getServiceAsync<UserService>()}

🧬 ViewModel 与页面生命周期深度绑定

原生的 viewModel() 在纯 Compose Multiplatform 项目中往往缺乏路由弹栈感知能力。

天衢提供了与单次页面路由同生共死的专有 ViewModel,并支持三种不同的获取方式,满足各种复杂场景的需求。

1. 声明标准的 ViewModel

import androidx.lifecycle.ViewModelimport kotlinx.coroutines.flow.MutableStateFlowclass CounterViewModel : ViewModel() {    val count = MutableStateFlow(0)    init { println("ViewModel 初始化分配") }    // 🌟 当该路由页面被 pop 出栈时,此方法自动触发,释放内存!    override funonCleared() {        super.onCleared()        println("该页面已被销毁,释放各种流与资源!")    }}

2. 获取 ViewModel 的三种方式

在路由页面中,您可以通过以下三种方式获取与当前页面生命周期绑定的 ViewModel:

方式一:反射无参构造获取(tianquViewModel<T>())

最简单直接的方式,适用于 ViewModel 只有无参构造函数的情况。在 JVM/Android 平台上通过反射实例化。

import shijing.tianqu.runtime.tianquViewModel@Router(path = "/demo_viewmodel")@ComposablefunDemoViewModelScreen(context: RouterContext) {    // 自动通过反射调用无参构造函数创建 ViewModel    val viewModel = tianquViewModel<CounterViewModel>()    // ...}
  • 优点:使用极其简单,无需额外配置,开箱即用。
  • 缺点:依赖反射机制,在 iOS 等 Kotlin/Native 平台上不支持无参反射实例化,会导致 Crash;不支持带参数的构造函数。

方式二:自定义 Factory 获取(tianquViewModel<T>(factory))

适用于 ViewModel 需要传递参数(如 Repository、UseCase)或在 iOS 平台上运行的情况。

import shijing.tianqu.runtime.tianquViewModelimport androidx.lifecycle.ViewModelProviderimport androidx.lifecycle.viewmodel.CreationExtrasimport kotlin.reflect.KClass// 自定义 Factoryclass CounterViewModelFactory : ViewModelProvider.Factory {    override fun<T : ViewModel>create(modelClass: KClass<T>, extras: CreationExtras): T {        return CounterViewModel() as T // 如果有参数,可以在这里传入    }}@Router(path = "/demo_viewmodel")@ComposablefunDemoViewModelScreen(context: RouterContext) {    // 传入自定义 Factory 进行实例化    val viewModel = tianquViewModel<CounterViewModel>(factory = CounterViewModelFactory())    // ...}
  • 优点:灵活性最高,支持带参数的构造函数;完全兼容所有平台(包括 iOS)。
  • 缺点:每次使用都需要手动编写和传入 Factory,样板代码较多,略显繁琐。

方式三:基于 KSP 的全自动依赖注入(tianQuViewModelInject<T>()) 【⭐ 强烈推荐】

结合了前两者的优点,通过 @InjectViewModel 注解在编译期生成工厂代码,实现无反射、跨平台的自动注入。

步骤 A:在 ViewModel 上添加注解

import shijing.tianqu.router.InjectViewModel@InjectViewModel // 🌟 标记此 ViewModel 需要生成注入工厂classCounterViewModel : ViewModel(){    // ...}

步骤 B:在页面中一键获取

import shijing.tianqu.runtime.tianQuViewModelInject@Router(path = "/demo_viewmodel")@ComposablefunDemoViewModelScreen(context: RouterContext) {    // 底层自动从 ServiceLocator 获取生成的工厂并实例化,无需反射!    val viewModel = tianQuViewModelInject<CounterViewModel>()    // ...}
  • 优点:无反射(性能极佳)、完全跨平台(完美支持 iOS)、无样板代码(使用极其简单)。
  • 缺点:目前生成的默认工厂仅支持无参构造函数(若需带参构造,请继续使用方式二)。

💡 内存泄漏终结者:无论您使用哪种方式获取,当点击返回,当前页面被 popBackStack 移出栈时,框架将主动触发 ViewModel 的 onCleared(),杜绝任何内存泄漏。

🧩 协程驱动的动态按需懒加载 (Dynamic Feature)

在大型项目中,为了减小包体积或加快初始启动速度,某些业务模块可以设计为动态下载与按需加载。在传统路由中,这往往需要复杂的异步回调与占位页面。

而在天衢路由中,得益于底层的协程架构与强大的全局守卫 (RouterGuard),您可以像写同步代码一样轻松实现带有等待 UI 的动态模块加载

核心原理:

  1. 业务层调用挂起函数 navigator.push("/dynamic_route"),并在协程外层控制 “Loading 转圈” UI 状态。
  2. 路由框架进行匹配,发现目标路由不存在。
  3. 全局 RouterGuard 拦截到请求,判断该 URL 属于某个未加载的模块。
  4. 守卫在内部使用挂起执行下载逻辑(如下载 js bundle、加载 dex、或从服务器拉取配置)。
  5. 下载完成后,守卫动态注册新模块的路由表:navigator.registerDynamicRoutes(...)
  6. 守卫返回 true,拦截器放行。
  7. 框架自动完成向目标页面的跳转。
  8. 业务层 navigator.push 挂起恢复,隐藏 “Loading” 状态。

完整演示代码:

// 1. 定义动态加载守卫class DynamicFeatureGuard : RouterGuard {    // 拦截特定前缀的路由    override funmatches(context: RouterContext)Boolean {        return context.url.startsWith("/dynamic_feature")    }    override suspend funcanActivate(context: RouterContext, chain: GuardChain)Boolean {        val navigator = chain.navigator        // 判断路由表中是否已经有了该页面,如果没有才需要模拟下载        if (navigator.backStack.none { it.url == context.url } &&            !navigator.hasRoute(context.url)) {            println("正在异步下载并加载动态模块...")            // 模拟真实环境下的网络下载耗时            delay(2000)            // 下载完成后,动态将新模块的节点注册进当前导航器中            navigator.registerDynamicRoutes(listOf(                RouterNode(                    path = "/dynamic_feature",                    composable = { ctx -> DynamicFeatureScreen(ctx) }                )            ))            println("动态模块加载完毕,路由已注册!")        }        // 模块已就绪,放行        return chain.proceed(context)    }}// 2. 业务侧发起调用并展示 Loading UI@ComposablefunHomeScreen(context: RouterContext) {    val navigator = LocalNavigator.current    val coroutineScope = rememberCoroutineScope()    // 维护按钮的加载状态    var isDynamicLoading by rememberSaveable { mutableStateOf(false) }    Button(        onClick = {            coroutineScope.launch {                isDynamicLoading = true // 1. 显示转圈                // 2. 使用 suspend 的 push(),它会等待所有异步守卫(包含下载)执行完毕                navigator.push("/dynamic_feature")                isDynamicLoading = false // 3. 页面跳转成功或被拒绝后,隐藏转圈            }        }    ) {        if (isDynamicLoading) {            CircularProgressIndicator(modifier = Modifier.size(20.dp), color = Color.White)            Spacer(modifier = Modifier.width(8.dp))            Text("正在下载模块...")        } else {            Text("✨ 动态按需加载模块")        }    }}

通过这种方式,我们不仅完美隔离了底层模块的动态下载逻辑,同时让上层 UI 得到了优雅的状态管理,一切都得益于 Kotlin 协程 suspend 机制的强大力量。

🎨 其他极客能力全景公开

1. 多返回栈嵌套管理与 Tab 状态持久化

App 主页往往包含底部的多个 Tab。天衢底层深度打通了 SaveableStateHolder,完美解决“切换 Tab 重新渲染导致输入内容与滚动条丢失”的问题。这是一种原生的“单宿主+多挂载点”轻量实现:

@Router(path = "/main_tab")@ComposablefunMainTabScreen(context: RouterContext) {    // 1. 保存 Tab 选项索引    var selectedTab by rememberSaveable { mutableStateOf(0) }    // 2. 拿到自带的状态持久化容器    val saveableStateHolder = rememberSaveableStateHolder()    Scaffold(        bottomBar = {            // ... 底部导航栏切换 selectedTab        }    ) {        // 3. 为不同 Tab 提供状态独立作用域        if (selectedTab == 0) {            // 通过 SaveableStateProvider 包裹,哪怕切换到别的 Tab,它的内部状态依然被持久保留!            saveableStateHolder.SaveableStateProvider("tab_home") {                val homeNode = GlobalRouteAggregator.routers.find { it.path == "/home" }                homeNode?.composable?.invoke(RouterContext("/home"))            }        } else {            saveableStateHolder.SaveableStateProvider("tab_profile") {                val profileNode = GlobalRouteAggregator.routers.find { it.path == "/profile" }                profileNode?.composable?.invoke(RouterContext("/profile"))            }        }    }}

2. Compose 原生共享元素动画 (Shared Element)

天衢基于 Compose 1.7 实现了原生的共享元素能力。您只需用 routerSharedBounds(key) 为发起方和接收方的组件打上相同的 Key 标签,点击跳转时,框架将为您自动呈现惊艳的形变飞跃动画。

import shijing.tianqu.runtime.transition.routerSharedBounds// 页面 A (发起页) 中的组件Box(    modifier = Modifier        .routerSharedBounds(key = "shared_avatar_id"// 第一步:声明共享标签        .size(100.dp)        .clickable { navigator.navigateTo("/detail") })// 页面 B (详情页) 中的组件Image(    modifier = Modifier        .routerSharedBounds(key = "shared_avatar_id"// 第二步:目标页使用同一标签        .fillMaxWidth())

3. 全局路由 404 降级拦截处理

意外跳转到了未注册的页面怎么办?天衢不会发生崩溃,而是触发一个 NotFound 事件!

您只需在 rememberAppTianQuState 中配置 onRouteEvent,即可实现自由调度或重定向。

val navigator = rememberAppTianQuState {    startRoute = "/home"    onRouteEvent = { event, nav ->        when (event) {            // 🌟 拦截到了空路由 /not_exist_page            is RouterEvent.NotFound -> {                println("⚠️ [全局降级拦截] 找不到路由: ${event.url}")                // 您可以选择在这里跳转到一个专用的 H5 容错升级页面,或者回退到首页                nav.navigateTo("/home")            }            is RouterEvent.Navigated -> {                println("ℹ️ 跳转成功: ${event.url}")            }        }    }}

4. 离线 DeepLink 意图缓存 (Offline DeepLink Caching)

当应用在后台被系统杀死,或者尚未启动时,用户通过外部 DeepLink 唤起 App。此时底层的 UI 引擎和 Navigator 尚未初始化完毕,如果直接执行跳转,意图将会丢失。天衢路由内置了 DeepLinkManager,利用协程无界 Channel 实现了离线意图的 FIFO 缓存。

步骤 1:在原生平台入口接收并分发 DeepLink

// Android MainActivity.kt 或 iOS AppDelegate// 此时 Compose 和 Navigator 可能还未初始化val url = intent.data?.toString() ?: returnDeepLinkManager.dispatch(url)

步骤 2:Navigator 自动消费当 Navigator 初始化完成后,它会自动在内部协程中收集并消费这些被缓存的意图,确保用户无缝跳转到目标页面,绝不丢失任何一次外部唤醒!

5. 声明一个弹窗路由 (Dialog)

天衢支持将普通路由节点声明为弹窗类型,弹窗将悬浮展示在现有页面之上。您可以通过 BackHandler 和 clickable 拦截区域自由控制物理返回键与点击外部的关闭逻辑。

@Router(    path = "/demo_dialog",    // 指定类型为弹窗    type = RouteType.DIALOG)@ComposablefunDemoDialogScreen(context: RouterContext) {    val navigator = LocalNavigator.current    var dismissOnClickOutside by remember { mutableStateOf(true) }    var dismissOnBackPress by remember { mutableStateOf(true) }    BackHandler(enabled = true) {        if (dismissOnBackPress) {            navigator.popBackStack()        }    }    Box(        modifier = Modifier            .fillMaxSize()            .clickable(                interactionSource = remember { MutableInteractionSource() },                indication = null            ) {                if (dismissOnClickOutside) {                    navigator.popBackStack()                }            },        contentAlignment = Alignment.BottomCenter    ) {        Box(            modifier = Modifier                .fillMaxWidth()                .wrapContentHeight()                .background(                    color = MaterialTheme.colorScheme.surface,                    shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp)                )                .clickable(                    interactionSource = remember { MutableInteractionSource() },                    indication = null                ) {}                .padding(24.dp)        ) {            Column {                Text("业务层控制弹窗行为演示")                Switch(                    checked = dismissOnClickOutside,                    onCheckedChange = { dismissOnClickOutside = it }                )                Switch(                    checked = dismissOnBackPress,                    onCheckedChange = { dismissOnBackPress = it }                )                Button(onClick = { navigator.popBackStack() }) {                    Text("关闭弹窗")                }            }        }    }}

上面的示例对应仓库中的 composeApp/src/commonMain/kotlin/shijing/tianqu/screens/DemoDialogScreen.kt,说明天衢不只支持全屏页面跳转,也支持将路由节点作为弹窗悬浮在当前页面之上。

弹窗的遮罩、点击空白关闭、返回键关闭等交互由业务层自己定义。

感谢您选择使用天衢,希望这条大道能让您的跨平台开发之旅风雨无阻!

如果你在接入的过程中,遇到问题,可以下载源码(链接:https://github.com/peiyunfei/TianQu),让 AI 参考源码,给你解决问题。后续提供一个skill,只需一句话,就可以让 AI 自动接入天衢。

最后点击查看实现原理(链接:https://github.com/peiyunfei/TianQu/tree/master/%E6%96%87%E6%A1%A3)。

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-06 21:30:34 HTTP/2.0 GET : https://c.mffb.com.cn/a/495935.html
  2. 运行时间 : 0.106079s [ 吞吐率:9.43req/s ] 内存消耗:4,711.80kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=3e62715ac626a42307dec0ddad91d13d
  1. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/runtime/temp/cefbf809ba1a84190cb04b0cb7abcf79.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/c.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000629s ] mysql:host=127.0.0.1;port=3306;dbname=c_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000871s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000284s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000316s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000484s ]
  6. SELECT * FROM `set` [ RunTime:0.000221s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000592s ]
  8. SELECT * FROM `article` WHERE `id` = 495935 LIMIT 1 [ RunTime:0.000608s ]
  9. UPDATE `article` SET `lasttime` = 1783344634 WHERE `id` = 495935 [ RunTime:0.014401s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000312s ]
  11. SELECT * FROM `article` WHERE `id` < 495935 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000576s ]
  12. SELECT * FROM `article` WHERE `id` > 495935 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.004724s ]
  13. SELECT * FROM `article` WHERE `id` < 495935 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.003262s ]
  14. SELECT * FROM `article` WHERE `id` < 495935 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.000660s ]
  15. SELECT * FROM `article` WHERE `id` < 495935 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.008688s ]
0.107742s