🏗️ UniApp + Vue3 + TypeScript 企业级项目结构最佳实践
💡 摘要: 本文详细介绍 UniApp + Vue3 + TypeScript 企业级项目结构的完整设计方案,涵盖目录规划、核心配置、请求封装、状态管理、类型定义等实战内容。通过若依管理系统实战案例,展示如何搭建高可维护、高性能的移动应用架构。提供完整的配置文件和工具函数代码,帮助你快速构建标准化、工程化的跨平台应用。
关键词: UniApp、Vue3、TypeScript、项目结构、前端架构、跨平台开发、飞鱼管理系统、Pinia、Vite、移动应用开发
🎯 一、背景与痛点
1.1 为什么要规范项目结构?
在实际开发中,我们经常遇到这些问题:
场景一:新人入职第一周
小王:这个文件在哪?
老员工:好像在 src 目录下…
小王:这个组件能复用吗?
老员工:应该可以,找找看有没有类似的
小王:这个接口请求怎么到处都是?
老员工:额…我们好像没有统一封装
结果:一周过去了,还在找文件…
场景二:项目维护半年后
代码越来越乱:
– API 调用分散在各个页面
– 工具函数到处复制粘贴
– 组件命名混乱
– 类型定义缺失
– 配置文件散落各处
想重构?牵一发而动全身…
场景三:多端适配时
小程序一套代码
H5 一套代码
App 又是一套代码
改个功能,要改三个地方…
1.2 好的项目结构带来的价值
#mermaid-svg-xnhuLd1TqFLP7FwS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xnhuLd1TqFLP7FwS .error-icon{fill:#552222;}#mermaid-svg-xnhuLd1TqFLP7FwS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xnhuLd1TqFLP7FwS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xnhuLd1TqFLP7FwS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xnhuLd1TqFLP7FwS .marker.cross{stroke:#333333;}#mermaid-svg-xnhuLd1TqFLP7FwS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xnhuLd1TqFLP7FwS p{margin:0;}#mermaid-svg-xnhuLd1TqFLP7FwS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster-label text{fill:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster-label span{color:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster-label span p{background-color:transparent;}#mermaid-svg-xnhuLd1TqFLP7FwS .label text,#mermaid-svg-xnhuLd1TqFLP7FwS span{fill:#333;color:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS .node rect,#mermaid-svg-xnhuLd1TqFLP7FwS .node circle,#mermaid-svg-xnhuLd1TqFLP7FwS .node ellipse,#mermaid-svg-xnhuLd1TqFLP7FwS .node polygon,#mermaid-svg-xnhuLd1TqFLP7FwS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xnhuLd1TqFLP7FwS .rough-node .label text,#mermaid-svg-xnhuLd1TqFLP7FwS .node .label text,#mermaid-svg-xnhuLd1TqFLP7FwS .image-shape .label,#mermaid-svg-xnhuLd1TqFLP7FwS .icon-shape .label{text-anchor:middle;}#mermaid-svg-xnhuLd1TqFLP7FwS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xnhuLd1TqFLP7FwS .rough-node .label,#mermaid-svg-xnhuLd1TqFLP7FwS .node .label,#mermaid-svg-xnhuLd1TqFLP7FwS .image-shape .label,#mermaid-svg-xnhuLd1TqFLP7FwS .icon-shape .label{text-align:center;}#mermaid-svg-xnhuLd1TqFLP7FwS .node.clickable{cursor:pointer;}#mermaid-svg-xnhuLd1TqFLP7FwS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xnhuLd1TqFLP7FwS .arrowheadPath{fill:#333333;}#mermaid-svg-xnhuLd1TqFLP7FwS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xnhuLd1TqFLP7FwS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xnhuLd1TqFLP7FwS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xnhuLd1TqFLP7FwS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xnhuLd1TqFLP7FwS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xnhuLd1TqFLP7FwS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster text{fill:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS .cluster span{color:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xnhuLd1TqFLP7FwS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xnhuLd1TqFLP7FwS rect.text{fill:none;stroke-width:0;}#mermaid-svg-xnhuLd1TqFLP7FwS .icon-shape,#mermaid-svg-xnhuLd1TqFLP7FwS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xnhuLd1TqFLP7FwS .icon-shape p,#mermaid-svg-xnhuLd1TqFLP7FwS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xnhuLd1TqFLP7FwS .icon-shape rect,#mermaid-svg-xnhuLd1TqFLP7FwS .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xnhuLd1TqFLP7FwS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xnhuLd1TqFLP7FwS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xnhuLd1TqFLP7FwS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
标准化项目结构
提高开发效率
降低维护成本
提升代码质量
便于团队协作
快速定位文件
组件可复用
清晰的模块划分
统一的代码风格
TypeScript 类型约束
ESLint 代码检查
新人快速上手
减少沟通成本
实际收益(基于飞鱼项目实战经验):
| 新人上手时间 | 2 周 | 3 天 | 83% ↓ |
| 代码复用率 | 30% | 70% | 133% ↑ |
| Bug 修复时间 | 4 小时 | 1 小时 | 75% ↓ |
| 需求交付周期 | 2 周 | 1 周 | 50% ↓ |
1.3 本文你将学到
✅ 基础篇:
– 标准化的目录结构设计
– 核心配置文件详解 (package.json/tsconfig.json/vite.config.ts)
– 环境配置最佳实践
✅ 进阶篇:
– 统一的 HTTP 请求封装
– 认证授权管理方案
– 状态管理 Pinia 实战
✅ 实战篇:
– 企业级项目完整结构
– 若依管理系统集成方案
– 多端适配技巧
✅ 避坑篇:
– 常见陷阱及解决方案
– 性能优化建议
– 团队协作规范
项目目录结构
uniapp-ruoyi-demo/
├── src/
│ ├── api/ # 接口请求
│ │ ├── auth.ts # 认证相关接口
│ │ ├── dashboard.ts # 首页数据接口
│ │ └── system/ # 系统管理接口
│ │ ├── user.ts
│ │ ├── role.ts
│ │ ├── menu.ts
│ │ └── dept.ts
│ ├── components/ # 公共组件
│ │ ├── layout/
│ │ │ ├── AppHeader.vue
│ │ │ └── AppSidebar.vue
│ │ ├── form/
│ │ │ ├── FormInput.vue
│ │ │ └── FormSelect.vue
│ │ └── business/
│ │ ├── UserAvatar.vue
│ │ └── StatusTag.vue
│ ├── pages/ # 页面文件
│ │ ├── login/
│ │ │ └── login.vue
│ │ ├── home/
│ │ │ └── home.vue
│ │ ├── system/
│ │ │ ├── user/
│ │ │ │ ├── list.vue
│ │ │ │ ├── detail.vue
│ │ │ │ └── edit.vue
│ │ │ └── role/
│ │ │ └── list.vue
│ │ └── profile/
│ │ └── profile.vue
│ ├── store/ # 状态管理
│ │ ├── modules/
│ │ │ ├── user.ts
│ │ │ ├── app.ts
│ │ │ └── permission.ts
│ │ └── index.ts
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── auth.ts # 认证工具
│ │ ├── format.ts # 格式化工具
│ │ └── validate.ts # 验证工具
│ ├── types/ # 类型定义
│ │ ├── api.ts
│ │ ├── component.ts
│ │ └── store.ts
│ ├── styles/ # 样式文件
│ │ ├── variables.scss
│ │ ├── mixins.scss
│ │ └── common.scss
│ └── static/ # 静态资源
│ ├── images/
│ └── icons/
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── tsconfig.json # TypeScript配置
├── vite.config.ts # Vite配置
├── package.json
└── README.md
核心配置文件
package.json
{
"name": "uniapp-ruoyi-demo",
"version": "1.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:h5": "uni",
"dev:mp-weixin": "uni -p mp-weixin",
"build:app": "uni build -p app",
"build:h5": "uni build",
"build:mp-weixin": "uni build -p mp-weixin",
"type-check": "vue-tsc –noEmit",
"lint": "eslint . –ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts –fix –ignore-path .gitignore"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-3000020220729001",
"@dcloudio/uni-app-plus": "3.0.0-alpha-3000020220729001",
"@dcloudio/uni-h5": "3.0.0-alpha-3000020220729001",
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-3000020220729001",
"pinia": "^2.0.22",
"vue": "^3.2.37"
},
"devDependencies": {
"@dcloudio/types": "^3.0.15",
"@dcloudio/uni-automator": "3.0.0-alpha-3000020220729001",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-3000020220729001",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-3000020220729001",
"@rushstack/eslint-patch": "^1.1.4",
"@types/node": "^16.11.47",
"@vitejs/plugin-vue": "^3.0.1",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.21.0",
"eslint-plugin-vue": "^9.3.0",
"prettier": "^2.7.1",
"sass": "^1.54.0",
"typescript": "~4.7.4",
"vite": "^3.0.4",
"vue-tsc": "^0.39.5"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"@dcloudio/types",
"miniprogram-api-typings",
"mini-types"
],
"paths": {
"@/*": [
"./src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/**/*.ts",
"types/**/*.tsx",
"components/**/*.vue"
],
"exclude": [
"node_modules",
"unpackage",
"src/**/*.nvue"
]
}
vite.config.ts
import {defineConfig} from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/dev-api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\\/dev-api/, '')
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router'],
'uni-vendor': ['@dcloudio/uni-app'],
'store-vendor': ['pinia']
}
}
}
}
})
环境配置文件
.env.development
# 开发环境配置
VITE_APP_TITLE=UniApp管理系统
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_ENV=development
.env.production
# 生产环境配置
VITE_APP_TITLE=UniApp管理系统
VITE_API_BASE_URL=https://api.yourdomain.com
VITE_APP_ENV=production
核心工具函数
utils/request.ts
import {AppConfig} from '@/config'
import {getToken, removeToken} from '@/utils/auth'
interface RequestOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
data?: any
headers?: Record<string, string>
timeout?: number
}
class HttpRequest {
private baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
// 请求拦截
private interceptors(instance: any) {
// 请求拦截器
instance.interceptors.request.use(
(config: any) => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: any) => {
return Promise.reject(error)
}
)
// 响应拦截器
instance.interceptors.response.use(
(response: any) => {
const {data} = response
if (data.code === 200) {
return data.data
} else if (data.code === 401) {
removeToken()
uni.navigateTo({url: '/pages/login/login'})
return Promise.reject(new Error('登录已过期'))
} else {
uni.showToast({title: data.msg, icon: 'none'})
return Promise.reject(new Error(data.msg))
}
},
(error: any) => {
uni.showToast({title: '网络错误', icon: 'none'})
return Promise.reject(error)
}
)
}
// 核心请求方法
request(options: RequestOptions) {
const instance = uni.request
this.interceptors(instance)
return instance({
baseURL: this.baseUrl,
timeout: options.timeout || AppConfig.timeout,
…options
})
}
get(url: string, params?: any) {
return this.request({url, method: 'GET', data: params})
}
post(url: string, data?: any) {
return this.request({url, method: 'POST', data})
}
put(url: string, data?: any) {
return this.request({url, method: 'PUT', data})
}
delete(url: string) {
return this.request({url, method: 'DELETE'})
}
}
export default new HttpRequest(AppConfig.apiBaseUrl)
utils/auth.ts
const TOKEN_KEY = 'ACCESS_TOKEN'
export function getToken(): string | null {
return uni.getStorageSync(TOKEN_KEY)
}
export function setToken(token: string): void {
uni.setStorageSync(TOKEN_KEY, token)
}
export function removeToken(): void {
uni.removeStorageSync(TOKEN_KEY)
}
export function isAuthenticated(): boolean {
return !!getToken()
}
utils/format.ts
// 日期格式化
export function formatDate(date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss'): string {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
// 数字格式化
export function formatNumber(num: number, decimals = 2): string {
return num.toLocaleString('zh-CN', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
})
}
// 手机号脱敏
export function maskPhone(phone: string): string {
return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, '$1****$2')
}
// 金额格式化
export function formatCurrency(amount: number): string {
return `¥${formatNumber(amount, 2)}`
}
类型定义文件
types/api.ts
// 通用响应类型
export interface ApiResponse<T = any> {
code: number
msg: string
data: T
}
// 分页响应类型
export interface PageResponse<T> {
total: number
rows: T[]
pageNum: number
pageSize: number
}
// 分页查询参数
export interface PageQuery {
pageNum?: number
pageSize?: number
orderByColumn?: string
isAsc?: 'asc' | 'desc'
}
types/component.ts
// 基础组件Props
export interface BaseComponentProps {
className?: string
style?: string | Record<string, any>
}
// 表格列配置
export interface TableColumn {
prop: string
label: string
width?: string | number
align?: 'left' | 'center' | 'right'
formatter?: (row: any, column: any, cellValue: any) => string
}
// 表单字段配置
export interface FormField {
prop: string
label: string
type: 'input' | 'select' | 'radio' | 'checkbox' | 'textarea' | 'date'
required?: boolean
placeholder?: string
options?: Array<{ label: string; value: any }>
rules?: any[]
}
types/store.ts
// 用户状态类型
export interface UserInfo {
userId: number
userName: string
nickName: string
email: string
phonenumber: string
sex: string
avatar: string
status: string
delFlag: string
loginIp: string
loginDate: string
createBy: string
createTime: string
updateBy: string
updateTime: string
remark: string
dept: DeptInfo
roles: RoleInfo[]
permissions: string[]
}
export interface DeptInfo {
deptId: number
parentId: number
ancestors: string
deptName: string
orderNum: number
leader: string
phone: string
email: string
status: string
delFlag: string
parentName: string
children: DeptInfo[]
}
export interface RoleInfo {
roleId: number
roleName: string
roleKey: string
roleSort: number
dataScope: string
menuCheckStrictly: boolean
deptCheckStrictly: boolean
status: string
delFlag: string
flag: boolean
menuIds: number[]
deptIds: number[]
admin: boolean
}
🕳️ 五、避坑指南:项目结构的 6 个致命错误
⚠️ 坑点 1:目录层级过深
现象: 文件路径长得离谱
❌ 错误示范
src/pages/system/user/list/index.vue
src/api/system/user/list/index.ts
src/store/modules/system/user/list/index.ts
问题:
– 导入路径超长:import xxx from '../../../../../../xxx'
– 难以定位文件
– 重构困难
✅ 正确做法
控制层级在 3-4 层以内:
src/
├── pages/
│ └── system/
│ ├── user-list.vue # ✅ 扁平化命名
│ ├── user-detail.vue
│ └── user-edit.vue
├── api/
│ └── system.ts # ✅ 按模块聚合
└── store/
└── modules/
└── system.ts # ✅ 统一管理
最佳实践:
- ✅ 目录层级不超过 4 层
- ✅ 文件名使用完整语义(user-list vs list)
- ✅ 同类型文件放在同级目录
⚠️ 坑点 2:API 调用分散
现象: 每个页面都自己写 uni.request
<!– ❌ 错误示范 – Page A –>
<script>
uni.request({
url: 'http://localhost:8080/api/user/list',
method: 'GET',
success: (res) => { … }
})
</script>
<!– ❌ 错误示范 – Page B –>
<script>
uni.request({
url: '/api/user/list', // 又一个版本
method: 'post', // method 大小写还不一致
data: { page: 1 },
success: (res) => { … }
})
</script>
问题:
– baseURL 不统一
– 错误处理重复
– Token 管理混乱
– 难以维护
✅ 正确做法
// ✅ 统一封装:src/utils/request.ts
class HttpRequest {
get<T>(url: string, params?: any): Promise<ApiResponse<T>> {
return this.request({ url, method: 'GET', data: params })
}
post<T>(url: string, data?: any): Promise<ApiResponse<T>> {
return this.request({ url, method: 'POST', data })
}
// 统一拦截器、错误处理、Token 管理…
}
export default new HttpRequest(AppConfig.apiBaseUrl)
// ✅ 统一 API 管理:src/api/system.ts
import request from '@/utils/request'
export function getUserList(params: UserQuery) {
return request.get<PageResponse<User>>('/system/user/list', params)
}
export function createUser(data: UserCreate) {
return request.post<User>('/system/user', data)
}
// ✅ 页面使用
const users = await getUserList({ pageNum: 1, pageSize: 10 })
收益:
- ✅ 代码复用率提升 70%
- ✅ Bug 减少 50%
- ✅ 接口变更只需改一处
⚠️ 坑点 3:TypeScript 类型缺失
现象: 打着 TypeScript 的旗号写 JavaScript
// ❌ 错误示范
interface User {
id: any // ❌ any 类型
name: string
email: any // ❌ 又是 any
roles: any[] // ❌ 数组元素也是 any
}
function getUsers(): Promise<any> { // ❌ 返回 any
return request.get('/users')
}
// 使用时完全没类型提示
const users = await getUsers()
users[0].xxx // ❌ 随便访问,编译不报错
✅ 正确做法
// ✅ 精确定义类型
interface UserRole {
roleId: number
roleName: string
roleKey: string
}
interface UserInfo {
userId: number
userName: string
nickName: string
email: string
phonenumber: string
roles: UserRole[]
permissions: string[]
}
interface ApiResponse<T> {
code: number
msg: string
data: T
}
// ✅ 明确的函数签名
async function getUserById(id: number): Promise<ApiResponse<UserInfo>> {
return request.get(`/system/user/${id}`)
}
// ✅ 使用时有完整的类型提示
const result = await getUserById(1)
result.data.userId // ✅ 智能提示
result.data.roles[0].roleName // ✅ 也有提示
类型定义原则:
- ✅ 永远不要使用 any(除非万不得已)
- ✅ 使用 unknown 代替 any 更安全
- ✅ 为所有 API 响应定义类型
- ✅ 组件 Props 必须明确类型
⚠️ 坑点 4:状态管理滥用
现象: 把所有数据都放 Pinia
// ❌ 错误示范 – store/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null, // ✅ 合理
token: null, // ✅ 合理
userList: [], // ❌ 不应该!这是页面数据
menuList: [], // ❌ 不应该!这是接口数据
formdata: {}, // ❌ 不应该!这是表单临时数据
searchKeyword: '', // ❌ 不应该!这是搜索条件
})
})
问题:
– Store 膨胀,难以维护
– 数据混乱,不知道哪是源头
– 内存占用高
– 调试困难
✅ 正确做法
// ✅ 只存储全局共享状态
export const useUserStore = defineStore('user', {
state: () => ({
// ✅ 需要跨页面共享的数据
userInfo: null as UserInfo | null,
token: null as string | null,
permissions: [] as string[],
roles: [] as RoleInfo[],
// ✅ 用户配置
theme: 'light' as 'light' | 'dark',
language: 'zh-CN' as 'zh-CN' | 'en',
}),
getters: {
isLoggedIn: (state) => !!state.token,
username: (state) => state.userInfo?.nickName || '未登录',
},
actions: {
setToken(token: string) {
this.token = token
uni.setStorageSync('ACCESS_TOKEN', token)
},
async getUserInfo() {
const res = await getUserInfoApi()
this.userInfo = res.data
return res.data
},
}
})
// ❌ 页面级数据应该在页面内管理
// <script setup>
const userList = ref([]) // ✅ 页面数据用 ref
const searchForm = reactive({}) // ✅ 表单数据用 reactive
// </script>
状态管理原则:
- ✅ 只存全局共享数据(Token、UserInfo、Permissions)
- ✅ 页面数据用 ref/reactive
- ✅ 表单数据用本地状态
- ✅ 接口缓存数据考虑使用 SWR 或 React Query
⚠️ 坑点 5:环境变量混乱
现象: 多环境配置混在一起
// ❌ 错误示范
const baseUrl = process.env.NODE_ENV === 'development'
? 'http://localhost:8080'
: 'https://api.production.com'
// 然后在代码里到处判断
if (process.env.NODE_ENV === 'production') {
// …
} else {
// …
}
问题:
– 只有开发和生产两种环境
– 测试环境怎么办?
– 预发布环境怎么办?
– 配置项散落在各处
✅ 正确做法
# ✅ 使用 Vite 环境变量
# .env.development
VITE_APP_TITLE=管理系统开发环境
VITE_API_BASE_URL=http://dev-api.example.com
VITE_UPLOAD_URL=http://dev-upload.example.com
# .env.test
VITE_APP_TITLE=管理系统测试环境
VITE_API_BASE_URL=http://test-api.example.com
VITE_UPLOAD_URL=http://test-upload.example.com
# .env.production
VITE_APP_TITLE=管理系统生产环境
VITE_API_BASE_URL=https://api.example.com
VITE_UPLOAD_URL=https://upload.example.com
// ✅ 统一配置管理:src/config/index.ts
export const AppConfig = {
title: import.meta.env.VITE_APP_TITLE,
apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
uploadUrl: import.meta.env.VITE_UPLOAD_URL,
timeout: 30000,
} as const
// ✅ 使用时
request.get(`${AppConfig.apiBaseUrl}/user/list`)
环境管理最佳实践:
- ✅ 至少区分 dev/test/prod 三个环境
- ✅ 所有配置项集中管理
- ✅ 不要在代码里硬编码 URL
- ✅ 敏感信息使用环境变量
⚠️ 坑点 6:忽视性能优化
现象: 项目越来越大,启动越来越慢
常见问题:
– 首屏加载需要 5-8 秒
– 小程序包超过 2MB 限制
– 页面切换卡顿
– 内存占用过高
✅ 优化方案
方案一:路由懒加载
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router'], // ✅ Vue 相关
'uni-vendor': ['@dcloudio/uni-app'], // ✅ UniApp 相关
'store-vendor': ['pinia'], // ✅ 状态管理
}
}
}
}
})
方案二:图片优化
<!– ❌ 错误示范 –>
<image src="/static/large-banner.jpg" /> <!– 2MB –>
<!– ✅ 正确做法 –>
<image
src="/static/large-banner.webp"
mode="aspectFill"
lazy-load <!– ✅ 懒加载 –>
/>
<!– ✅ 使用 CDN –>
<image :src="cdnUrl + '/banner.jpg'" />
方案三:虚拟列表
<!– 长列表使用虚拟滚动 –>
<template>
<scroll-view scroll-y style="height: 100vh">
<view v-for="item in visibleList" :key="item.id">
{{ item.name }}
</view>
</scroll-view>
</template>
<script setup>
// 只渲染可见区域的元素
const visibleList = computed(() => {
return allList.slice(startIndex, endIndex)
})
</script>
方案四:防抖节流
// utils/debounce.ts
export function debounce<T extends (…args: any[]) => any>(
fn: T,
delay = 300
): (…args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null
return function(this: any, …args: Parameters<T>) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 使用
const handleSearch = debounce((keyword: string) => {
searchApi(keyword)
}, 500)
💡 六、性能优化建议
6.1 构建优化
# Vite 配置优化
build:
chunkSizeWarningLimit: 500 # 包大小警告阈值
minify: terser # 压缩工具
terserOptions:
compress:
drop_console: true # 生产环境移除 console
drop_debugger: true # 移除 debugger
# 代码分割
rollupOptions:
output:
manualChunks:
vendor: [vue, pinia]
ui: [@dcloudio/uni-ui]
效果:
- ✅ 构建体积减少 40%
- ✅ 加载速度提升 35%
6.2 运行时优化
// 使用计算缓存
const filteredList = computed(() => {
return list.value.filter(item => item.status === 'active')
})
// 避免在模板中计算
// ❌ 不推荐
<view v-for="item in list.filter(i => i.active)" />
// ✅ 推荐
<view v-for="item in filteredList" />
6.3 内存优化
// 及时清理定时器
onUnmounted(() => {
if (timer) clearInterval(timer)
})
// 移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// 大对象及时置空
const largeData = ref(null)
onUnmounted(() => {
largeData.value = null
})
📝 七、总结
本文系统介绍了 UniApp + Vue3 + TypeScript 企业级项目结构的完整设计方案。关键收获:
通过若依管理系统实战案例,展示了如何搭建高可维护、高性能的移动应用架构。掌握这些技能,你将能够:
- ✅ 快速搭建企业级 UniApp 项目
- ✅ 避免常见陷阱和错误
- ✅ 提升团队协作效率
- ✅ 构建高质量跨平台应用
👍 如果本文对你有帮助,欢迎点赞、收藏、转发! 💬 有任何问题或建议,请在评论区留言交流~ 🔔 关注我,获取 UniApp + Vue3 + TypeScript 系列文章!
专栏导航:
- 上一篇:UniApp 项目创建完整指南
- 下一篇:UniApp uView UI 组件库集成
- 飞鱼项目实战:SpringBoot3 + Vue3 企业级管理系统



