UniApp 作为跨端开发框架,凭借“一次编写,多端运行”的能力,成为众多团队构建小程序、H5、App 的首选方案。然而,当项目从 Demo 走向生产,缺乏工程化约束的代码会迅速腐化:目录混乱、状态管理失控、请求层重复、构建流程低效。本文将从 0 到 1 梳理一套可落地的 UniApp 工程化架构,覆盖目录设计、状态管理、请求封装、组件规范、构建优化与代码质量六个维度。
一、目录结构:按职责分层,而非按文件类型
许多开发者习惯将文件按类型堆放——所有页面在 pages,所有组件在 components,所有 API 在 api。项目变大后,修改一个功能需要跨五个目录跳转。更合理的做法是按业务模块划分,内部再按职责分层:
src/
├── pages/ # 主包页面(仅保留 TabBar 和核心页面)
├── subPackages/ # 分包目录,按业务模块拆分
│ ├── order/
│ ├── user/
│ └── activity/
├── components/ # 全局公共组件
├── hooks/ # 组合式函数
├── store/ # 状态管理
├── api/ # 接口定义层
├── utils/ # 工具函数
├── static/ # 静态资源
├── styles/ # 全局样式与变量
└── config/ # 环境配置与常量
分包策略是 UniApp 工程化的关键。微信小程序对主包体积有 2MB 限制,建议将非核心业务全部放入 subPackages,并在 pages.json 中配置 preloadRule 实现分包预下载,兼顾体积与体验。
二、状态管理:Pinia 替代 Vuex
UniApp 从 Vue2 到 Vue3 的迁移中,状态管理推荐使用 Pinia。相比 Vuex,Pinia 去掉了 Mutation,TypeScript 支持更友好,且天然支持组合式 API。
// store/modules/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: null
}),
getters: {
isLogin: (state) => !!state.token
},
actions: {
async login(credentials) {
const { data } = await api.login(credentials)
this.token = data.token
this.userInfo = data.userInfo
uni.setStorageSync('token', data.token)
},
logout() {
this.token = ''
this.userInfo = null
uni.removeStorageSync('token')
}
}
})
关键实践:按业务域拆分 Store 文件,避免单一巨型 Store;在 main.js 中全局注册 Pinia 实例;对于需要持久化的状态(如 token),在 action 中同步写入 Storage,而非依赖插件自动持久化,以便精确控制写入时机。
三、请求层:统一拦截与类型安全
直接调用 uni.request 会导致每个页面重复处理 loading、错误提示和 token 刷新。建议封装一个请求类,统一处理:
// utils/request.js
class Request {
constructor(baseURL) {
this.baseURL = baseURL
this.interceptors = { request: [], response: [] }
}
async request(options) {
// 请求拦截
for (const fn of this.interceptors.request) {
options = await fn(options)
}
return new Promise((resolve, reject) => {
uni.request({
url: this.baseURL + options.url,
method: options.method || 'GET',
data: options.data,
header: { 'Content-Type': 'application/json', ...options.header },
success: async (res) => {
// 响应拦截
for (const fn of this.interceptors.response) {
res = await fn(res)
}
if (res.statusCode === 200) {
resolve(res.data)
} else {
uni.showToast({ title: res.data.message || '请求失败', icon: 'none' })
reject(res)
}
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' })
reject(err)
}
})
})
}
}
配合 api/ 目录按模块定义接口函数,实现请求逻辑与业务逻辑解耦。例如 api/user.js 导出 login、getProfile 等函数,页面只关心调用与数据消费。
四、组件规范:EasyCom 与原子化设计
UniApp 的 EasyCom 机制允许组件自动按需引入,无需手动 import。在 pages.json 中配置:
{
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "@/components/u-$1/u-$1.vue",
"^my-(.*)": "@/components/my-$1/my-$1.vue"
}
}
}
建议将组件分为三层:
- 基础组件:按钮、输入框、弹窗等,与业务无关,可跨项目复用。
- 业务组件:如订单卡片、用户头像组,包含特定业务逻辑。
- 页面组件:仅在某页面使用的局部组件,放在页面同级
components目录。
命名约定:基础组件以 base- 或 UI 库前缀命名,业务组件以模块名开头(如 order-card),避免命名冲突。
五、构建优化:条件编译与体积控制
UniApp 的条件编译是跨端适配的利器,但滥用会导致代码可读性下降。建议将平台差异收敛到配置文件或工具函数中,而非散落在业务代码里:
// utils/platform.js
export const isMP = process.env.UNI_PLATFORM === 'mp-weixin'
export const isH5 = process.env.UNI_PLATFORM === 'h5'
export function setNavigationBar(title) {
// #ifdef MP-WEIXIN
uni.setNavigationBarTitle({ title })
// #endif
// #ifdef H5
document.title = title
// #endif
}
体积控制方面:开启 uni-app 的 --minimize 压缩;使用 webpack-bundle-analyzer 分析依赖;将大图上传 CDN 而非打包进 static;对非首屏页面使用分包异步化。
六、代码质量:Lint 与 Git Hooks
工程化离不开自动化约束。推荐配置:
- ESLint + Prettier:统一代码风格,
@vue/eslint-config-standard配合 UniApp 全局变量声明。 - Stylelint:约束 SCSS/CSS 书写规范。
- Husky + lint-staged:在
pre-commit阶段对暂存文件执行 lint 和格式化,防止脏代码进入仓库。 - Commitlint:规范提交信息,便于生成 CHANGELOG。
// package.json
{
"lint-staged": {
"*.{js,vue}": ["eslint --fix", "prettier --write"],
"*.{scss,css}": ["stylelint --fix"]
}
}
总结
UniApp 工程化不是一次性配置,而是持续迭代的约束体系。从目录分层明确代码归属,用 Pinia 管理共享状态,以请求封装隔离网络细节,借 EasyCom 提升组件复用,靠条件编译收敛处理平台差异,最后用 Lint 工具链守住质量底线。这套架构在多个中大型项目中验证可行,能将协作效率提升 40% 以上。建议团队根据自身规模裁剪,先落地请求层与目录规范,再逐步引入其余模块。
未经允许不得转载:任鹏个人博客 » UniApp 项目从 0 到 1 搭建工程化架构的最佳实践


朋友圈点赞图在线生成源码