本文所有结论均经过运行验证(Web 端全链路),完整模板见文末仓库地址(v1.0.0,MIT 可直接套用)。
导语:uni-app x 的「工程化真空」
uni-app x 是 DCloud 的新一代跨端方案:用 UTS 语言开发,编译期静态模型,一份代码编译为 JS(Web / 小程序)与 Kotlin / Swift 原生代码。性能确实能打------但当你想认真做一个中大型项目时,会撞上一个尴尬的现实:生态默认形态是「页面堆」。
请求写在页面里、状态挂在页面 data、跳转靠裸字符串路径、响应是裸 UTSJSONObject 下标取值。项目一旦超过三个页面,改一个接口要全局搜索。
熟悉标准 Vue3 工程化的同学会想:把那套搬过来不就行了?不行。 uni-app x 的页面是编译期静态模型(多端编译到 JS / Kotlin / Swift),标准 Vue3 的「单路由表、单插件协议」形态在这里有四处根本性差异:
- 没有单路由表 ------页面在
pages.json静态注册,编译期确定,运行时注册不存在; - 插件协议不可靠 ------App 端(Kotlin)
app.use(pinia)实测不生效; - 存储全是异步------没有同步 cookie 可用;
- 名义类型系统------Kotlin 端不是结构化类型,类型不兼容不是报错而是 cast 崩溃。
这意味着工程化必须在底座约束下重新设计,而不是搬运。本文就是这套设计的完整记录。
一、先看结论:最终长什么样
1.1 目录结构
bash
├── main.uts # 唯一装配点:app.use(router) + initRequest() + setupPermission()
├── App.uvue # 应用生命周期
│
├── api/ # 接口层:薄封装(请求转发 + 路径收敛 + DTO 还原)
│ ├── user.uts
│ ├── types.uts # 域类型聚合出口(具名类型转发)
│ └── types/api.uts # DTO 唯一事实源(LoginResult / UserInfo)
│
├── stores/ # 状态层:免挂载,模块级注册表即全局单例
│ ├── index.uts / types.uts / types/
│ └── modules/user.uts # token/name/avatar + login/getInfo/logout
│
├── composables/ # 组合式函数:函数直接导入,类型走聚合出口
├── components/ # easycom 约定目录,页面免注册使用
│
├── router/ # 路由层:createRouter + 全局守卫 + 插件生成的路由表
│ ├── index.uts / permission.uts
│ └── routes.gen.uts # ⚠ vite 插件自动生成,勿手改
│
├── utils/ # request.uts(网络封装)/ auth.uts(token 读写)
├── config/index.uts # 全局唯一配置源
└── pages/ # 页面,pages.json 静态注册
1.2 核心调用链
bash
页面(.uvue)
→ composables/useXxx(横切 UI 状态)
→ stores/modules/*(业务状态与动作)
→ api/<domain>(DTO 还原)
→ utils/request(拦截器:token 注入 + 统一错误 toast)
→ mx-request → uni.request
三条硬规则贯穿全程:
- 页面不直连网络层,不接触裸 JSON;store 不反向引用组件;
- api 层只做薄封装:请求转发 + 路径收敛 + DTO 还原,禁止写业务逻辑;
- 装配点唯一 :全局初始化只发生在
main.uts,禁止在模块顶层做副作用。
二、关键设计决策
2.1 三种装配范式并存------不是不统一,是底座如此
uts
// main.uts(唯一装配点,保持极薄)
app.use(router) // ux-router:实现了 Vue install 协议
initRequest() // mx-request:纯函数库,全局默认实例 + 双拦截器(幂等)
setupPermission() // 注册 beforeEach + 冷启动补跑守卫链
注意三个插件用了三种不同的接入方式 :router 走 app.use(install 协议在 Kotlin 端可靠),pinia 走免挂载(app.use(pinia) 在原生端实测不可靠,改为模块级注册表实现全局单例),request 走显式初始化函数(避免模块顶层副作用的时序问题)。
这不是设计洁癖问题------是三种机制在原生端的实测结果决定了各自唯一可靠的接入形态。
2.2 鉴权数据流:用内存缓存抹平「异步存储」
Web 时代的经验是 js-cookie 同步读 token,到处直接取。但 uni-app x 的持久化(ux-storage)是异步的,而守卫、请求拦截器都是「同步心智」。直接 async/await 到处传染会污染整条调用链。
解法是模块级内存缓存 + 显式就绪原语:
utils/auth.uts维护 token 的内存缓存:写入时「内存即时生效 + 异步落盘」,读取永远是同步的;ensureAuthReady():冷启动时由守卫链首行await调用,从持久层恢复内存缓存。内部 Promise 复用------只读一次持久层,并发调用共享同一 Promise,无竞态。
这样异步被压缩在启动的一瞬间,之后的整条链路(守卫、拦截器、页面)都回到同步心智。
2.3 全局守卫:白名单 + 补跑 + 兜底,三层防漏
守卫逻辑对标经典 admin 场景:
csharp
await ensureAuthReady() // 异步守卫,ux-router 守卫链支持 await Promise
├─ 有 token
│ ├─ 访问登录页 → 重定向首页
│ ├─ 已有用户信息 → 放行
│ └─ 无用户信息 → await getInfoAction
│ ├─ 成功 → 放行
│ └─ 失败 → resetToken + 重定向登录页(带 redirect)
└─ 无 token
├─ 白名单(登录页)→ 放行
└─ 其余 → 重定向登录页(query 带 redirect=path)
两个容易被忽略的坑:
冷启动补跑 。H5 直达 URL 或 deeplink 进入时,页面可能已加载而守卫未执行。解法是启动时 router.isReady().then(() => router.guardRoute()) 显式补跑一遍守卫链,redirect 按 relaunch 执行。
导航兜底 。仅靠规约「页面必须用 useRouter()」挡不住所有场景(第三方组件内部直调 uni.navigateTo 会绕过守卫链)。运行时启用 InterceptorPlugin(interceptUniApi: true)拦截所有原生导航 API 转交守卫链------但规约仍要求统一走 useRouter(),兜底不是拿来依赖的。
另外一个 UTS 特有的细节:重定向指令必须用路由库导出的 RawLocation / NavigationRedirect 显式类型构造,禁止裸对象字面量------后者在 Kotlin 名义类型下会 cast 崩溃(详见第四节)。
2.4 类型体系:类型跟着消费关系走
判据只有一句话:类型放它被消费的地方。
| 类型性质 | 位置 |
|---|---|
| 跨层共享(api → store → 页面/守卫) | <域>/types/ + <域>/types.uts 聚合出口 |
| 多文件域的出口形状(如 Store 类型) | 同上 |
| 单文件模块私有契约 | 就近定义 |
| 第三方库类型 | import type 导入,绝不复制 |
三条强制规则:
规则一:聚合出口只做具名类型转发。
uts
// api/types.uts
export type { LoginResult, UserInfo } from './types/api.uts'
禁止 export * from,禁止经转发链导出值(函数/常量)------这两条在 Kotlin 端未实证或已证伪,是实测结论而非风格偏好。
规则二:DTO 还原逐调用点内联,禁止提取公共泛型包装。
uts
// api/user.uts
export const login = (username: string, password: string): Promise<LoginResult> => {
return new Promise((resolve, reject) => {
request<UTSJSONObject>(/* ... */).then((res) => {
// 逐调用点内联还原,禁止包装成 parse<T> 公共函数
const dto = JSON.parse<LoginResult>(JSON.stringify(res.data))
if (dto === null) {
reject(new Error('登录响应解析失败'))
return
}
resolve(dto)
}).catch((err) => {
reject(err)
})
})
}
原因:JSON.parse<T> 的泛型二次传递在 Kotlin 端触发泛型擦除,公共包装函数在 Web 端跑得好好的,上 Android 就是运行时炸点。这是整个体系里最反直觉、也最值得记住的一条。
规则三:禁用 TypeScript 工具类型。 Partial / Record / Pick 一律不存在,需要就手写等效 type。UTS 不是「更严的 TS」,是另一门语言。
2.5 反馈层:toast / modal 全端自绘
uni 原生 showToast 的 position / icon 在各端表现差异很大。方案是统一收敛到 ux-utils 的自绘通道(App 端 UTS 直挂系统窗口 / Web DOM 自绘 / 小程序原生降级),调用侧一套 API 全端一致;退出登录这类破坏性操作统一走 showModalAsync 确认。
三、UTS 踩坑红线清单(不踩会崩)
以下每一条都对应一次真实的编译错误或运行时崩溃,按「坑 → 正确姿势」整理:
| # | 坑 | 正确姿势 |
|---|---|---|
| 1 | 不支持 undefined(UTS110111119) |
一切可空用 `null |
| 2 | 条件语句禁 truthy/falsy(UTS110111120) | if (x > 0)、while (isValid),禁止 if (arr) |
| 3 | 对象字面量只能赋给 type 定义的类型(UTS110111163) |
禁 interface 承接字面量;先 type 后赋值 |
| 4 | 泛型二次传递触发 Kotlin 泛型擦除 | JSON.parse<T> 只能逐调用点以具体类型内联 |
| 5 | export * from / 值转发链 Kotlin 端不可靠 |
聚合出口一律 export type { X } from 具名转发 |
| 6 | type 的属性方法不能带泛型(uts110111161) | 回调签名具体化,或泛型提升到 type 级别 |
| 7 | enum 不用于跨端传递 | 字面量联合类型替代 |
| 8 | 函数声明(function)不能作值传递(UTS110111152) | 回调一律箭头函数 |
| 9 | Kotlin 端数组越界抛异常(JS 端返回 undefined) | 访问前判界:index >= 0 && index < arr.length |
| 10 | Kotlin 名义类型下裸对象 cast 崩溃 | 跨库对象一律用导出的显式类型构造(如 RawLocation) |
这张表值得打印出来贴在显示器旁边------TS 越熟练,越容易在 1、2、4 三条上栽跟头。
四、验证现状与已知问题(截至 v1.0.0)
Web 端已全量验证:冷启动守卫重定向、登录闭环(redirect 回跳)、带 token 冷启动、登出确认弹窗(取消/确定双路径)、DTO 解析链路、easycom 组件渲染、统一错误 toast;mock 服务端日志确认拦截器注入与请求协议正确。
Android(Kotlin)同构代码待真机回归 ,但已知风险点已在代码层面全部规避------上面的红线清单就是规避清单本身:泛型二次传递(DTO 内联还原)、export { x } from 值转发(仅用 export type)、UTSJSONObject cast(重定向用导出类型构造)、app.use(pinia)(免挂载)。
两个已记录的 Web 端已知行为(非错误):
redirectquery 存在双重编码(%252F)现象,round-trip 正常;- relaunch 后控制台打印
NavigationFailure确认日志,跳转结果正确。
五、如何使用这套模板
bash
git clone https://github.com/MengXi-Studio/unix-architecture.git
- 改一处配置:
config/index.uts的baseURL指向你的后端; - 本地联调可用任意 mock(协议:
POST /user/login → {token}、GET /user/info → {name, avatar}、POST /user/logout,CORS 放行Authorization); - 新增业务功能按固定顺序落码:DTO 类型 → api 薄封装 → store 模块 → 页面,四个环节缺一不可;新页面默认要求登录,免登录页需在守卫白名单登记;
- 编译运行(HBuilderX CLI):
D:\HBuilderX\cli.exe launch web --project <项目绝对路径>。
新增页面的三个动作:建 .uvue 文件 + pages.json 注册 + 页内 defineUniPage({ name, title }) 编译宏------路由表由 vite 插件自动生成,人手不碰。
六、一点延伸:把架构约束交给 AI 执行
这个模板还内置了一套 AI 协作体系(.trae/ 目录,随仓库公开):rules/ 四件套把分层红线、插件清单、落码顺序写成机器可执行的强制规则,skills/ 提供「新功能六步落码」的标准流程,任务方案与实施记录全部落盘归档。
实际体验下来,最大的收益不是「写得快」,而是红线不漂移------AI 生成代码时被规则约束在分层内,UTS 红线清单直接注入上下文,上面第四节那些坑在生成阶段就被拦住了。人机协作的工程化,也值得像代码一样被认真设计。
写在最后
- 仓库:github.com/MengXi-Stud... (v1.0.0)
- 技术栈:uni-app x(HBuilderX 5.26,vapor)· UTS · Vue3 组合式 API
- 底座插件系:ux-router / ux-pinia / mx-request / ux-storage / ux-utils
后续计划:Android(Kotlin)真机回归后,会再出一篇「Kotlin 端实测报告」------红线清单里每条规避项的实战验证结果。如果这篇文章帮你省下了一些踩坑时间,欢迎去仓库点个 Star。