📦 本文基于的完整项目源码:https://github.com/meirongdev/shop

2026-04 实践更新 当前主线后端已经完成 RS256 + JWKS、BFF @HttpExchange typed client、以及 Redis/Redisson 统一接入,因此本文里 KMP 前端与后端协作的认证 / API 访问背景,比文章初稿时更加接近真实可运行平台。

Shop Platform 的前端策略是渐进式的:买家门户用 Kotlin + Thymeleaf SSR 承担 SEO 和免登交易路径,交互式购物体验和卖家工作台则采用 Compose Multiplatform 的共享模块实现。当前仓库已经提交了 WASM 目标、Android 包装 App,以及 iOS framework target;但没有把完整 iOS App shell 一起提交。这不是"所有端都用一套代码"的理想化方案,而是当前阶段的折中——SSR 管 SEO,KMP 管交互,各取所需。


整体架构#

flowchart TD subgraph KMP["Compose Multiplatform 分层"] direction TB Core["core
网络层 / 模型 / Session"] Auth["feature-auth
认证流程"] Cart["feature-cart
购物车管理"] Market["feature-marketplace
商品浏览"] Order["feature-order
订单管理"] Profile["feature-profile
用户档案"] Promo["feature-promotion
促销活动"] Wallet["feature-wallet
钱包支付"] UIShared["ui-shared
通用 UI 组件 / 主题"] BuyerApp["buyer-app
买家应用组装"] SellerApp["seller-app
卖家应用组装"] end Core --> Auth Core --> Cart Core --> Market Core --> Order Core --> Profile Core --> Promo Core --> Wallet UIShared --> BuyerApp UIShared --> SellerApp BuyerApp --> WASM["WASM Web
/buyer-app/"] BuyerApp --> Android["Android APK
wrapper app"] BuyerApp --> iOS["iOS Framework"] SellerApp --> WASM2["WASM Web
/seller/"] SellerApp --> Android2["Android APK
wrapper app"] SellerApp --> iOS2["iOS Framework"]

功能模块化与分层#

模块清单#

模块 类型 职责
core 公共库 网络层(Ktor Client)、数据模型、Session 管理、DI
feature-auth 功能模块 登录/注册/游客登录 UI + Repository
feature-cart 功能模块 购物车列表、增删改、合并逻辑
feature-marketplace 功能模块 商品浏览、搜索、详情、卖家库存管理
feature-order 功能模块 订单列表、详情、状态追踪
feature-profile 功能模块 用户档案、地址簿、卖家店铺
feature-promotion 功能模块 促销活动列表、优惠券展示
feature-wallet 功能模块 钱包余额、充值、支付方式展示
ui-shared 公共库 ProductCard、SearchBar、PriceTag、ShopTheme
buyer-app 应用组装 买家路由、底部导航、Auth 流程
seller-app 应用组装 卖家路由、商品管理、订单管理

core 模块:网络层与 Session 管理#

HTTP 客户端#

KMP 使用 Ktor Client 作为跨平台 HTTP 客户端:

// core/src/commonMain/kotlin/.../network/HttpClientFactory.kt
object HttpClientFactory {
    fun create(tokenStorage: TokenStorage): HttpClient = HttpClient(createHttpEngine()) {
        install(ContentNegotiation) {
            json(Json {
                ignoreUnknownKeys = true
                isLenient = true
            })
        }
        install(Auth) {
            bearer {
                loadTokens { tokenStorage.loadTokens() }
                refreshTokens { tokenStorage.refreshTokens(client) }
            }
        }
        install(HttpRequestRetry) {
            retryOnServerErrors(maxRetries = 2)
            exponentialDelay()
        }
    }
}

关键设计:

  • 当前示例里直接开启了 HTTP 重试HttpRequestRetry 对 5xx 做最多 2 次指数退避重试
  • TokenStorage 还比较朴素:当前仓库里的 MutableTokenStorage 是内存态,并没有真正落到 localStorage / SharedPreferences / Keychain

平台差异处理#

KMP 通过 expect/actual 处理平台差异:

// core/src/commonMain/.../network/GatewayApiBaseUrl.kt
expect fun gatewayApiBaseUrl(): String

// wasmJs
actual fun gatewayApiBaseUrl(): String = "/api"

// androidMain
actual fun gatewayApiBaseUrl(): String = "http://10.0.2.2:8080/api"

// iosArm64 / iosSimulatorArm64
actual fun gatewayApiBaseUrl(): String = "http://127.0.0.1:8080/api"

参考:Ktor Client DocumentationKotlin expect/actual


买家端的游客态边界#

买家应用启动时自动以游客身份登录,让未登录用户也能立即浏览商品和加购物车:

LaunchedEffect(Unit) {
    if (autoLoginAttempted || buyerSession != null) return@LaunchedEffect
    autoLoginAttempted = true
    val result = runCatching { authRepository.loginGuest("buyer") }
    result.getOrNull()?.let { session ->
        buyerSession = session
        tokenStorage.saveTokens(session.accessToken, session.accessToken)
    }
}

游客态下,Orders、Wallet、Profile 三个入口会从底部导航里隐藏,只留 Marketplace、Cart、Promotions、Auth。更关键的是 BuyerCartScreen 当前明确写了 allowCheckout = !session.isGuestBuyer(),所以 buyer-app 的 guest session 目前还不能直接结账

这是一个有意但需要尽快补齐的边界。从产品和合规角度看,guest checkout 仍然是值得尽快补上的能力;当前实现更像 POC/demo 阶段的取舍,面向真实用户时还需要继续完善。

这也是它和 SSR buyer-portal 的一个关键差异——SSR 版本支持完整的 guest 购买路径,而 KMP 版本还需要用户先升级成正式买家才能完成支付。


WASM 前端策略#

为什么选 WASM#

Compose Multiplatform 支持编译为 WASM(WebAssembly),让 Kotlin/Compose 代码直接在浏览器中运行:

维度 WASM SPA 传统 SSR
SEO ❌ 需要额外 SSR ✅ 原生 SEO
交互体验 ✅ 即时响应,无页面刷新 ⚠️ 需要整页或局部刷新
部署 静态文件,Nginx 即可 需要 JVM 运行
开发体验 与 Android/iOS 共享代码 独立 Thymeleaf 模板

Shop Platform 的选择:

  • Buyer Portal(Thymeleaf SSR):登录页、注册页、商品浏览页、guest checkout —— SEO + 免登交易路径
  • Buyer App(KMP WASM):登录态的商品浏览、购物车、订单交互 —— 交互密集,不需要 SEO

WASM 部署#

Gateway 路由配置:

- id: buyer-app
  uri: ${BUYER_APP_URI:http://buyer-app:80}
  predicates:
    - Path=/buyer-app/**
  filters:
    - StripPrefix=1
    - PreserveHost

buyer-app 的 Dockerfile 是一个简单的 Nginx 配置:

FROM nginx:alpine
COPY dist/wasmJs/productionExecutable/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf

WASM 的局限性#

  • 首屏体积仍要关注:加载时延高度取决于 bundle 大小、压缩策略和网络质量,不能写成一个固定秒数
  • 浏览器能力要单独验证:现代浏览器普遍支持 WebAssembly,但 Kotlin/Wasm 相关生态和调试能力仍在持续演进
  • SEO 不可用:WASM 渲染内容依旧不适合承担 SEO 关键入口

与后端 BFF 的协作#

请求链路#

flowchart LR KMP["KMP WASM/Android/iOS"] GW["API Gateway"] BFF["buyer-bff / seller-bff"] Domain["领域服务"] KMP -->|"GET /api/buyer/v1/marketplace"| GW GW -->|"JWT 校验 + Trusted Headers"| BFF BFF -->|"RestClient 聚合调用"| Domain Domain --> BFF --> GW --> KMP

JWT 管理#

KMP 应用中的 JWT 生命周期:

  1. 获取 Token:调用 /auth/login/auth/guest-login 获取 JWT
  2. 当前存储MutableTokenStorage 只保存在内存里,尚未映射到平台安全存储(localStorage / SharedPreferences / Keychain)
  3. 自动注入:Ktor Client 的 Auth 插件在每个请求中自动添加 Authorization: Bearer {token}
  4. refresh token 尚未接入refreshTokens() 目前只是返回已有 token,并未真正发起 refresh 轮换
  5. 响应解包:KMP 侧统一解包 ApiResponse<T>,并把业务错误映射为 NetworkError 密封类

多平台目标声明#

buyer-app/build.gradle.kts 这段 kotlin { ... } 是本文里最直接体现"一次配置跑三端"的地方:

// buyer-app/build.gradle.kts
kotlin {
    androidLibrary {
        namespace = "dev.meirong.shop.buyer"
        compileSdk = 35
        minSdk = 26
    }
    iosArm64 {
        binaries.framework {
            baseName = "BuyerApp"
            isStatic = true
        }
    }
    iosSimulatorArm64 {
        binaries.framework {
            baseName = "BuyerApp"
            isStatic = true
        }
    }
    wasmJs {
        binaries.executable()
        browser {
            commonWebpackConfig {
                outputFileName = "buyer-app.js"
            }
        }
    }
}

另外,仓库还额外提交了 buyer-android-app / seller-android-app 两个 Android wrapper app;它们依赖 :kmp:buyer-app / :kmp:seller-app,作为真正可安装的 Android 入口。

参考:Kotlin Multiplatform Project Structure


当前阶段的经验与不足#

经验#

  1. feature 模块化:同一套 feature-* 模块同时服务于 WASM、Android、iOS framework targets,目前复用效果还不错
  2. expect/actual 是很重要的跨平台基石:当前主要用于网关基础地址与引擎差异
  3. 与后端 BFF 的边界相对清晰:KMP 侧统一消费 ApiResponse<T>,错误模型和 DTO 边界比较稳定

不足#

  1. WASM 首屏加载仍需优化:bundle 体积、压缩和缓存策略待继续完善
  2. Token 存储未持久化MutableTokenStorage 仍是内存态,适合 demo / POC 阶段
  3. iOS 侧只有 framework target:需单独补 App shell 与打包流程才能交付给真实用户
  4. Guest checkout 尚未支持:如前所述,这在 POC 阶段可以接受,但面向真实用户时需尽快补齐

参考与实现位置#


小结#

SSR 管 SEO,KMP 管交互,core + feature-* + ui-shared 管复用——这是 Shop Platform 当前阶段的选择,不太像什么终局方案,后面大概率还会继续调整。

项目仓库:github.com/meirongdev/shop(私有,仅供参考)