Skip to content

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.request

Three 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) ​

uts
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 chain

Three 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:

  1. Whitelist + token check: conventional guard semantics;
  2. 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;
  3. Navigation interception fallback: the runtime enables InterceptorPlugin (interceptUniApi: true) to intercept raw native navigation such as uni.navigateTo and hand it to the guard chain — but the convention still requires going through useRouter(), 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) ​

NeedOnly Location
Network init / interceptorsutils/request.uts → initRequest() (idempotent guard)
Route guardsrouter/permission.uts → setupPermission()
Global configconfig/index.uts
App lifecycleApp.uvue (lifecycle logic only)

main.uts stays minimal: app.use(router) + initRequest() + setupPermission(), with no additional assembly calls.

Released under the MIT License.