ARTICLE · 1300189
uni-app x下自研架构的工程化突围
本文是 unix-architecture(v1.1.0)的设计与实践总结。技术栈:uni-app x(VDOM 模式)· UTS 语言 · Vue3 组合式 API,底层由自研插件系(ux-router / ux-pinia / mx-request / ux-storage / ux-utils / m-ui-x)承载。
一、为什么需要「重新发明」分层
如果你写过标准的 Vue3 工程,大概率熟悉这套形态:vue-router 单路由表、pinia 挂载即用、axios 拦截器全局生效、vite 约定式路由。一切顺理成章。
但 uni-app x 不是标准 Vue3。它的页面是编译期静态模型——同一份 .uvue 代码要编译到 JavaScript(Web / 小程序)和 Kotlin(Android)、Swift(iOS),没有动态路由注册,没有全局 layout,甚至 computed 在某些插件场景下都会失效。标准 Vue3 的「单路由表、单插件协议」在这里天然不成立。
unix-architecture 想验证的问题是:在 uni-app x 的底座约束下,能不能构建一套完整自洽的工程化分层?api、store、router、utils、pages 各司其职,类型契约清晰,装配点唯一。
答案是能,但每一层都要重新想一遍。
二、分层总览:依赖单向,职责单一
整个架构是一条严格单向的调用链:
页面(.uvue)→ composables/useXxx 横切 UI 状态(loading 等)→ stores/modules/* 业务状态与动作→ api/ 请求转发 + 路径收敛 + DTO 还原→ utils/request 拦截器:token 注入 + 统一错误提示→ mx-request → uni.request 底层网络
几条铁律贯穿始终:
页面不接触裸 JSON——页面永远消费还原后的 DTO,网络细节被彻底隔离; api 层薄如纸——只做请求转发、路径收敛和 DTO 还原,禁止写业务逻辑; store 不引用组件,组件不反向依赖 store 之外的任何层; 装配点唯一—— main.uts保持极薄,只有三行:app.use(router)、initRequest()、setupPermission(),任何全局副作用都不允许在别处发生。
支撑各层的插件系由同一个工作室自研,彼此语义咬合:ux-router 提供命名路由与守卫链,ux-pinia 提供免挂载的 class 式状态管理,mx-request 提供双拦截器网络层,ux-storage 负责异步持久化,ux-utils 提供全端自绘的 toast/modal。
三、三个「标准姿势失效」的地方
架构不是照搬出来的,而是被约束逼出来的。以下是三个最典型的「标准 Vue3 直觉在这里不工作」的例子。
1. Pinia 的三种装配范式并存
标准 pinia 是 app.use(pinia) 一行搞定。但在 uni-app x 的原生端,实测 app.use(pinia) 不可靠。于是本架构中三种装配范式并存:
ux-router:实现了 Vue install 协议,走 app.use(router);ux-pinia:免挂载设计,模块级注册表即全局单例,无需注册; mx-request:纯函数库,全局默认实例,显式调用 initRequest()。
「三种范式并存」听起来不够统一,但它是底座约束下的诚实设计——强求形式统一只会埋进更深的坑。
2. token 的同步读写体验
js-cookie 是同步的,但 uni-app x 的 storage API 是异步的。鉴权守卫却是「同步心智」:beforeEach 里读 token、判断、重定向,中间塞一个 await 就要全链路重想。
解法是双层缓存:token 读写永远走模块级内存缓存(同步体验),后台由 ux-storage 异步落盘;冷启动时守卫首行 await ensureAuthReady()(Promise 复用,只读一次持久层)先把内存缓存恢复好。于是:
登录 → setToken(内存即时生效 + 异步落盘)请求 → 拦截器注入 Authorization: Bearer冷启动 → ensureAuthReady() 先行恢复内存缓存,守卫无竞态
3. 静态模型下的路由表与守卫
uni-app x 没有动态路由。本项目用 vite 插件从 pages/ 目录编译期生成routes.gen.uts(禁止手改),配合页面内 defineUniPage({ name, title }) 宏声明元数据——新增页面 = 建文件 + 注册 + 声明宏,路由表自动维护。
守卫链则补齐了 vue-router 的核心体验:白名单 + token 检查 + getInfo + 重定向,冷启动直达页面(H5 deeplink)时 router.isReady().then(() => router.guardRoute()) 补跑守卫链。
四、UTS 实战:那些只有跑起来才知道的坑
UTS 是名义类型系统,编译到 Kotlin 后很多 JS 直觉会翻车。以下是几条用真机打包失败换来的红线:
DTO 还原必须逐调用点内联 JSON.parse(JSON.stringify(res.data))。提取一个「公共泛型包装函数」看似优雅,但二次泛型传递在 Kotlin 端触发泛型擦除,直接编译失败。架构宁可接受重复代码,也换取编译确定性。类型转发链有布局约束:app-android 的发行拷贝不追踪 export type ... from——仅被类型转发引用的子目录文件根本不进发行树,分析器直接报index not found。因此域类型收敛在<域>/types.uts单文件内定义,禁止types/子目录转发链。没有 undefined,一切可空用null | T;条件语句必须是显式布尔表达式,truthy/falsy 写法直接报错。重定向对象禁止裸字面量:守卫重定向必须用导出的 RawLocation类型构造——UTSJSONObject在 Kotlin 名义类型下 cast 会崩溃。守卫内禁 computed字段(插件内 effectScope 跨页面失效),派生值一律用 class getter。
这些约束最终全部沉淀进了项目的架构规则文档,成为 AI 辅助开发的强制约束层——违反即视为错误实现,无论写法在 TS 里多么「惯用」。
五、类型跟着消费关系走
类型组织只有一条判据:类型跟着消费关系走。
<域>/types.uts | |
import type |
这套规则消灭了两类常见顽疾:类型定义散落各处导致「改一个字段全局搜」,以及复制第三方类型导致「库升级后类型漂移」。
六、把「AI 怎么写代码」也当成架构的一部分
这是本项目最有意思的一个设计:仓库里有一个 .trae/ 目录,把「AI 助手如何在此架构下工作」沉淀为可执行的规则与技能,与代码同库演进。
architecture.md:机器强制约束层——分层调用链、插件导入路径唯一事实源、UTS 红线、标准落码顺序(类型 → api → store → 页面,缺一不可); core-protocol.md:协作协议——辩证执行、DoD 以运行时表现为准(编译通过不算完成,日志验证才算)、熔断机制; workflow.md:文档驱动工作流——需求分析、伪代码设计、疑问清单全部落盘跨会话持久,对话只做通知与执行; skills/:新功能开发按固定流程落码的技能封装。
原理层文档回答「为什么这样设计」,规则层回答「AI 必须怎么写」,二者同源同步演进。实践下来,这让 AI 生成的代码从「看起来对」变成「架构上对」——新功能天然落在正确的层,类型天然进正确的文件。
七、写在最后
unix-architecture 目前已发布 v1.1.0,Web 端全链路验证通过(冷启动守卫、登录闭环、DTO 链路),Android 已知风险点均已规避并经云打包验证。
它的价值不在代码量——模板本身很小——而在于把 uni-app x 这类「非标准运行时」上的工程化经验显式化了:每一处与标准 Vue3 的偏离,都对应一条被实测验证过的约束和一条明确的规则。
如果你也在多端编译框架上做工程化,希望这份踩坑记录能帮你少烧几次打包机。
仓库: 文档站: Releases:















































































