Architecture
Directory Structure
├── main.uts # The only assembly point: app.use(router) + initRequest() + setupPermission()
├── App.uvue # App lifecycle (e.g. Android back-button interception)
├── pages.json / manifest.json
│
├── api/ # API layer: thin wrappers (request forwarding + path consolidation + DTO restoration), no business logic
│ ├── user.uts
│ └── types.uts # Single source of truth for DTOs (LoginResult / UserInfo, single file doubles as the aggregate export)
│
├── stores/ # State layer: ux-pinia class-style stores, mount-free (module-level registry as global singleton)
│ ├── index.uts # useXxxStore exports
│ ├── types.uts # Domain type aggregate export (forwards store class types)
│ └── modules/
│ └── user.uts # UserStore class: state(reactive) + login/getInfo/logout/resetToken
│
├── composables/ # Composables: functions with no aggregate export (import directly as needed), types go through the aggregate export
│ ├── useLoading.uts
│ └── types.uts # LoadingState definition (domain types in a single file)
│
├── components/ # easycom convention directory (components/<Name>/<Name>.uvue, usable without registration)
│ └── AppLogo/AppLogo.uvue
│
├── router/ # Routing layer
│ ├── index.uts # createRouter (routes generated by the vite plugin)
│ ├── permission.uts # Global guard: whitelist + token check + getInfo + redirects
│ └── routes.gen.uts # ⚠ Auto-generated by @meng-xi/unix-router/vite-plugin, do not edit
│
├── utils/ # Utility layer
│ ├── request.uts # mx-request wrapper: configureRequest + dual interceptors
│ └── auth.uts # Token read/write: in-memory cache (sync reads) + ux-storage persistence (async writes)
│
├── config/
│ └── index.uts # Single source of global config (appName / baseURL / timeout)
│
├── pages/ # Pages (uni-app x convention, statically registered in pages.json)
│ ├── index/index.uvue
│ └── login/login.uvue
│
├── types/ # Plugin-generated d.ts (defineUniPage / RouteNameMap), never hold business types here
├── .trae/ # AI-assisted development system (rules / skills / documents / mcp.json)
├── docs/ # This documentation site (VitePress)
├── static/ uni.scss
└── uni_modules/ # Plugin suite + m-ui-x component library (zero-registration easycom)Core Call Chain
Page (.uvue)
→ composables/useXxx (cross-cutting UI state)
→ stores/modules/* (business state and actions)
→ api/<domain> (DTO restoration)
→ utils/request (interceptors: token injection + unified error toast)
→ mx-request → uni.requestThree hard rules apply throughout:
- Pages never touch the network layer directly and never see raw JSON; stores never reference components;
- The api layer only does thin wrapping: request forwarding + path consolidation + DTO restoration; business logic is forbidden;
- One assembly point: global initialization happens only in
main.uts; side effects at module top level are forbidden.
Startup Assembly (main.uts, the only assembly point)
app.use(router) // ux-router: provide + global mixin (onShow syncRoute)
initRequest() // mx-request: default instance config + dual interceptors (idempotent)
setupPermission() // registers beforeEach + cold-start guardRoute() re-run of the guard chainThree assembly paradigms coexist not out of inconsistency but because the platform demands it: ux-router implements the Vue install protocol (reliable on Kotlin); ux-pinia is mount-free (app.use on native is verified not to work, so the module-level registry acts as the global singleton); mx-request is a pure function library (explicit initialization avoids timing issues with module top-level side effects).
Auth Guard (router/permission.uts)
await ensureAuthReady() // async guard (the ux-router guard chain supports await Promise)
├─ Has token
│ ├─ Visiting login page → redirect home
│ ├─ User info present → allow
│ └─ No user info → await getInfoAction
│ ├─ Success → allow
│ └─ Failure → resetToken + redirect to login (with redirect)
└─ No token
├─ Whitelisted (login page) → allow
└─ Otherwise → redirect to login (query carries redirect=path)Three layers of protection:
- Whitelist + token check: conventional guard semantics;
- Cold-start re-run: with H5 deep links / direct entry, the page may have loaded without the guard running — at startup
router.isReady().then(() => router.guardRoute(null, null))explicitly re-runs the chain; - Navigation interception fallback: the runtime enables
InterceptorPlugin(interceptUniApi: true) to intercept raw native navigation such asuni.navigateToand hand it to the guard chain — but the convention still requires going throughuseRouter(), never relying on the fallback.
backFallback is configured to the home page: when the page stack is too shallow after deep links / refresh and back() would fail, it automatically relaunches home (through the full guard chain) instead of failing with CANCELLED.
Assembly Points (unique; global side effects nowhere else)
| Need | Only Location |
|---|---|
| Network init / interceptors | utils/request.uts → initRequest() (idempotent guard) |
| Route guards | router/permission.uts → setupPermission() |
| Global config | config/index.uts |
| App lifecycle | App.uvue (lifecycle logic only) |
main.uts stays minimal: app.use(router) + initRequest() + setupPermission(), with no additional assembly calls.
