1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-09-12 16:40:40 +08:00

refactor: 架构升级

This commit is contained in:
小莫唐尼
2026-08-31 07:58:23 +08:00
commit ba5b77568b
693 changed files with 118430 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
# unibest · Hermes 规范
> Hermes = 项目的信使:把工程规范传递给每一位开发者(和 AI)。
基于 unibest 4.4.1(uni-app + Vue3 + TS + Vite5 + UnoCSS)的实际配置提炼。
## 三条铁律
1. **生成物不手改**:`src/pages.json``src/manifest.json``src/types/*.d.ts` 全部由 `*.config.ts` 生成,手改会在下次构建被覆盖
2. **平台差异只用条件编译**:`#ifdef H5 / #ifdef MP-WEIXIN / #ifdef APP-PLUS`,编译期能确定的不留到运行时
3. **UI 优先原子类**:先 UnoCSS 原子类,再考虑自定义 CSS
## 文档索引
| 文件 | 内容 |
|------|------|
| [architecture.md](./architecture.md) | 架构规范:事实源与生成物、平台接缝、校验边界、目录分层 |
| [conventions.md](./conventions.md) | 代码规范:命名、SFC 结构、TS、状态、提交与验证命令 |
| [platforms.md](./platforms.md) | 平台适配手册:差异决策树、条件编译速查、本项目差异点表、多端地址 |
| [api.md](./api.md) | 请求层规范:分层、httpGet/Post 用法、错误四分类、401 双 token 策略 |
| [sop-new-page.md](./sop-new-page.md) | 新页面/组件/分包/tabbar/hooks SOP 与验证清单 |
| [performance.md](./performance.md) | 性能与分包:主包体积、内置优化表、包体积检查、编码侧规则 |
| [release.md](./release.md) | 发布流程:upload:mp 全流程、changesets、uvm 升级、环境切换、合入门禁 |
## 常用命令
```bash
pnpm dev # H5 开发
pnpm dev:mp # 微信小程序开发
pnpm dev:app # APP 开发
pnpm build:mp # 微信小程序生产构建
pnpm type-check # vue-tsc 类型检查(合入前必过)
pnpm lint:fix # ESLint 修复
pnpm test:run # vitest 单次运行
```
+67
View File
@@ -0,0 +1,67 @@
# 请求层规范
> 事实源:`src/http/http.ts`(请求封装)+ `src/http/interceptor.ts`(拦截器)。本文规则均来自这两个文件的实际行为。
## 1. 分层
```
页面/组件
↓ 只调用
src/api/* # 接口定义 + 类型声明
↓ 只调用
src/http/http.ts # httpGet/httpPost/httpPut/httpDelete
↓ 自动经过
src/http/interceptor.ts # URL 拼接、token 注入、超时
uni.request / uni.uploadFile
```
业务代码**禁止**直接调 `uni.request`;新接口一律在 `src/api/` 按模块建文件并声明入参/出参类型。
## 2. 调用方式
```ts
import { httpGet, httpPost } from '@/http'
// GET(query 自动序列化拼接)
const data = await httpGet<IUser>('/user/info', { id: 1 })
// POST:第二参是 body,第三参是 query(微信系接口常用,勿省)
await httpPost<ILoginRes>('/login', { username, password }, { platform: 'wx' })
```
- `http<T>` 直接 resolve 业务 `data`(响应契约 `{ code, data, ... }` 已在封装内拆包)
- 超时 60s、`Authorization: Bearer <token>` 均由拦截器注入,**业务不手写请求头 token**
## 3. 错误处理(类型归一)
catch 到的永远是 `HttpError`,`type` 四选一,按需分支:
| type | 触发 | 默认行为 |
|------|------|---------|
| `Auth` | HTTP 401 或业务码 401 | 封装内已处理(见下),业务只需 catch |
| `Business` | HTTP 2xx 但业务码失败 | 自动 toast 错误消息 |
| `Http` | 非 2xx 状态码 | 自动 toast |
| `Network` | 请求 fail | 自动 toast「网络错误」 |
静默场景(自己处理提示)传 `hideErrorToast`:
```ts
await httpPost('/log/track', payload, undefined, undefined, { hideErrorToast: true })
```
## 4. 401 / token(业务零处理)
`http.ts` 统一处理,受 `env/.env``VITE_AUTH_MODE` 控制:
- `single`:清用户态 → 跳登录页
- `double`:用 refreshToken 无感刷新(并发请求进队列,刷新成功后自动重放;失败才登出)
**业务代码不要自行处理 401,也不要读 store 手动拼 Authorization。**
## 5. 地址与环境
- 基准地址 `VITE_SERVER_BASEURL`;第二后端用 `VITE_SERVER_BASEURL_SECONDARY`
- H5 dev 开代理时(`VITE_APP_PROXY_ENABLE=true`)自动走 `/fg-api` 前缀,nginx 需同步该前缀
- 微信三环境(开发/体验/正式)用 `VITE_SERVER_BASEURL__WEIXIN_*` 覆写,见 platforms.md 第 4 节
- 对接多个后端:在 `interceptor.ts` 的 URL 拼接处扩展,不在业务层散落拼 URL
+64
View File
@@ -0,0 +1,64 @@
# 架构规范
## 1. 事实源与生成物(唯一事实源与投影)
配置的**事实源**是根目录的 `*.config.ts`,构建时投影为 JSON/d.ts。改投影不改源 = 改动丢失。
| 事实源(手改这里) | 生成物(禁止手改) | 生成者 |
|------------------|------------------|--------|
| `pages.config.ts` | `src/pages.json` | vite-plugin-uni-pages |
| `manifest.config.ts` | `src/manifest.json` | vite-plugin-uni-manifest |
| `src/tabbar/config.ts` | pages.json 的 tabBar 段 | 引入 pages.config.ts |
| 组件目录 `src/components/**` | `src/types/components.d.ts` | vite-plugin-uni-components |
| `src/pages/**` + `definePage` 宏 | `src/types/uni-pages.d.ts` | vite-plugin-uni-pages |
| `src/hooks/**`、vue/uni-app API | `src/types/auto-import.d.ts` | unplugin-auto-import |
页面路由配置只写在页面内的 `definePage` 宏里,不写进 pages.config.ts 的页面列表。
## 2. 平台接缝
- **条件编译是唯一的平台分支手段**(编译期能确定的场景):JS 用 `// #ifdef` / `// #endif`,模板用 `<!-- #ifdef -->` / `<!-- #endif -->`
- 运行时判断平台用 `uni.getSystemInfoSync().uniPlatform` 等 API,只用于编译期无法确定的场景
- 仅 H5 生效的行为(如 devServer 代理 `VITE_APP_PROXY_*`、eruda 调试面板)不需要写条件编译,其他端构建时天然剔除
- 小程序端 dev 也是 `build` command(无 devServer),不要在非 H5 端假设热更新代理可用
## 3. 校验只在真边界
以下边界的数据**不可信**,必须校验后才进业务层:
| 真边界 | 校验位置 |
|--------|---------|
| HTTP 响应 | `src/http` / alova 拦截器统一做,业务层不重复判空 |
| `uni.getStorageSync` 读出的数据 | 读取处判类型/结构 |
| URL 参数(onLoad options) | 页面入口处校验 |
| postMessage / 第三方 SDK 回调 | 回调入口处校验 |
组件 props、store 内部传递信任 TS 类型,**不**重复运行时校验。
## 4. 目录分层与职责
```
src/
├── pages/ # 页面(约定式路由,文件即路径;分包页面不放这里)
├── pages-demo/ # 分包目录(subPackages 配置,分包不能是 src/pages 的子目录)
├── components/ # 全局组件(fg- 前缀,easycom 自动注册)
├── layouts/ # 布局
├── api/ # API 接口定义
├── http/ # 请求封装(uni.request 为主;alova 为可选并行链路,拦截器中默认注释)
├── store/ # Pinia store(持久化用 pinia-plugin-persistedstate)
├── hooks/ # 组合式函数(auto-import,免 import)
├── tabbar/ # 底部导航(config.ts 为唯一配置源)
├── static/ # 静态资源(@img 别名指向 images 子目录)
├── service/ # openapi 生成目录(`pnpm openapi`,eslint 忽略,勿手改)
└── types/ # 类型声明(大部分为生成物,见第 1 节)
```
- 别名:`@``src/`;`@img``src/static/images`(vite 与 tsconfig 已对齐)
- 请求一律走 `src/http` 封装,业务代码不直接调 `uni.request`
- 服务端状态以接口为事实源,store 只放需要跨页共享/持久化的状态;凡不能从 store 重建的 UI 状态都是刷新后丢失的隐患
## 5. 环境变量
- env 文件统一在 `env/` 目录(vite.config.ts 的 `envDir`),不在项目根
- 变更部署相关变量(端口 `VITE_APP_PORT`、接口地址 `VITE_SERVER_BASEURL`、代理开关 `VITE_APP_PROXY_*` 等)改 env 文件,不在代码里硬编码
- `VITE_DELETE_CONSOLE=true` 时构建移除 console/debugger,提交前不需要手工删
+47
View File
@@ -0,0 +1,47 @@
# 代码规范
## 1. Vue SFC
- `<script setup lang="ts">` 必须第一个,`<template>` 第二,`<style scoped>` 最后(eslint `vue/block-order` error 强制)
- 页面配置用 `definePage` 宏,且放在最上面
- 组件文件 PascalCase 命名;全局组件放 `src/components/`(fg- 前缀,easycom 自动注册),局部组件放页面的 `components/` 子目录
- 列表页用 z-paging(已配置 easycom:`<z-paging>` 直接用)
- 组合式 API + auto-import:vue/uni-app 的 API 和 `src/hooks/**` 不需要手写 import
## 2. TypeScript
- 禁止新增 `any`(团队约定;eslint 未单独强制)
- 对象类型用 `interface`,联合类型用 `type`;导入类型用 `import type`
- 不自动排序 import:条件编译注释可能包裹 import(eslint 已关 `perfectionist/sort-imports`,勿重新打开)
- API 响应必须在 `src/api/` 定义接口类型,响应数据过边界校验后才使用
## 3. 样式(UnoCSS)
- 优先原子类,减少自定义 CSS;常用快捷方式:`center`(flex 居中)
- 主题色用 `text-primary` / `bg-primary`(uno.config.ts theme 定义,含 wot-ui 联动变量)
- 安全区用自定义规则:`p-safe` / `pt-safe` / `pb-safe`(刘海屏/底部横条)
- 小字号:`text-2xs`(20rpx)/ `text-3xs`(18rpx)
- **动态拼接的图标类名必须加入 uno.config.ts 的 safelist**,否则不生成样式
- 本地 SVG 放 `src/static/my-icons/`,用 `i-my-icons-图标名` 调用
## 4. 状态管理
- store 放 `src/store/`,`defineStore` 定义
- 需要持久化的状态配置 `pinia-plugin-persistedstate`,不手写 uni.setStorage 同步业务状态
- uni.getStorageSync 读出的数据是真边界数据,使用前校验(见 architecture.md 第 3 节)
## 5. Git 提交
- commitlint 强制 conventional commits:`feat: / fix: / docs: / style: / refactor: / perf: / test: / chore:`
- 版本发布走 changesets:`pnpm upload:changeset`
- husky 钩子已启用,lint-staged 会拦截不合规提交
## 6. 合入前验证(三条命令)
```bash
pnpm type-check # vue-tsc --noEmit,类型零错误
pnpm lint # eslint 零 error
pnpm test:run # vitest 全绿
```
微信小程序上传:`pnpm upload:mp`(需 miniprogram-ci 配置)。
+34
View File
@@ -0,0 +1,34 @@
# 性能与分包规范
## 1. 分包(微信主包 2M 硬限制)
- 分包目录放 `src/pages/` 之外,注册到 `vite.config.ts``UniPages({ subPackages })`
- 低频/独立业务(如 demo、设置二级页)优先进分包
- bundle-optimizer 已启用(仅微信端):支持模块异步跨包调用、组件异步跨包引用,分包间复用代码不需要复制
- 判断该不该分包:页面是否首屏必需?不是 → 分包
## 2. 构建内置优化(不要重复做)
| 优化 | 状态 | 说明 |
|------|------|------|
| console/debugger 移除 | `VITE_DELETE_CONSOLE=true` | 提交前不用手工删 console |
| sourcemap | 关闭 | 需要时改 vite.config.ts,勿长期开 |
| minify | esbuild | dev 不压缩、prod 压缩 |
| build target | es6 | 兼容低版本 WebView |
## 3. 包体积日常检查
```bash
pnpm build:h5 # H5 生产构建自动打开 visualizer 分析
# 产物:node_modules/.cache/visualizer/stats.html
```
小程序端看微信开发者工具的「代码依赖分析」。
## 4. 编码侧规则
- 静态图片:大图优先网络地址或压缩后放 `src/static`;SVG 图标走 `src/static/my-icons/`(i-my-icons-*)
- UnoCSS `safelist` 只加**动态拼接类名**,每加一项全量注入,能不用则不用
- 列表页一律 z-paging(虚拟分页),不手写 onReachBottom 加载
- auto-import(`src/hooks`)按名导入即用;注意 eslint 已关闭 no-unused-vars,未使用导入无守卫,删改代码时自查
- 新增第三方依赖前看体积:小程序端无 tree-shake 保障的库(如全量 UI 库)优先按需引入
+67
View File
@@ -0,0 +1,67 @@
# 平台适配手册
## 1. 差异处理决策树
```
这个差异编译期就能确定吗?
├── 能 → 条件编译(#ifdef / #ifndef)
└── 不能 → 运行时 API 判断(uni.getSystemInfoSync().uniPlatform)
```
- 构建脚本/插件层面:`process.env.UNI_PLATFORM`(仅 vite.config.ts、构建脚本内可用)
- 禁止:用 UA 嗅探替代条件编译;在能编译期剔除的代码里留运行时 if
## 2. 条件编译速查
```vue
<script setup lang="ts">
// #ifdef H5
import { h5Api } from '@/utils/h5'
// #endif
// #ifndef MP-WEIXIN
// 所有平台生效,除微信小程序
// #endif
</script>
<template>
<!-- #ifdef APP-PLUS -->
<view> APP</view>
<!-- #endif -->
</template>
```
常用平台标识:`H5``MP-WEIXIN``MP-ALIPAY``MP-BAIDU``MP-TOUTIAO``MP-LARK``MP-QQ``MP-KUAISHOU``MP-JD``MP-XHS``APP-PLUS`(app-android / app-ios 可再细分)、`MP`(`%MP%``MP` 泛小程序)。
## 3. 本项目已存在的平台差异(改动时别破坏)
| 差异点 | 位置 | 规则 |
|--------|------|------|
| devServer 代理(`VITE_APP_PROXY_*`) | src/http/interceptor.ts | 仅 H5 dev 生效;非 H5 端 dev 也是 build command,无代理 |
| `responseType: 'json'` | src/http/http.ts | 用 `#ifndef MP-WEIXIN` 包裹,微信小程序不支持 |
| eruda 调试面板 | vite.config.ts | 仅 H5 development |
| 打包分析 visualizer | vite.config.ts | 仅 H5 production,产物在 `node_modules/.cache/visualizer/stats.html` |
| 自动打开开发者工具 | scripts/open-dev-tools.js | mp-weixin / mp-alipay / mp-lark 构建后自动打开对应工具;上传脚本用 `SKIP_OPEN_DEVTOOLS=true` 跳过 |
| bundle-optimizer 分包优化 | vite.config.ts | `enable: isMpWeixin`,仅微信小程序端 |
| 原生插件资源复制 | vite.config.ts | 仅 app 平台且 `VITE_COPY_NATIVE_RES_ENABLE=true` |
## 4. 多端请求地址
`env/.env` 支持按微信开发者工具 envVersion 覆写(不配则回退 `VITE_SERVER_BASEURL`):
```ini
VITE_SERVER_BASEURL = 'https://api.example.com'
VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://dev.xxx.com' # 开发版
VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://trial.xxx.com' # 体验版
VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://prod.xxx.com' # 正式版
```
## 5. 平台命令
```bash
pnpm dev # H5
pnpm dev:mp # 微信小程序(其余 dev:mp-alipay / mp-baidu / mp-jd / mp-kuaishou / mp-lark / mp-qq / mp-toutiao / mp-xhs 同理)
pnpm dev:app # APP(app-android / app-ios 可细分)
pnpm build:mp # 微信小程序生产构建
```
新增平台特定行为时,同步更新本文第 3 节的差异点表。
+49
View File
@@ -0,0 +1,49 @@
# 发布流程
## 1. 微信小程序上传(`pnpm upload:mp`)
脚本 `scripts/upload-weixin.js` 全自动:构建(`build:mp:prod`,跳过打开开发者工具)→ miniprogram-ci 上传。
```bash
pnpm upload:mp # 版本取 package.json,描述取最新 Git commit
pnpm upload:mp --version=1.0.1 # 指定版本号
pnpm upload:mp --desc="修复登录bug" # 指定描述
pnpm upload:mp --robot=2 # 指定机器人(1-30,多人协作避免互相覆盖)
```
版本号优先级:命令行 > package.json;描述优先级:命令行 > Git commit > 时间戳。
**前置条件(缺一上传失败)**:
1. 公众平台已开通「小程序代码上传」权限并配置 IP 白名单
2. 根目录有私钥文件 `private.<appid>.key`(appid 与 `env/.env``VITE_WX_APPID` 一致)
3. 上传后到公众平台「版本管理 → 开发版本」设为体验版
## 2. 版本管理(changesets)
```bash
pnpm upload:changeset # 生成变更记录 + 消费变更(升版本)
```
- 对外可见的变更(新页面、接口变动、依赖升级)提交 changeset
- `pnpm bump-version` 单独升 package.json 版本(上传脚本读它)
## 3. uni-app 依赖升级
```bash
pnpm uvm # 交互式升级 @dcloudio/* 全家桶
pnpm uvm-rm # 清理升级残留
```
升级后必跑:`pnpm type-check && pnpm test:run`,并三端冒烟(`dev` / `dev:mp` / `dev:app`)。
## 4. 环境切换
env 文件在 `env/` 目录,按 mode 叠加:`.env`(公共)→ `.env.development` / `.env.test` / `.env.production`。发布前核对生产 env:接口地址、微信三环境地址(`VITE_SERVER_BASEURL__WEIXIN_*`)、`VITE_DELETE_CONSOLE=true`
## 5. 合入前门禁
```bash
pnpm type-check && pnpm lint && pnpm test:run
```
提交信息 conventional commits(commitlint 强制):`feat: / fix: / docs: / style: / refactor: / perf: / test: / chore:`
+48
View File
@@ -0,0 +1,48 @@
# 新页面 / 新组件 SOP
## 1. 新建页面(3 步)
1. **建文件**:`src/pages/模块名/页面名.vue`(约定式路由,文件路径即路由,**无需注册**)
2. **写 `definePage` 宏,放在 script 最上面**:
```vue
<script setup lang="ts">
definePage({
style: { navigationBarTitleText: '页面标题' },
})
</script>
```
3. **跑 `pnpm type-check`**:重新生成 `src/types/uni-pages.d.ts`,路由类型才可用
注意:
- 页面私有组件放同目录 `components/` 子目录(构建时已排除,**不会被识别成页面**)
- 需要登录态的页面按项目现有登录拦截方式处理,不自行在页面内写 token 判断
## 2. 新建全局组件(1 步)
`src/components/fg-组件名/fg-组件名.vue`,easycom 规则 `^fg-(.*)` 自动注册,任何页面直接 `<fg-组件名 />`,**无需 import**。
- z-paging 同理:`<z-paging>` 直接用
- 其他第三方库组件按各自 easycom 规则
## 3. 新建分包页面(2 步)
1. 分包目录放 `src/pages/` **之外**(如 `src/pages-sub/xxx`),内建页面文件
2.`vite.config.ts``UniPages({ subPackages: [...] })` 数组加目录路径
## 4. 新增 tabbar 项(1 步)
只改 `src/tabbar/config.ts`(唯一配置源),pages.json 的 tabBar 段自动生成。图标等资源同步放约定目录。
## 5. 新增组合式函数(1 步)
`src/hooks/`,auto-import 已配置 `dirs: ['src/hooks']`,页面内**直接调用,无需 import**。
## 6. 验证清单(新建任何东西后)
```bash
pnpm type-check # 路由/组件类型生成且零错误
pnpm lint # 格式合规
pnpm dev:mp # 目标平台真机/模拟器跑一遍
```