夜雨聆风学习资料网

ARTICLE · 1280337

uni-app 开发小程序:项目架构以及经验分享

uni-app 开发小程序:项目架构以及经验分享

uni-app 开发小程序:项目架构与实战经验分享

使用 uni-app 开发微信小程序、支付宝小程序以及 H5 等多端应用时,真正影响项目质量的,不仅是页面开发速度,更重要的是项目架构设计、业务模块划分、状态管理、接口封装、性能优化,以及团队协作规范。

本文从实际项目开发的角度,分享一套适合中小型项目、也便于扩展到大型业务的 uni-app 项目架构,并结合常见开发问题总结一些实战经验。

一、uni-app 项目整体架构

1. 技术选型

一个比较实用的技术栈如下:

技术
推荐方案
主要用途
跨端框架
uni-app
一套代码开发多个平台
开发语言
Vue 3 + TypeScript
组件开发与类型约束
构建工具
Vite
项目构建与开发调试
状态管理
Pinia
用户信息、全局状态管理
网络请求
自定义 request 封装
统一处理请求与响应
样式方案
SCSS
变量管理、样式复用
代码规范
ESLint + Prettier
统一代码风格
版本管理
Git
分支管理与协作开发

选型建议:

•
新项目优先考虑 Vue 3、TypeScript 和 Pinia。
•
如果团队已经有成熟的 Vue 2 项目,可以根据现有依赖和迁移成本选择技术栈。
•
如果项目以微信小程序为主,需要重点关注微信小程序的运行环境、分包规则和平台 API 差异。
•
如果需要同时发布到多个平台,尽量将业务逻辑与平台专属代码分离。

需要注意,uni-app 的传统 Vue CLI 项目与基于 Vite 的项目在工程配置上有所区别;具体采用哪种方式,应以项目创建方式及相关依赖的兼容性为准。

2. 推荐的项目目录结构

建议采用公共基础设施 + 业务模块 + 页面分包的组织方式,而不是把所有工具函数、接口和业务逻辑都堆在一个目录里。

uniapp-project/
├── api/                       # 接口层
│   ├── modules/
│   │   ├── user.ts            # 用户接口
│   │   ├── order.ts           # 订单接口
│   │   └── product.ts         # 商品接口
│   └── request.ts             # 统一请求封装
│
├── components/                # 全局公共组件
│   ├── AppButton.vue
│   ├── AppEmpty.vue
│   └── AppLoading.vue
│
├── pages/                     # 主包页面
│   ├── index/index.vue         # 首页
│   ├── login/login.vue         # 登录页
│   └── user/user.vue           # 个人中心
│
├── pages-sub/                 # 业务分包页面
│   ├── order/
│   │   ├── list.vue
│   │   └── detail.vue
│   ├── product/
│   │   ├── list.vue
│   │   └── detail.vue
│   └── address/
│       └── list.vue
│
├── stores/                    # Pinia 状态管理
│   ├── user.ts
│   └── cart.ts
│
├── composables/               # 组合式函数
│   ├── usePagination.ts
│   └── useLogin.ts
│
├── utils/                     # 通用工具函数
│   ├── storage.ts
│   ├── validate.ts
│   └── format.ts
│
├── constants/                 # 常量与枚举
│   ├── api.ts
│   └── business.ts
│
├── types/                     # TypeScript 类型定义
│   ├── user.ts
│   └── order.ts
│
├── static/                    # 静态资源
├── App.vue
├── main.ts
├── manifest.json
├── pages.json
├── uni.scss
└── package.json

这是一种推荐的组织方案,并不是 uni-app 强制要求的目录结构。项目实际页面路径必须与 pages.json 中的配置保持一致,分包根目录也应按所采用的构建方案配置。官方对标准工程目录的说明可参考 uni-app 工程结构文档。

3. 各层职责如何划分?

一个容易维护的项目,可以划分为以下几层:

页面层 Pages
    │
    ▼
业务逻辑层 Composables
    │
    ▼
接口层 API
    │
    ▼
请求基础设施 Request
    │
    ▼
后端服务 Backend
全局状态 Stores ──► 页面层 / 业务逻辑层
通用组件 Components ──► 页面层
通用工具 Utils ──► 各业务模块

各层的职责如下:

层级
核心职责
不建议做的事情
Pages
页面布局、交互、页面生命周期
堆积大量复杂业务逻辑
Components
可复用 UI 组件
直接依赖某个具体业务页面
Composables
复用业务流程和响应式逻辑
混入不相关的全局状态
API
定义业务接口及参数类型
直接操作页面 UI
Request
统一处理请求、响应和异常
写入具体页面的业务逻辑
Stores
管理跨页面共享状态
存储所有临时页面状态
Utils
提供纯函数和通用工具
依赖页面实例或特定业务状态

架构设计的核心原则:让变化尽可能局限在自己的模块内。

例如,修改订单接口时,不应该需要同时修改首页、个人中心和商品详情页中的多份重复请求代码。


二、网络请求封装:项目中最值得提前规划的部分

实际开发中,网络请求往往涉及登录态、Token 过期、统一错误提示、接口超时、重复提交等问题。

如果每个页面都直接调用 uni.request(),项目规模增大后会出现大量重复代码。

建议采用三层结构:

1
request.ts:统一请求基础设施。
2
api/modules/*.ts:定义各业务模块接口。
3
页面或组合式函数:调用业务接口并处理页面状态。

1. 封装统一请求方法

下面给出一个适用于 Vue 3 + TypeScript 项目的基础示例。

api/request.ts:

const BASE_URL = 'https://api.example.com'
interface RequestOptions {
  url: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: Record<string, unknown>
  header?: Record<string, string>
}
interface ApiResponse<T> {
  code: number
  message: string
  data: T
}
export function request<T>(
  options: RequestOptions
): Promise<T> {
  return new Promise((resolve, reject) => {
    const token = uni.getStorageSync('token')
    uni.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data,
      header: {
        'Content-Type': 'application/json',
        ...(token
          ? { Authorization: `Bearer ${token}` }
          : {}),
        ...options.header
      },
      timeout: 15000,
      success: (res) => {
        if (res.statusCode < 200 || res.statusCode >= 300) {
          reject(new Error(`HTTP 错误:${res.statusCode}`))
          return
        }
        const result = res.data as ApiResponse<T>
        if (result.code !== 0) {
          reject(new Error(result.message || '请求失败'))
          return
        }
        resolve(result.data)
      },
      fail: (error) => {
        reject(error)
      }
    })
  })
}

注意:这是一份基础示例,需要根据后端约定调整。 例如,有些后端使用 code === 200 表示成功,有些接口直接返回业务数据,也有些接口需要特殊处理 HTTP 状态码。

另外,BASE_URL 应根据开发、测试、生产环境分别配置,不应将示例域名直接用于生产。

2. 按业务模块封装 API

api/modules/user.ts:

import { request } from '../request'
export interface UserInfo {
  id: number
  nickname: string
  avatar: string
}
export function getUserInfo() {
  return request<UserInfo>({
    url: '/user/info',
    method: 'GET'
  })
}
export function updateUserInfo(
  data: Pick<UserInfo, 'nickname' | 'avatar'>
) {
  return request<UserInfo>({
    url: '/user/info',
    method: 'PUT',
    data
  })
}

这样,页面不必关心接口地址、Token 拼接和底层请求细节。

3. 页面中调用接口

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getUserInfo } from '@/api/modules/user'
import type { UserInfo } from '@/api/modules/user'
const userInfo = ref<UserInfo | null>(null)
const loading = ref(false)
const errorMessage = ref('')
async function loadUserInfo() {
  loading.value = true
  errorMessage.value = ''
  try {
    userInfo.value = await getUserInfo()
  } catch (error) {
    errorMessage.value =
      error instanceof Error
        ? error.message
        : '加载失败'
  } finally {
    loading.value = false
  }
}
onMounted(() => {
  loadUserInfo()
})
</script>
<template>
  <view class="user-page">
    <view v-if="loading">加载中...</view>
    <view v-else-if="errorMessage">
      {{ errorMessage }}
    </view>
    <view v-else-if="userInfo">
      <image
        class="avatar"
        :src="userInfo.avatar"
        mode="aspectFill"
      />
      <text>{{ userInfo.nickname }}</text>
    </view>
  </view>
</template>

这套方式有几个好处:

•
接口调用统一,方便维护。
•
TypeScript 能帮助检查接口参数和返回值。
•
页面可以独立控制加载、空数据和异常状态。
•
更换后端地址或调整请求头时,不必逐个修改页面。

进一步优化建议: 生产项目还应统一处理未登录状态、Token 刷新、业务错误码、请求取消或去重、文件上传下载,以及错误日志。不要在每个页面里重复编写这些逻辑。


三、页面路由与分包:小程序架构设计的关键

uni-app 的页面路由主要通过 pages.json 配置。

随着业务页面增多,合理划分主包和分包,有助于控制主包体积,改善小程序首次启动体验。具体行为和限制应以目标平台的官方规范为准。uni-app

1. 主包应该放什么?

通常建议主包放置:

•
首页。
•
TabBar 页面。
•
登录页等必要的基础页面。
•
启动时必须加载的公共资源。

分包则可以放置:

•
订单列表、订单详情。
•
商品分类、商品详情。
•
地址管理。
•
优惠券、积分、会员中心。
•
营销活动、帮助中心等相对独立的业务页面。

这里有一个容易忽略的问题:不是页面越多,越应该拆分分包,而是应该根据业务边界、依赖关系和加载频率进行拆分。

2. 分包配置示例

假设项目包含首页、个人中心、订单管理和商品详情。

可以在 pages.json 中进行如下配置:

{
  "pages": [
    {
      "path": "pages/index/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    },
    {
      "path": "pages/user/user",
      "style": {
        "navigationBarTitleText": "个人中心"
      }
    }
  ],
  "subPackages": [
    {
      "root": "pages-sub/order",
      "pages": [
        {
          "path": "list",
          "style": {
            "navigationBarTitleText": "我的订单"
          }
        },
        {
          "path": "detail",
          "style": {
            "navigationBarTitleText": "订单详情"
          }
        }
      ]
    },
    {
      "root": "pages-sub/product",
      "pages": [
        {
          "path": "detail",
          "style": {
            "navigationBarTitleText": "商品详情"
          }
        }
      ]
    }
  ],
  "tabBar": {
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的"
      }
    ]
  }
}

以上示例展示了配置思路。实际项目还需要根据目标平台配置必要的页面样式、图标及其他选项。

3. 分包设计中的三个经验

经验一:按照业务模块拆分,而不是机械地平均分配。

例如:

主包
├── 首页
├── 登录
└── 个人中心
订单分包
├── 订单列表
├── 订单详情
└── 物流详情
商品分包
├── 商品列表
├── 商品详情
└── 商品评价
营销分包
├── 优惠券
├── 活动详情
└── 积分商城

这样做的好处是业务边界清晰,后续增加页面时也容易找到合适的位置。

经验二:注意公共代码的依赖。

如果多个分包都依赖同一份工具代码或组件,这些依赖的实际打包位置会受到构建配置和平台分包优化规则影响。

因此,不能仅凭源码目录判断最终包体积,应当检查实际编译产物。

经验三:不要把所有公共组件都放进主包。

有些组件只被某个分包使用,例如复杂的订单物流组件,可以考虑放在对应业务模块中,避免不必要的公共依赖。

分包配置与优化细节可以参考官方文档:uni-app 页面路由与分包配置。


四、状态管理:什么时候使用 Pinia?

在 uni-app Vue 3 项目中,Pinia 是常用的状态管理方案,官方也提供了相应的集成说明。Uni App

但实际开发中,并不是所有数据都需要放到全局状态里。

1. 哪些数据适合全局管理?

数据类型
是否建议全局管理
原因
用户信息
是
多个页面共享
登录状态
是
影响多个页面的访问权限
购物车数据
通常是
商品列表、购物车、结算页可能共享
当前页面表单输入
通常不是
页面内部使用即可
商品详情接口数据
视情况而定
取决于缓存和复用需求
弹窗是否显示
通常不是
一般属于局部 UI 状态
全局主题配置
是
多个页面需要保持一致

判断标准很简单:

如果数据需要跨页面共享,或者需要集中管理业务状态,可以考虑使用 Pinia;如果只是当前页面临时使用的数据,优先使用组件自身的响应式状态。

2. 用户状态管理示例

stores/user.ts:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface UserInfo {
  id: number
  nickname: string
  avatar: string
}
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const userInfo = ref<UserInfo | null>(null)
  const isLogin = computed(() => Boolean(token.value))
  function setToken(value: string) {
    token.value = value
    uni.setStorageSync('token', value)
  }
  function setUserInfo(value: UserInfo | null) {
    userInfo.value = value
  }
  function logout() {
    token.value = ''
    userInfo.value = null
    uni.removeStorageSync('token')
  }
  function restoreToken() {
    token.value = uni.getStorageSync('token') || ''
  }
  return {
    token,
    userInfo,
    isLogin,
    setToken,
    setUserInfo,
    logout,
    restoreToken
  }
})

在页面中使用:

import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
console.log(userStore.isLogin)
console.log(userStore.userInfo)

需要注意:这里的 restoreToken() 需要在合适的应用初始化流程中调用。

另外,生产项目不能仅凭本地 Token 判断用户一定有效。服务端仍需验证身份和权限;退出登录时,也要考虑清理用户缓存、购物车等可能存在的用户隔离数据。

一个常见错误:同时维护 Pinia 中的 Token 和多个页面中的 Token 副本。 这会导致登录状态更新后,各处数据不一致。建议明确唯一的数据来源,并统一登录、登出及 Token 刷新流程。


五、跨端开发:如何减少平台兼容问题?

uni-app 的跨端能力依赖编译器和各平台运行时,因此一套代码可以运行在多个平台,但并不意味着所有 API、组件和样式都完全一致。uni-app

1. 平台差异尽量集中处理

假设微信小程序需要调用特定的登录 API,而 H5 使用不同的认证流程。

不建议在很多页面里散落平台判断:

// 不建议在各个页面重复编写类似判断
// #ifdef MP-WEIXIN
// 微信平台逻辑
// #endif
// #ifdef H5
// H5 平台逻辑
// #endif

更好的方式是将平台差异收敛到专门的模块中。

例如:

services/
├── auth.ts
└── platform/
    ├── auth.mp-weixin.ts
    └── auth.h5.ts

这是一种组织思路。具体文件如何参与编译,需要根据 uni-app 支持的条件编译规则和项目构建配置实现。

2. 常见的平台兼容问题

问题
处理建议
浏览器 API 不可用
使用 uni-app 提供的跨端 API,或针对平台适配
不同平台的登录机制不同
将认证流程抽象为统一接口
页面样式出现差异
尽量使用常规布局,针对目标平台进行验证
文件上传行为不同
封装上传服务,统一业务调用方式
分享功能表现不一致
结合平台生命周期和分享规范实现
页面跳转参数复杂
统一路由参数格式,注意 URL 编码
第三方 UI 组件不兼容
提前验证目标平台支持情况

3. 使用条件编译的注意事项

条件编译适合解决确实存在的平台差异,但不应该成为所有业务逻辑的默认写法。

建议遵循以下原则:

•
优先使用跨端 API。
•
确实存在差异时,再使用条件编译。
•
将平台专属逻辑封装到独立模块。
•
对核心流程进行多平台测试。
•
不要假定微信小程序测试通过,就意味着支付宝小程序和 H5 也一定正常。

官方跨端开发说明:uni-app 跨端原理与开发规范。


六、性能优化:从启动速度到页面体验

小程序性能优化,不应该只盯着某一个页面的加载时间,而应关注启动、网络请求、页面渲染、资源加载和用户交互的整体体验。

1. 控制主包体积

建议优先检查:

•
主包是否包含不常用业务页面。
•
是否引入了体积较大的第三方依赖。
•
是否将仅被某个分包使用的组件放进了公共模块。
•
是否存在大量不必要的静态资源。
•
实际编译后的主包和分包体积是否符合目标平台的限制。

特别注意,源码看起来很精简,不代表编译后的代码一定很小。

应该以实际构建产物和目标平台的开发者工具检查结果为准。

2. 减少不必要的请求

例如,个人中心页面通常需要用户信息,但如果用户在首页已经获取过相关信息,就可以根据数据时效性和一致性要求,考虑复用缓存数据。

不过,缓存并不是越多越好。

建议明确不同数据的策略:

数据
可采用的策略
用户头像、昵称
短期复用,必要时刷新
商品列表
按业务要求缓存并支持刷新
订单状态
重点考虑数据时效性
支付结果
以服务端确认结果为准
登录状态
本地保存辅助状态,服务端校验权限

3. 避免列表渲染卡顿

长列表是小程序中很常见的性能问题。

可以从以下方面优化:

1
使用合理的分页加载策略。
2
避免一次性渲染大量数据。
3
减少列表项中不必要的复杂组件。
4
避免在渲染过程中重复执行昂贵的计算。
5
及时清理不再需要的定时器和事件监听。
6
根据实际性能测试结果,决定是否采用虚拟列表等方案。

对于大部分业务列表,先做好分页、数据结构和组件渲染优化,通常比一开始就引入复杂方案更容易取得稳定收益。

4. 图片资源优化

图片是影响页面体验的重要因素之一。

建议:

•
使用适合展示场景的图片尺寸。
•
尽量避免加载远大于显示尺寸的图片。
•
列表优先使用缩略图。
•
对长列表图片使用合理的懒加载策略。
•
对需要长期缓存的资源制定明确的更新机制。

七、实战经验:项目中最容易踩的 8 个坑

这一部分比单纯介绍技术栈更重要。很多项目后期难维护,并不是因为技术选型错误,而是因为早期没有建立明确的开发规范。

坑一:把所有逻辑写在页面里

不推荐:

order-detail.vue
├── 页面布局
├── 接口请求
├── 订单状态判断
├── 支付逻辑
├── 物流逻辑
└── 数据格式化

随着业务增加,页面会越来越复杂。

建议将页面 UI、业务逻辑、接口请求和通用工具分开,让每个模块有明确的职责。

坑二:公共组件与业务组件混在一起

例如,按钮、空状态、加载提示适合放在公共组件目录。

而订单状态卡片、商品促销标签等具有明显业务属性的组件,更适合放在相应业务模块中。

不要为了复用而复用。 如果两个组件只有表面相似,但业务规则完全不同,强行合并反而会增加维护成本。

坑三:所有页面都直接使用本地缓存

uni.setStorageSync() 和 uni.getStorageSync() 适合部分本地持久化场景,但不应该被当成全局状态管理的替代品。

推荐区分:

•
页面临时状态。
•
应用内共享状态。
•
需要持久化的数据。
•
必须由服务端确认的数据。

这样可以减少数据不一致问题。

坑四:忽略异常状态

一个完整的页面,不应该只有成功状态。

建议至少考虑:

页面状态
├── 加载中
├── 加载成功
├── 空数据
├── 请求失败
├── 未登录
└── 无权限

对于分页列表,还要考虑加载更多、没有更多数据和重复请求等情况。

坑五:没有统一的环境配置

开发、测试和生产环境的 API 地址、应用标识等配置应当明确区分。

建议:

•
将环境配置集中管理。
•
避免在业务代码中散落域名。
•
发布前检查目标环境。
•
不要将密钥或敏感凭证直接写入前端代码。

需要特别强调:小程序客户端代码最终可以被获取,因此不能把真正的服务端密钥当成普通前端配置。

坑六:忽略请求竞态问题

例如,用户快速切换商品分类时,先发出的请求可能晚于后发出的请求返回,导致旧数据覆盖新数据。

处理方式包括:

•
为请求维护序号。
•
对可以取消的请求执行取消操作。
•
只接受当前有效请求的结果。
•
对搜索输入增加合理的防抖处理。

坑七:缺少统一的代码规范

项目初期只有一两个人时,代码规范看起来不重要。

但当多人协作时,命名、目录结构、错误处理和代码风格不一致,会显著增加维护成本。

建议统一:

•
文件与变量命名。
•
API 参数和返回类型。
•
组件 Props 和事件规范。
•
Git 分支和提交信息。
•
代码格式化及静态检查规则。

坑八:只在开发工具中测试

上线前至少要验证:

•
真机运行。
•
登录和退出登录。
•
网络异常。
•
页面返回与重复进入。
•
长列表滚动。
•
分享和授权流程。
•
不同机型的布局。
•
正式环境的接口与权限配置。

对于跨端项目,还需要在实际计划发布的平台上进行测试。


八、推荐的团队开发流程

如果是多人协作项目,可以采用下面的开发流程。

需求评审
   ↓
页面与接口设计
   ↓
模块划分
   ↓
基础架构搭建
   ↓
组件与接口开发
   ↓
业务页面开发
   ↓
联调与测试
   ↓
性能检查
   ↓
代码审查
   ↓
构建与发布

一个实用的开发顺序

第一阶段:建立基础设施

完成项目初始化、环境配置、路由、请求封装、状态管理和基础组件。

第二阶段:确定业务规范

统一接口返回结构、错误处理、页面状态、组件命名及代码规范。

第三阶段:按模块开发

例如,订单模块由固定成员负责接口、页面和相关业务组件,避免多人同时修改大量共享文件。

第四阶段:持续集成与测试

在每次提交或合并代码时,执行必要的静态检查、构建和测试。

第五阶段:发布前检查

检查构建产物、分包体积、环境配置、接口权限和核心业务流程。

对于较大的团队,可以进一步建立自动化测试、代码审查清单和版本发布流程。


九、项目架构设计的几个最终建议

如果从零开始搭建一个 uni-app 小程序项目,我会优先关注以下五件事:

优先级
工作内容
原因
P0
确定技术栈与目标平台
避免后续出现依赖兼容问题
P0
建立统一请求层
减少重复代码,统一异常处理
P0
设计主包与分包结构
为小程序加载和业务扩展打好基础
P1
规范状态管理
避免页面之间的数据不一致
P1
建立代码规范与测试流程
提升多人协作效率

我的建议是:不要一开始就追求复杂的架构,而是优先解决真实业务中的重复、耦合和维护问题。

对于中小型项目,清晰的模块划分、稳定的请求封装和合理的分包设计,往往比引入大量抽象层更有价值。

随着项目规模扩大,再逐步引入更完善的类型约束、业务模块隔离、自动化测试和持续集成机制。

官方文档参考

•
uni-app 工程结构
•
uni-app 跨端开发原理
•
uni-app Pinia 状态管理
•
uni-app 页面路由与分包配置
•
uni-app 分包优化配置

如果要把这套架构真正用于项目开发,我建议下一步直接确定项目类型。 不同类型的小程序,目录结构和业务模块设计会有明显区别:

1
电商小程序:商品、购物车、订单、支付、优惠券。
2
企业管理小程序:用户权限、审批流程、数据看板、表单。
3
内容社区小程序:内容列表、详情、评论、用户互动。
4
通用业务模板:登录、首页、个人中心、请求封装、权限控制。

确定项目类型后,就可以进一步设计一套更贴近实际开发的目录结构,并补全 pages.json、Pinia、API 请求封装和业务模块的可运行示例。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料