Compose Multiplatform 跨端实战:一套代码跑 WASM / Android / iOS 的电商应用
目录
📦 本文基于的完整项目源码:https://github.com/meirongdev/shop
2026-04 实践更新 当前主线后端已经完成 RS256 + JWKS、BFF
@HttpExchangetyped 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 管交互,各取所需。
整体架构#
网络层 / 模型 / 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"
买家端的游客态边界#
买家应用启动时自动以游客身份登录,让未登录用户也能立即浏览商品和加购物车:
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 的协作#
请求链路#
JWT 管理#
KMP 应用中的 JWT 生命周期:
- 获取 Token:调用
/auth/login或/auth/guest-login获取 JWT - 当前存储:
MutableTokenStorage只保存在内存里,尚未映射到平台安全存储(localStorage/SharedPreferences/ Keychain) - 自动注入:Ktor Client 的
Auth插件在每个请求中自动添加Authorization: Bearer {token} - refresh token 尚未接入:
refreshTokens()目前只是返回已有 token,并未真正发起 refresh 轮换 - 响应解包: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 入口。
当前阶段的经验与不足#
经验#
- feature 模块化:同一套
feature-*模块同时服务于 WASM、Android、iOS framework targets,目前复用效果还不错 expect/actual是很重要的跨平台基石:当前主要用于网关基础地址与引擎差异- 与后端 BFF 的边界相对清晰:KMP 侧统一消费
ApiResponse<T>,错误模型和 DTO 边界比较稳定
不足#
- WASM 首屏加载仍需优化:bundle 体积、压缩和缓存策略待继续完善
- Token 存储未持久化:
MutableTokenStorage仍是内存态,适合 demo / POC 阶段 - iOS 侧只有 framework target:需单独补 App shell 与打包流程才能交付给真实用户
- Guest checkout 尚未支持:如前所述,这在 POC 阶段可以接受,但面向真实用户时需尽快补齐
参考与实现位置#
- Compose Multiplatform 官方文档:https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform.html
- Kotlin Multiplatform 学习资源:https://kotlinlang.org/docs/multiplatform/kmp-learning-resources.html
- Kotlin/Wasm Overview:https://kotlinlang.org/docs/wasm-overview.html
- Ktor Client Documentation:https://ktor.io/docs/welcome.html
- Kotlin expect/actual:https://kotlinlang.org/docs/multiplatform/multiplatform-expect-actual.html
- Kotlin Multiplatform Project Structure:https://kotlinlang.org/docs/multiplatform/multiplatform-discover-project.html
- Kotlin Multiplatform Quickstart:https://kotlinlang.org/docs/multiplatform/quickstart.html
- 仓库实现入口:
frontend/kmp/buyer-app/src/commonMain/kotlin/dev/meirong/shop/buyer/BuyerApp.kt、frontend/kmp/buyer-app/build.gradle.kts、frontend/kmp/buyer-android-app/build.gradle.kts、frontend/kmp/core/src/commonMain/、frontend/kmp/core/src/androidMain/、frontend/kmp/core/src/iosArm64Main/、frontend/kmp/core/src/iosSimulatorArm64Main/、frontend/settings.gradle.kts
小结#
SSR 管 SEO,KMP 管交互,core + feature-* + ui-shared 管复用——这是 Shop Platform 当前阶段的选择,不太像什么终局方案,后面大概率还会继续调整。
项目仓库:github.com/meirongdev/shop(私有,仅供参考)