📌 本文定位: 面向 iOS/Android/鸿蒙原生工程师,系统梳理 uni-app 项目的完整目录结构。基于 uni-app 官方文档 并大幅补充官方未覆盖的工程化细节、隐藏配置和实战经验。
一、标准项目目录全景图
使用 HBuilderX 或 CLI (npx degit dcloudio/uni-preset-vue#vite-ts my-project) 创建的标准项目结构如下:
perl
my-uni-app/
├── pages/ # 📄 页面目录(核心)
│ ├── index/
│ │ └── index.vue # 首页
│ └── detail/
│ └── detail.vue # 详情页
├── components/ # 🧩 可复用组件目录
│ ├── uni-card/
│ │ └── uni-card.vue
│ └── my-button/
│ └── my-button.vue
├── static/ # 🖼️ 静态资源目录(不参与编译)
│ ├── images/
│ ├── fonts/
│ └── tabbar/
├── assets/ # 🎨 需编译处理的资源目录
│ ├── styles/
│ │ ├── variables.scss # SCSS 变量
│ │ └── global.css # 全局样式
│ └── icons/ # SVG 图标等
├── utils/ # 🔧 工具函数目录
│ ├── request.ts # 网络请求封装
│ ├── auth.ts # 登录鉴权
│ └── format.ts # 格式化工具
├── api/ # 🌐 API 接口定义目录
│ ├── user.ts
│ └── product.ts
├── store/ # 🗃️ 状态管理目录
│ ├── index.ts
│ ├── modules/
│ │ ├── user.ts
│ │ └── cart.ts
│ └── types.ts
├── composables/ # 🪝 Vue3 组合式函数目录
│ ├── useAuth.ts
│ └── usePagination.ts
├── types/ # 📝 TypeScript 类型定义目录
│ ├── global.d.ts
│ ├── api.d.ts
│ └── env.d.ts
├── uni_modules/ # 📦 uni-app 插件目录
│ ├── uni-popup/
│ ├── z-paging/
│ └── uni-scss/
├── hybrid/ # 🔀 App端本地HTML资源目录
│ └── html/
├── nativeplugins/ # 🔌 App原生插件目录
│ └── MyPlugin/
├── platform/ # 🏗️ 平台专属配置目录(CLI项目)
│ ├── app/
│ ├── mp-weixin/
│ └── h5/
├── App.vue # 🏠 应用根组件
├── main.ts / main.js # 🚀 应用入口文件
├── manifest.json # ⚙️ 应用配置清单(最重要)
├── pages.json # 📋 页面路由与窗口配置
├── uni.scss # 🎨 全局 SCSS 变量文件
├── index.html # 🌐 H5 模板文件
├── vite.config.ts # ⚡ Vite 构建配置
├── tsconfig.json # 📘 TypeScript 配置
├── package.json # 📦 项目依赖与脚本
├── .env / .env.production # 🌍 环境变量文件
├── .gitignore # 🙈 Git 忽略规则
└── README.md # 📖 项目说明
二、逐目录/逐文件深度解析
2.1 pages/ --- 页面目录
📖 官方定义: "存放所有页面的目录,每个页面以文件夹形式组织。"
作用
- 存放所有业务页面,每个子文件夹对应一个路由页面
- 文件夹名即为路由路径(如
pages/detail/detail→/pages/detail/detail) - 页面文件名建议与文件夹同名
关键规则
| 规则 | 说明 |
|---|---|
| 首页必须是第一个 | pages.json 中 pages 数组第一项为启动页 |
| 页面必须在 pages.json 注册 | 未注册的页面无法通过 navigateTo 跳转 |
| 分包页面放子包目录 | 如 pagesA/detail/detail,在 subPackages 中配置 |
| 页面内可包含私有组件 | 但推荐放到 components/ 统一管理 |
原生对照
| 平台 | 对应概念 |
|---|---|
| iOS | Storyboard/XIB + ViewController 文件组 |
| Android | Activity/Fragment + layout XML |
| 鸿蒙 | Page + Ability |
2.2 components/ --- 可复用组件目录
📖 官方定义: "存放可复用组件的目录。"
作用
- 存放跨页面复用的 UI 组件和业务组件
- 支持 easycom 自动导入(无需手动 import)
easycom 规范(重要!)
uni-app 内置了组件自动导入机制,符合以下目录结构的组件无需 import 即可直接使用:
css
components/
├── uni-card/
│ └── uni-card.vue ✅ 自动导入(组件名=文件夹名=文件名)
├── my-button/
│ └── my-button.vue ✅ 自动导入
├── CustomHeader.vue ❌ 不符合规范,需手动 import
└── nested/
└── deep-comp/
└── deep-comp.vue ✅ 支持多级嵌套
easycom 匹配规则: components/组件名称/组件名称.vue
也可在 pages.json 中自定义 easycom 规则:
bash
{
"easycom": {
"autoscan": true,
"custom": {
"^my-(.*)": "@/components/my-$1/my-$1.vue",
"^uni-(.*)": "@/uni_modules/uni-$1/components/uni-$1/uni-$1.vue"
}
}
}
组件分类建议
csharp
components/
├── base/ # 基础UI组件(按钮、输入框、弹窗)
├── business/ # 业务组件(商品卡片、订单列表项)
├── layout/ # 布局组件(导航栏、TabBar、侧边栏)
└── third-party/ # 第三方封装组件
2.3 static/ --- 静态资源目录
📖 官方定义: "存放不参与编译过程的静态资源。"
作用
- 存放图片、字体、视频、音频等二进制资源
- 原样复制到输出目录,不经过 webpack/vite 处理
- 可通过绝对路径
/static/xxx.png直接引用
⚠️ 关键注意事项
| 要点 | 说明 |
|---|---|
| 文件大小限制 | 小程序端主包 static 总大小 ≤ 2MB |
| 引用方式 | 必须用 /static/xxx 或 ../../static/xxx,不能用 @/static/ |
| CSS 中引用 | url(/static/images/bg.png) |
| JS 中引用 | '/static/images/avatar.png' |
| 不要放代码文件 | JS/CSS/JSON 等不应放在 static 中 |
| 大文件走 CDN | 超过 200KB 的图片建议上传 CDN |
为什么需要区分 static 和 assets?
| 维度 | static/ |
assets/ |
|---|---|---|
| 编译处理 | ❌ 原样复制 | ✅ 经打包工具处理 |
| 路径引用 | 绝对路径 /static/xxx |
相对路径/import |
| 哈希指纹 | ❌ 无 | ✅ 有(缓存友好) |
| Tree Shaking | ❌ 不支持 | ✅ 支持 |
| 适用场景 | TabBar图标、固定背景图 | 主题图片、SVG图标、样式文件 |
2.4 assets/ --- 编译资源目录(官方未强调,实战必备)
📖 官方未明确定义此目录,但这是社区和工程化实践中的标准约定。
作用
- 存放需要被构建工具(Vite/Webpack)处理的资源
- 支持 import 导入、SCSS 编译、PostCSS 处理、图片压缩等
典型结构
bash
assets/
├── styles/
│ ├── variables.scss # 全局 SCSS 变量(颜色、间距、字号)
│ ├── mixins.scss # SCSS Mixin
│ ├── reset.css # 样式重置
│ ├── theme-light.scss # 亮色主题
│ └── theme-dark.scss # 暗色主题
├── icons/ # SVG 图标(可用 unplugin-icons)
│ ├── home.svg
│ └── user.svg
└── images/ # 需要压缩/哈希处理的图片
├── banner.webp
└── logo.png
在 uni.scss 中引入
scss
/* uni.scss - 全局自动注入,无需手动 import */
@import '@/assets/styles/variables.scss';
@import '@/assets/styles/mixins.scss';
2.5 utils/ --- 工具函数目录
作用
- 存放纯逻辑的工具函数,与 UI 无关
- 通常不包含 Vue 响应式逻辑(那是 composables 的职责)
推荐结构
python
utils/
├── request.ts # uni.request 二次封装(拦截器、Token注入、错误处理)
├── auth.ts # Token 存取、登录态判断
├── storage.ts # uni.setStorageSync 类型安全封装
├── format.ts # 日期格式化、金额格式化、手机号脱敏
├── validate.ts # 表单校验规则
├── platform.ts # 平台判断工具(条件编译的运行时补充)
├── crypto.ts # 加密解密
└── constants.ts # 常量定义(枚举、配置值)
request.ts 示例(高频使用)
typescript
// utils/request.ts
interface RequestOptions {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: Record<string, any>;
header?: Record<string, string>;
showLoading?: boolean;
}
export const request = <T = any>(options: RequestOptions): Promise<T> => {
return new Promise((resolve, reject) => {
if (options.showLoading !== false) {
uni.showLoading({ title: '加载中...' });
}
uni.request({
url: `${import.meta.env.VITE_BASE_URL}${options.url}`,
method: options.method || 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
Authorization: `Bearer ${uni.getStorageSync('token')}`,
...options.header,
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data as T);
} else if (res.statusCode === 401) {
uni.reLaunch({ url: '/pages/login/login' });
reject(new Error('Unauthorized'));
} else {
uni.showToast({ title: '请求失败', icon: 'none' });
reject(res);
}
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' });
reject(err);
},
complete: () => {
if (options.showLoading !== false) {
uni.hideLoading();
}
},
});
});
};
2.6 api/ --- 接口定义目录(官方未提及,工程化必备)
作用
- 按业务模块集中管理所有后端接口
- 与
utils/request配合,实现接口调用的类型安全和统一管理
typescript
// api/user.ts
import { request } from '@/utils/request';
export interface UserInfo {
id: number;
nickname: string;
avatar: string;
}
export const getUserInfo = () =>
request<UserInfo>({ url: '/user/info' });
export const updateUserProfile = (data: Partial<UserInfo>) =>
request({ url: '/user/profile', method: 'PUT', data });
xml
<!-- 页面中使用 -->
<script setup lang="ts">
import { getUserInfo, type UserInfo } from '@/api/user';
const user = ref<UserInfo>();
onLoad(async () => {
user.value = await getUserInfo();
});
</script>
2.7 store/ --- 状态管理目录
作用
- 管理跨页面共享的全局状态
- 推荐使用 Pinia(Vue3)或 Vuex(Vue2)
bash
store/
├── index.ts # Store 实例创建与导出
├── modules/
│ ├── user.ts # 用户状态
│ ├── cart.ts # 购物车状态
│ └── settings.ts # 应用设置
└── types.ts # Store 相关类型定义
typescript
// store/modules/user.ts (Pinia)
import { defineStore } from 'pinia';
import { getUserInfo, type UserInfo } from '@/api/user';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null as UserInfo | null,
token: uni.getStorageSync('token') || '',
}),
getters: {
isLoggedIn: (state) => !!state.token,
displayName: (state) => state.userInfo?.nickname || '游客',
},
actions: {
async fetchUserInfo() {
this.userInfo = await getUserInfo();
},
logout() {
this.userInfo = null;
this.token = '';
uni.removeStorageSync('token');
uni.reLaunch({ url: '/pages/login/login' });
},
},
});
2.8 composables/ --- 组合式函数目录(Vue3 专属)
作用
- 封装可复用的响应式逻辑(区别于 utils 中的纯函数)
- 命名约定以
use开头
ini
// composables/usePagination.ts
import { ref, onMounted } from 'vue';
export function usePagination<T>(fetchFn: (page: number) => Promise<T[]>) {
const list = ref<T[]>([]) as Ref<T[]>;
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const loadMore = async () => {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const data = await fetchFn(page.value);
list.value.push(...data);
hasMore.value = data.length >= 20;
page.value++;
} finally {
loading.value = false;
}
};
const refresh = async () => {
page.value = 1;
list.value = [];
hasMore.value = true;
await loadMore();
};
return { list, loading, hasMore, loadMore, refresh };
}
2.9 types/ --- TypeScript 类型定义目录
作用
- 存放全局类型声明、API 响应类型、环境类型等
.d.ts文件会被 TS 编译器自动识别
typescript
// types/global.d.ts
declare namespace UniApp {
// 扩展 globalData 类型
interface GlobalData {
userInfo: UserInfo | null;
isDarkMode: boolean;
}
}
// types/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BASE_URL: string;
readonly VITE_APP_TITLE: string;
}
2.10 uni_modules/ --- uni-app 插件目录
📖 官方定义: "uni_modules 是 uni-app 的插件模块化规范,支持组件、JS SDK、云函数、原生插件的统一管理。"
作用
- 从 DCloud 插件市场 下载的插件自动安装到此目录
- 支持组件自动导入(easycom)、API 调用、原生能力扩展
典型插件
| 插件 | 用途 |
|---|---|
uni-popup |
弹出层 |
uni-icons |
图标库 |
z-paging |
高性能分页列表 |
uni-scss |
官方 SCSS 变量体系 |
luch-request |
HTTP 请求库 |
uni-read-pages |
读取 pages.json 配置 |
⚠️ 注意事项
- 不要手动修改
uni_modules内的插件源码(升级会被覆盖) - 如需定制,fork 后放入
components/自行维护 - 部分插件仅支持特定平台,使用前查阅兼容表
2.11 hybrid/ --- App 端本地 HTML 资源目录
📖 官方定义: "存放 App 端 web-view 加载的本地 HTML 文件。"
作用
- 仅在 App 端有效,用于
web-view组件加载本地网页 - H5/小程序端忽略此目录
bash
hybrid/
└── html/
├── agreement.html # 用户协议
├── privacy.html # 隐私政策
└── chart.html # ECharts 图表页
ini
<web-view src="/hybrid/html/agreement.html" />
2.12 nativeplugins/ --- App 原生插件目录
作用
- 存放自定义的原生插件(iOS/Android/鸿蒙原生代码)
- 用于调用 uni-app 未提供的原生能力
bash
nativeplugins/
└── MyFaceDetector/
├── ios/ # iOS 原生代码
├── android/ # Android 原生代码
├── harmony/ # 鸿蒙原生代码
└── package.json # 插件描述文件
💡 大多数原生需求可通过插件市场解决,仅在必要时自行开发。
2.13 platform/ --- 平台专属配置目录(CLI 项目)
📖 官方文档较少提及,这是 CLI 项目中用于存放各平台差异化配置的目录。
bash
platform/
├── app/
│ └── Info.plist # iOS 权限配置
├── mp-weixin/
│ └── project.config.json # 微信小程序项目配置
└── h5/
└── favicon.ico # H5 网站图标
三、核心配置文件详解
3.1 manifest.json --- 应用配置清单(最重要的配置文件)
📖 官方定义: "应用的配置文件,用于指定应用名称、appid、版本、权限、SDK 配置等。"
核心配置项
swift
{
"name": "我的应用", // 应用名称
"appid": "__UNI__XXXXXXX", // DCloud 分配的唯一ID
"description": "应用描述",
"versionName": "1.0.0", // 显示版本号
"versionCode": "100", // 内部版本号(整数)
// ✅ App 端配置
"app-plus": {
"usingComponents": true,
"splashscreen": { // 启动页配置
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true
},
"modules": { // 原生模块开关
"OAuth": {}, // 登录
"Push": {}, // 推送
"Maps": {}, // 地图
"Payment": {} // 支付
},
"distribute": {
"android": {
"permissions": [ // Android 权限声明
"<uses-permission android:name="android.permission.CAMERA"/>"
],
"minSdkVersion": 21,
"targetSdkVersion": 33
},
"ios": {
"privacyDescription": { // iOS 权限用途说明
"NSCameraUsageDescription": "用于拍照上传头像",
"NSLocationWhenInUseUsageDescription": "用于定位附近门店"
}
}
}
},
// ✅ 微信小程序配置
"mp-weixin": {
"appid": "wxXXXXXXXXXXXXXX",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "用于获取您的位置信息"
}
}
},
// ✅ H5 配置
"h5": {
"title": "我的应用",
"router": {
"mode": "history", // hash | history
"base": "/"
},
"devServer": {
"port": 8080,
"proxy": { // 开发代理
"/api": {
"target": "http://localhost:3000",
"changeOrigin": true
}
}
},
"optimization": {
"treeShaking": {
"enable": true // 按需引入 uni API
}
}
},
// ✅ 鸿蒙 NEXT 配置
"app-harmony": {
"package": "com.example.myapp",
"icons": {
"foreground": "static/harmony/icon_foreground.png",
"background": "static/harmony/icon_background.png"
}
}
}
原生对照
| uni-app manifest.json | iOS | Android | 鸿蒙 |
|---|---|---|---|
name |
CFBundleDisplayName | app_name (strings.xml) | bundleName |
appid |
Bundle Identifier | applicationId | bundleName |
versionName/Code |
CFBundleShortVersionString/CFBundleVersion | versionName/versionCode | versionName/versionCode |
distribute.ios.privacyDescription |
Info.plist NSxxxUsageDescription | --- | module.json5 requestPermissions |
distribute.android.permissions |
--- | AndroidManifest.xml | module.json5 requestPermissions |
mp-weixin.appid |
--- | --- | --- |
3.2 pages.json --- 页面路由与窗口配置
📖 官方定义: "对 uni-app 进行全局配置,决定页面文件的路径、窗口表现、导航条样式等。"
完整配置示例
json
{
// ✅ 全局窗口配置
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "我的应用",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F5F5",
"backgroundTextStyle": "dark",
"app-plus": {
"titleNView": false // App端隐藏原生导航栏
}
},
// ✅ 页面路由列表(第一项为首页)
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情",
"navigationStyle": "custom" // 自定义导航栏
}
}
],
// ✅ TabBar 配置
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"pagePath": "pages/mine/mine",
"text": "我的",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine-active.png"
}
]
},
// ✅ 分包配置
"subPackages": [
{
"root": "pagesA",
"pages": [
{ "path": "order/list", "style": { "navigationBarTitleText": "订单列表" } }
]
}
],
// ✅ 预下载分包
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pagesA"]
}
},
// ✅ easycom 自定义规则
"easycom": {
"autoscan": true,
"custom": {}
}
}
3.3 uni.scss --- 全局 SCSS 变量文件
📖 官方定义: "uni-app 内置的常用样式变量,会自动注入到每个 scss 文件中。"
作用
- 定义全局设计令牌(Design Tokens)
- 无需手动 @import ,编译器自动注入到每个
<style lang="scss">中 - 可直接使用 uni-ui 内置变量
css
/* uni.scss */
/* 品牌色 */
$brand-primary: #007AFF;
$brand-success: #4CD964;
$brand-warning: #F0AD4E;
$brand-error: #DD524D;
/* 文字色 */
$text-main: #333333;
$text-secondary: #666666;
$text-placeholder: #999999;
/* 间距 */
$spacing-sm: 8rpx;
$spacing-md: 16rpx;
$spacing-lg: 32rpx;
/* 圆角 */
$radius-sm: 4rpx;
$radius-md: 8rpx;
$radius-lg: 16rpx;
/* uni-ui 内置变量可直接使用 */
/* $uni-color-primary, $uni-font-size-base 等 */
3.4 main.ts --- 应用入口文件
javascript
// main.ts
import { createSSRApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
export function createApp() {
const app = createSSRApp(App);
// 注册 Pinia
const pinia = createPinia();
app.use(pinia);
// 注册全局组件(非 easycom 的)
// app.component('MyGlobalComp', MyGlobalComp);
// 注册全局指令
// app.directive('focus', { mounted: (el) => el.focus() });
return { app };
}
⚠️ 注意: uni-app 使用
createSSRApp而非createApp,这是为了支持服务端渲染和多实例隔离。
3.5 index.html --- H5 模板文件
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.ts"></script>
</body>
</html>
💡
<!--app-context-->和<!--app-html-->是 SSR 占位符,开发时可忽略。
3.6 .env 环境变量文件(官方未详述,实战必备)
ini
# .env.development
VITE_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=我的应用(开发)
# .env.production
VITE_BASE_URL=https://api.example.com
VITE_APP_TITLE=我的应用
在代码中使用:
ini
const baseUrl = import.meta.env.VITE_BASE_URL;
⚠️ 只有以
VITE_开头的变量才会暴露给客户端代码。
四、目录结构设计原则总结
| 原则 | 说明 |
|---|---|
| 按职责分层 | pages(视图)、api(接口)、store(状态)、utils(工具)、composables(逻辑) 各司其职 |
| 就近原则 | 仅某页面使用的组件/工具放在页面同级目录 |
| 命名规范 | 文件夹 kebab-case,组件 PascalCase,工具 camelCase |
| 平台隔离 | 用条件编译 #ifdef 而非分目录管理差异代码 |
| 资源分离 | 编译资源放 assets/,静态资源放 static/ |
| 类型先行 | API 响应类型、Store 类型、全局类型统一放 types/ |
| 插件优先 | 先查插件市场,避免重复造轮子 |
五、原生工程师快速映射表
| 原生概念 | uni-app 对应 | 位置 |
|---|---|---|
| Xcode Project / DevEco Module | 项目根目录 | / |
| Info.plist / module.json5 | manifest.json | /manifest.json |
| Navigation Controller / Router | pages.json | /pages.json |
| Storyboard / Layout XML | pages/*.vue | /pages/ |
| Custom View / Component | components/*.vue | /components/ |
| Assets.xcassets / resource | static/ + assets/ | /static/ /assets/ |
| AppDelegate / EntryAbility | App.vue | /App.vue |
| Singleton / DataManager | store/ | /store/ |
| NetworkManager / Retrofit | utils/request.ts + api/ | /utils/ /api/ |
| Build Settings / build.gradle | vite.config.ts | /vite.config.ts |
| CocoaPods / ohpm | uni_modules/ + package.json | /uni_modules/ |
| Entitlements / Permissions | manifest.json distribute | /manifest.json |
💡 一句话总结: uni-app 的目录结构是 "Vue 前端工程化 + 原生多端配置" 的融合体。
pages/components/store/composables来自 Vue 生态最佳实践,manifest.json/pages.json/static/hybrid/nativeplugins来自多端原生适配需求。理解了这两条线索,整个项目结构就一目了然了。
📚 参考资料: