UniApp 完整知识总结与使用教程
目录
- [uni-app 概述](#uni-app 概述)
- 跨端原理与架构
- 开发环境搭建
- 项目目录结构
- 核心配置文件详解
- 5.1 pages.json
- 5.2 manifest.json
- 5.3 App.vue
- 5.4 main.js
- 5.5 uni.scss
- 页面与组件
- 6.1 页面文件结构
- 6.2 页面生命周期
- 6.3 应用生命周期
- 6.4 组件生命周期
- 模板语法与数据绑定
- 路由与页面跳转
- 基础组件全览
- 样式与布局
- [uni API 常用接口](#uni API 常用接口)
- 11.1 网络请求
- 11.2 数据缓存
- 11.3 媒体与文件
- 11.4 设备信息
- 11.5 界面交互
- 11.6 导航栏操作
- 条件编译
- 状态管理(Pinia)
- 自定义组件开发
- 网络请求封装与拦截器
- [nvue 原生渲染页面](#nvue 原生渲染页面)
- [uniCloud 云开发](#uniCloud 云开发)
- [插件市场与 uni_modules](#插件市场与 uni_modules)
- 打包发布全流程
- 性能优化实战
- 常见问题与跨端兼容技巧
- [uni-app x 新架构简介](#uni-app x 新架构简介)
- 工程化最佳实践
- [附录:常用 API 速查 & 官方资源](#附录:常用 API 速查 & 官方资源)
1. uni-app 概述
1.1 什么是 uni-app?
uni-app 是由中国公司 DCloud(数字天堂) 开发并维护的一款开源跨平台前端应用开发框架,基于 Vue.js 技术栈。
其核心价值是:编写一套代码,可发布到 iOS、Android、HarmonyOS NEXT、Web(H5)以及各类小程序(微信、支付宝、百度、头条/抖音、飞书、QQ、快手、钉钉、淘宝、360、京东、小红书等)和快应用等十余个平台。
┌──────────────────────────────────────────────────────────────┐
│ uni-app 代码(一套) │
└──────────────────────┬───────────────────────────────────────┘
│ 编译
┌──────────────┼──────────────────────────┐
│ │ │
┌────▼────┐ ┌─────▼──────┐ ┌──────▼──────┐
│ App端 │ │ 小程序端 │ │ H5/Web端 │
│iOS/Android│ │微信/支付宝 │ │ PC+移动浏览器│
│HarmonyOS │ │百度/抖音...│ │ │
└──────────┘ └────────────┘ └─────────────┘
1.2 为什么选择 uni-app?
| 对比维度 | 传统多端开发 | uni-app |
|---|---|---|
| 语言成本 | 需学 Swift/OC、Java/Kotlin、JS | 只需 Vue.js + 小程序思维 |
| 开发成本 | 多套代码库、多个团队 | 一套代码、一个团队 |
| 维护成本 | N 套代码分别维护 | 统一维护 |
| 上线周期 | 各平台独立排期 | 一次开发同步发布 |
| 生态 | 各平台各自生态 | 丰富的 uni 生态(插件市场 10 万+) |
1.3 核心特性
- Vue.js 技术栈:支持 Vue2 / Vue3,Composition API,TypeScript,Vite 构建
- 多端覆盖:13+ 平台一键发布,覆盖国内主流平台
- 组件规范:标签靠近小程序规范,API 靠近微信小程序规范
- 条件编译:通过注释实现精确的跨端差异化代码控制
- easycom:组件零配置自动引入
- uniCloud:内置 Serverless 云开发能力(云函数、云数据库、云存储)
- 插件市场:10 万+ 插件,覆盖各类业务场景
- 热更新:App 端支持 wgt 热更新,无需过审发版
- 性能优化:支持 nvue 原生渲染,renderjs,bindingx 等高性能方案
1.4 支持平台一览
| 平台类型 | 具体平台 |
|---|---|
| App | iOS、Android、HarmonyOS NEXT |
| Web | PC 浏览器、H5 移动端 |
| 微信生态 | 微信小程序、微信公众号 H5 |
| 阿里生态 | 支付宝小程序、钉钉小程序、淘宝小程序 |
| 字节跳动 | 抖音小程序(原头条小程序)、飞书小程序 |
| 百度 | 百度小程序 |
| 其他 | QQ 小程序、快手小程序、360 小程序、京东小程序、小红书小程序、快应用 |
1.5 数据规模(2025)
uni-app 吸引了超过 400 万开发者 ,有数十万应用案例,累计覆盖超过 6.5 亿月活跃手机用户。
2. 跨端原理与架构
2.1 整体架构
┌─────────────────────────────────────────────────────────────────┐
│ 开发者代码层 │
│ Vue SFC 组件 | HQL(类 SQL) | 条件编译代码 │
└──────────────────────────┬──────────────────────────────────────┘
│ 编译器(Compiler)
┌──────────────────────────▼──────────────────────────────────────┐
│ uni-app 编译层 │
│ webpack / Vite 构建工具 | 模板编译 | 条件编译处理 │
└──────────────────────────┬──────────────────────────────────────┘
│
┌─────────────────┼───────────────────────┐
│ │ │
┌────────▼────────┐ ┌──────▼──────┐ ┌─────────────▼───────────┐
│ App 端 Runtime│ │ Web 端 │ │ 小程序端 │
│ │ │ │ │ │
│ ┌─────────────┐ │ │ 标准浏览器 │ │ 各小程序引擎 │
│ │ JS 引擎 │ │ │ 环境 │ │ (微信/支付宝等) │
│ │ Android: V8 │ │ │ │ │ │
│ │ iOS: JSCore │ │ │ │ │ │
│ └─────────────┘ │ │ │ │ │
│ ┌─────────────┐ │ │ │ │ │
│ │ 渲染引擎 │ │ │ │ │ │
│ │ .vue→webview│ │ │ │ │ │
│ │ .nvue→原生 │ │ │ │ │ │
│ └─────────────┘ │ │ │ │ │
└─────────────────┘ └─────────────┘ └───────────────────────────┘
2.2 Runtime 三大核心
uni-app runtime 包括三部分:基础框架、组件、API。
- 基础框架:包含语法、数据驱动、全局文件、应用管理、页面管理、JS 引擎、渲染和排版引擎等
- 组件 :跨端统一的 UI 组件,如
<view>、<text>、<image>等 - API :跨端统一的
uni.xxx()接口封装
2.3 逻辑层与视图层
uni-app 在非 H5 端(App、小程序)运行时,架构上分为逻辑层 和视图层两个部分:
┌──────────────────────────────────────────────────────────┐
│ 逻辑层(JS 层) │
│ 业务逻辑 / 数据处理 / API 调用 │
│ App-Android: V8 引擎 | App-iOS: JSCore 引擎 │
└───────────────────────┬──────────────────────────────────┘
│ 数据通信(有性能损耗)
┌───────────────────────▼──────────────────────────────────┐
│ 视图层(渲染层) │
│ .vue 页面 → WebView 渲染(App/小程序) │
│ .nvue 页面 → 原生渲染(Weex 引擎改造) │
└──────────────────────────────────────────────────────────┘
⚠️ 重要:逻辑层和视图层之间的通信存在性能损耗,这是理解 uni-app 性能瓶颈的关键。
2.4 App 端的两套渲染引擎
| 渲染方式 | 文件类型 | 原理 | 特点 |
|---|---|---|---|
| WebView 渲染(vue) | .vue |
类似小程序原理 | 兼容性好,CSS 支持完整 |
| 原生渲染(nvue) | .nvue |
类似 React Native | 性能更好,CSS 受限 |
3. 开发环境搭建
3.1 方式一:HBuilderX(官方 IDE,推荐新手)
HBuilderX 是 DCloud 官方提供的 IDE,对 uni-app 支持最完整:
bash
# 1. 下载 HBuilderX
# 访问 https://www.dcloud.io/hbuilderx.html 下载对应系统版本
# 2. 安装完成后,打开 HBuilderX
# 3. 新建项目:文件 → 新建 → 项目
# 选择"uni-app"类型,选择模板
# 4. 运行:工具栏 → 运行 → 选择目标平台
HBuilderX 快捷键:
| 快捷键 | 功能 |
|---|---|
Ctrl+R |
运行项目 |
Ctrl+U |
发行/打包 |
Ctrl+Alt+/ |
生成条件编译注释 |
Ctrl+P |
快速文件搜索 |
Alt+←/→ |
跳转到上/下一个编辑位置 |
3.2 方式二:CLI + VS Code(推荐团队协作)
bash
# 1. 安装 Node.js(推荐 v18+)
node -v
npm -v
# 2. 全局安装 vue-cli
npm install -g @vue/cli
# 3. 创建 uni-app 项目(Vue3 + Vite 版本,推荐)
npx degit dcloudio/uni-preset-vue#vite-ts my-project
# 或者使用默认 Vue3 模板(非 TypeScript)
npx degit dcloudio/uni-preset-vue#vite my-project
# 4. 进入项目目录并安装依赖
cd my-project
npm install
# 5. 运行到各平台
npm run dev:h5 # 运行到 H5(浏览器)
npm run dev:mp-weixin # 运行到微信小程序
npm run dev:mp-alipay # 运行到支付宝小程序
npm run dev:app # 运行到 App(需配合 HBuilderX)
# 6. 打包各平台
npm run build:h5
npm run build:mp-weixin
npm run build:app-plus
VS Code 推荐插件:
uni-helper:uni-app 代码提示和补全uni-app-snippets:uni-app 代码片段Volar:Vue3 语言支持ESLint:代码检查
3.3 创建 Vue3 + TypeScript 工程化项目(推荐)
bash
# 使用 unibest 脚手架(最受欢迎的社区脚手架)
npm create unibest@latest my-project
# 选择模板后进入项目
cd my-project
npm install
# 运行
npm run dev:h5
unibest 内置:Vue3 + Vite5 + TypeScript + Pinia + UnoCSS + 请求封装 + 路由守卫
4. 项目目录结构
my-uni-app/
├── pages/ # 页面文件目录(必须)
│ ├── index/
│ │ └── index.vue # 首页
│ └── user/
│ └── user.vue # 用户页
├── static/ # 静态资源目录(图片、字体等)
│ ├── logo.png
│ └── mp-weixin/ # 微信小程序专属静态资源(条件编译)
│ └── icon.png
├── components/ # 公共组件目录(easycom 自动引入)
│ └── my-button/
│ └── my-button.vue
├── stores/ # Pinia 状态管理(Vue3)
│ └── user.js
├── utils/ # 工具函数目录
│ ├── request.js # 请求封装
│ └── common.js
├── api/ # API 接口定义目录
│ └── user.js
├── uni_modules/ # uni-app 插件模块(uni_modules 规范)
├── platforms/ # 平台专有代码目录(可选)
│ ├── app-plus/ # App 专有代码
│ └── mp-weixin/ # 微信小程序专有代码
├── App.vue # 应用入口组件(必须)
├── main.js # 应用入口 JS(必须)
├── manifest.json # 应用配置文件(必须)
├── pages.json # 页面路由配置(必须)
├── uni.scss # 全局 SCSS 变量(可选)
└── package.json # npm 包配置
5. 核心配置文件详解
5.1 pages.json(最重要的配置文件)
pages.json 是 uni-app 的全局路由配置文件 ,相当于微信小程序的 app.json。
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#007AFF",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}
},
{
"path": "pages/user/user",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "My App",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F8F8F8",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,
"usingComponents": {}
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/user/user",
"iconPath": "static/tab/user.png",
"selectedIconPath": "static/tab/user-active.png",
"text": "我的"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "开发调试:订单详情页",
"path": "pages/order/detail",
"query": "id=123"
}
]
},
"subPackages": [
{
"root": "subpkg",
"pages": [
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情"
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["subpkg"]
}
},
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
}
}
}
pages.json 核心配置项说明
| 配置项 | 类型 | 说明 |
|---|---|---|
pages |
Array | 必填,页面路由数组,第一个页面为首页 |
globalStyle |
Object | 全局页面样式,可被页面级 style 覆盖 |
tabBar |
Object | 底部 Tab 栏配置,最少 2 个、最多 5 个 |
condition |
Object | 开发调试时的启动模式,仅开发期有效 |
subPackages |
Array | 分包配置(减小主包体积,小程序必备) |
preloadRule |
Object | 分包预加载配置 |
easycom |
Object | 组件自动引入规则 |
tabBar 配置注意事项
⚠️ tabBar 规则:
- 最少 2 个、最多 5 个 tab
- tabBar 中的页面必须在 pages 数组中定义
- tabBar 页面切换时不会触发页面的 onLoad,只触发 onShow
- position 设为 top 时不显示图标(仅显示文字)
5.2 manifest.json(应用配置)
json
{
"name": "My App",
"appid": "__UNI__XXXXXXX",
"description": "我的应用描述",
"versionName": "1.0.0",
"versionCode": 100,
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-compiler",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {
"Payment": {},
"Share": {},
"Push": {}
},
"distribute": {
"android": {
"packagename": "com.example.myapp",
"keystore": "keystore/release.keystore",
"password": "YOUR_PASSWORD",
"aliasname": "myapp",
"google": {},
"abiFilters": ["armeabi-v7a", "arm64-v8a"]
},
"ios": {
"bundleid": "com.example.myapp",
"privacyDescription": {}
}
}
},
"h5": {
"title": "My App",
"router": {
"mode": "history",
"base": "/h5/"
},
"devServer": {
"port": 3000,
"https": false,
"proxy": {
"/api": {
"target": "http://localhost:8080",
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
},
"publicPath": "./"
},
"mp-weixin": {
"appid": "wx_YOUR_APPID",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"requiredBackgroundModes": ["audio"],
"plugins": {}
},
"mp-alipay": {
"appid": "YOUR_ALIPAY_APPID"
}
}
5.3 App.vue(应用根组件)
vue
<script>
export default {
// 应用生命周期
onLaunch(options) {
// 应用第一次启动时触发(全局只触发一次)
console.log('App Launch', options)
// options.path 启动路径
// options.scene 启动场景值
// options.query 启动参数
},
onShow(options) {
// 应用从后台进入前台时触发(或第一次启动)
console.log('App Show')
},
onHide() {
// 应用从前台进入后台时触发
console.log('App Hide')
},
onError(err) {
// 应用抛出错误时触发
console.error('App Error', err)
},
onUniNViewMessage(e) {
// 对 nvue 页面发送消息进行监听
console.log('nvue message:', e.data)
},
onUnhandledRejection(e) {
// 对未处理的 Promise 拒绝事件监听
console.error('Unhandled Rejection:', e)
}
}
</script>
<style lang="scss">
/* 全局样式,可被所有页面和组件访问 */
/* 引入 uni.scss 后,这里可以使用其中定义的 SCSS 变量 */
@import "@/uni.scss";
/* 重置基础样式 */
page {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
font-size: 28rpx;
color: #333;
background-color: #f5f5f5;
}
/* 注意:App.vue 中的 style 不能有 scoped,否则无法全局生效 */
</style>
5.4 main.js(应用入口)
javascript
// Vue3 + Composition API 方式
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
// 导入全局组件(非 easycom 的情况)
// import MyComponent from '@/components/MyComponent.vue'
export function createApp() {
const app = createSSRApp(App)
// 注册 Pinia 状态管理
const pinia = createPinia()
app.use(pinia)
// 全局属性
app.config.globalProperties.$baseUrl = 'https://api.example.com'
// 全局组件注册
// app.component('MyComponent', MyComponent)
// 全局错误处理
app.config.errorHandler = (err, vm, info) => {
console.error('Global Error:', err, info)
}
return { app, pinia }
}
5.5 uni.scss(全局 SCSS 变量)
scss
/* uni.scss 中定义的变量可在所有页面、组件的 style 中直接使用 */
/* 无需 @import,uni-app 会自动注入 */
/* 主题色 */
$primary-color: #007AFF;
$success-color: #4cd964;
$warning-color: #f0ad4e;
$error-color: #dd524d;
/* 文字颜色 */
$text-primary: #333333;
$text-secondary: #666666;
$text-placeholder: #999999;
$text-disabled: #cccccc;
/* 背景色 */
$bg-color: #f5f5f5;
$bg-white: #ffffff;
/* 边框 */
$border-color: #e5e5e5;
$border-radius: 8rpx;
/* 尺寸 */
$nav-height: 44px;
$tab-height: 50px;
/* 动画 */
$animation-duration: 0.3s;
6. 页面与组件
6.1 页面文件结构
uni-app 页面遵循 Vue 单文件组件(SFC)规范:
vue
<template>
<!-- 模板:只能有一个根节点(Vue2) / 可以多根节点(Vue3) -->
<!-- uni-app 使用小程序标签,不用 div,用 view -->
<view class="container">
<text class="title">{{ title }}</text>
<button @click="handleClick" type="primary">点击按钮</button>
<image
src="/static/logo.png"
mode="aspectFit"
:style="{ width: '200rpx', height: '200rpx' }"
/>
<scroll-view
scroll-y
class="list"
@scrolltolower="loadMore"
>
<view
v-for="(item, index) in list"
:key="item.id"
class="list-item"
@click="goDetail(item.id)"
>
{{ item.name }}
</view>
</scroll-view>
</view>
</template>
<script setup>
// Vue3 Composition API + <script setup> 语法(推荐)
import { ref, reactive, computed, onMounted } from 'vue'
import { onLoad, onShow, onHide, onReachBottom, onPullDownRefresh } from '@dcloudio/uni-app'
// 响应式数据
const title = ref('Hello uni-app')
const list = ref([])
const loading = ref(false)
// 计算属性
const listLength = computed(() => list.value.length)
// 方法
const handleClick = () => {
console.log('按钮被点击')
title.value = '已点击'
}
const goDetail = (id) => {
uni.navigateTo({
url: `/pages/detail/detail?id=${id}`
})
}
const loadData = async () => {
try {
const res = await uni.request({
url: 'https://api.example.com/list',
method: 'GET'
})
list.value = res.data.list || []
} catch (e) {
console.error(e)
}
}
const loadMore = () => {
// 上拉加载更多逻辑
}
// 页面生命周期(uni-app 特有)
onLoad((options) => {
// 页面加载,options 为页面参数
console.log('页面加载,参数:', options)
loadData()
})
onShow(() => {
console.log('页面显示')
})
onHide(() => {
console.log('页面隐藏')
})
// 下拉刷新(需在 pages.json 中配置 enablePullDownRefresh: true)
onPullDownRefresh(async () => {
await loadData()
uni.stopPullDownRefresh() // 停止下拉刷新动画
})
// 上拉触底加载
onReachBottom(() => {
loadMore()
})
</script>
<style lang="scss" scoped>
/* scoped 让样式只作用于当前组件 */
.container {
padding: 20rpx;
min-height: 100vh;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: $text-primary; /* 使用 uni.scss 中的全局变量 */
}
.list {
height: 600rpx;
}
.list-item {
padding: 20rpx;
border-bottom: 1rpx solid $border-color;
&:active {
background-color: #f0f0f0;
}
}
</style>
6.2 页面生命周期
uni-app 页面生命周期函数(仅在页面有效,组件中使用需配合 onPageShow 等改名版本):
| 函数 | 说明 | 触发时机 |
|---|---|---|
onLoad(options) |
页面加载 | 页面被创建时,只触发一次 ;options 包含页面参数 |
onShow |
页面显示 | 页面显示/从后台切换回前台时(每次显示都触发) |
onReady |
页面初次渲染完成 | 页面第一次渲染完毕(首次触发一次) |
onHide |
页面隐藏 | 页面被遮挡/进入后台时 |
onUnload |
页面卸载 | 页面被销毁时(navigateBack 后触发) |
onPullDownRefresh |
下拉刷新 | 需开启 enablePullDownRefresh |
onReachBottom |
上拉触底 | 页面滚动到底部时 |
onPageScroll(e) |
页面滚动 | 页面滚动时,e.scrollTop 为滚动距离 |
onBackPress(e) |
返回前触发 | App/H5 独有,返回 true 可阻止默认返回行为 |
onShareAppMessage(e) |
分享给好友 | 点击分享给微信好友时 |
onShareTimeline |
分享到朋友圈 | 微信小程序 |
onAddToFavorites |
收藏 | 微信小程序 |
onNavigationBarButtonTap |
原生标题栏按钮点击 | App 端原生导航栏右侧按钮 |
onTabItemTap |
Tab 点击 | tabBar 页面中,点击当前 tab 时 |
生命周期执行顺序:
onLoad → onShow → onReady → ... → onHide → onShow → ... → onUnload
6.3 应用生命周期
在 App.vue 中的应用级生命周期:
| 函数 | 说明 |
|---|---|
onLaunch |
应用初始化完成(全局只触发一次) |
onShow |
应用进入前台 |
onHide |
应用进入后台 |
onError(err) |
应用发生错误 |
onPageNotFound(e) |
页面不存在时触发 |
onUnhandledRejection(e) |
未处理的 Promise 拒绝 |
6.4 组件生命周期
组件遵循标准 Vue 组件生命周期:
Vue2: beforeCreate → created → beforeMount → mounted → beforeUpdate → updated → beforeDestroy → destroyed
Vue3: setup → onBeforeMount → onMounted → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted
⚠️ 注意区分:页面生命周期(
onLoad、onShow等)只在页面中使用,组件中不支持。组件中应使用 Vue 标准组件生命周期。
7. 模板语法与数据绑定
7.1 基础数据绑定
vue
<template>
<!-- 文本插值 -->
<text>{{ message }}</text>
<text>{{ count + 1 }}</text>
<text>{{ ok ? '是' : '否' }}</text>
<!-- 属性绑定(v-bind 或简写 :) -->
<image :src="imageUrl" />
<view :class="['base-class', isActive ? 'active' : '']"></view>
<view :style="{ color: textColor, fontSize: fontSize + 'rpx' }"></view>
<!-- 条件渲染 -->
<view v-if="score >= 90">优秀</view>
<view v-else-if="score >= 60">及格</view>
<view v-else>不及格</view>
<!-- v-show(仅切换 display,不销毁元素) -->
<view v-show="isVisible">可见</view>
<!-- 列表渲染 -->
<view v-for="(item, index) in list" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</view>
<!-- 对象遍历 -->
<view v-for="(value, key, index) in obj" :key="key">
{{ key }}: {{ value }}
</view>
<!-- 事件绑定(v-on 或简写 @) -->
<button @click="handleClick">点击</button>
<button @click="handleClick($event, 'arg')">带参数</button>
<input @input="onInput" @change="onChange" @blur="onBlur" />
<!-- 双向绑定 -->
<input v-model="inputValue" placeholder="请输入" />
<switch v-model="isChecked" />
<slider v-model="sliderValue" min="0" max="100" />
<!-- 模板引用 -->
<view ref="myRef">引用元素</view>
</template>
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
const message = ref('Hello')
const count = ref(0)
const isActive = ref(false)
const imageUrl = ref('/static/logo.png')
const list = ref([{ id: 1, name: 'Item 1' }])
const inputValue = ref('')
// reactive 创建响应式对象
const user = reactive({
name: 'Alice',
age: 25
})
// 计算属性
const fullName = computed(() => `${user.name} - ${user.age}岁`)
// 侦听器
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 深度侦听
watch(user, (newVal) => {
console.log('user 变化了', newVal)
}, { deep: true })
// watchEffect(立即执行,依赖自动追踪)
watchEffect(() => {
console.log('count is:', count.value)
})
// 模板引用
import { onMounted } from 'vue'
const myRef = ref(null)
onMounted(() => {
console.log('DOM 引用:', myRef.value)
})
</script>
7.2 Class 与 Style 绑定
vue
<template>
<!-- Class 对象语法 -->
<view :class="{ active: isActive, disabled: isDisabled }"></view>
<!-- Class 数组语法 -->
<view :class="[baseClass, isActive ? 'active' : '', customClass]"></view>
<!-- Style 对象语法 -->
<view :style="{ color: '#007AFF', fontSize: '28rpx', marginTop: marginTop + 'rpx' }"></view>
<!-- Style 数组语法 -->
<view :style="[baseStyles, overrideStyles]"></view>
</template>
8. 路由与页面跳转
8.1 路由跳转 API
uni-app 提供 5 种路由跳转方式:
javascript
// 1. navigateTo:保留当前页面,跳转到新页面(最常用)
// 页面栈最多 10 层
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=hello',
animationType: 'pop-in', // 可选,动画效果
animationDuration: 300, // 可选,动画时长
success(res) {
console.log('跳转成功')
// 可通过 res.eventChannel 与新页面通信
res.eventChannel.emit('acceptDataFromOpenerPage', { data: '来自上一页' })
},
fail(err) {
console.error('跳转失败', err)
}
})
// 2. redirectTo:关闭当前页面,跳转到新页面(不保留当前页)
uni.redirectTo({
url: '/pages/login/login'
})
// 3. reLaunch:关闭所有页面,打开新页面(清空页面栈)
uni.reLaunch({
url: '/pages/index/index'
})
// 4. switchTab:跳转到 tabBar 页面(只能跳 tabBar 页面)
uni.switchTab({
url: '/pages/index/index'
})
// 5. navigateBack:返回上一页(或返回多层)
uni.navigateBack({
delta: 1, // 返回的层数,默认 1
animationType: 'pop-out',
animationDuration: 300
})
// 简写
uni.navigateBack()
8.2 接收路由参数
vue
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
const id = ref('')
const name = ref('')
onLoad((options) => {
// options 即为 URL 中的查询参数对象
id.value = options.id // '123'
name.value = decodeURIComponent(options.name) // 需要解码
console.log('接收到的参数:', options)
})
</script>
8.3 navigator 组件跳转
vue
<template>
<!-- 等价于 uni.navigateTo -->
<navigator url="/pages/detail/detail?id=1" hover-class="navigator-hover">
跳转到详情页
</navigator>
<!-- 等价于 uni.redirectTo -->
<navigator url="/pages/login/login" open-type="redirect">
重定向到登录页
</navigator>
<!-- 等价于 uni.navigateBack -->
<navigator open-type="navigateBack" delta="1">返回上一页</navigator>
<!-- 等价于 uni.switchTab -->
<navigator url="/pages/index/index" open-type="switchTab">
切换到首页 Tab
</navigator>
</template>
8.4 页面间通信
方式一:URL 参数(简单数据)
javascript
// 发送方
uni.navigateTo({ url: '/pages/b?name=Alice&age=25' })
// 接收方
onLoad((options) => {
const { name, age } = options // { name: 'Alice', age: '25' }
})
方式二:EventChannel(navigateTo 时,父传子)
javascript
// 父页面(发送方)
uni.navigateTo({
url: '/pages/child',
success(res) {
res.eventChannel.emit('fromParent', { data: '来自父页面的数据' })
}
})
// 子页面(接收方)
onLoad(() => {
const eventChannel = getCurrentInstance().proxy.$scope.eventChannel
eventChannel.on('fromParent', (data) => {
console.log('收到父页面数据:', data)
})
})
方式三:globalData(简单全局状态)
javascript
// 设置
getApp().globalData.userInfo = { name: 'Alice' }
// 获取
const userInfo = getApp().globalData.userInfo
方式四:uni. e m i t / u n i . emit / uni. emit/uni.on(全局事件总线)
javascript
// 页面 A(触发事件)
uni.$emit('updateCart', { count: 5 })
// 页面 B(监听事件,通常在 onLoad 或 onShow 中)
uni.$on('updateCart', (data) => {
console.log('购物车数量:', data.count)
})
// 页面销毁时取消监听(重要!防止内存泄漏)
uni.$off('updateCart')
// 只监听一次
uni.$once('updateCart', (data) => {
console.log('只触发一次:', data)
})
方式五:Pinia 状态管理(推荐)
javascript
// 见第 13 节
8.5 页面栈操作
javascript
// 获取当前页面栈
const pages = getCurrentPages()
console.log('当前页面栈深度:', pages.length)
// 获取上一个页面实例,并调用其方法
const prevPage = pages[pages.length - 2]
prevPage.$vm.refreshData() // 调用上一页的方法
9. 基础组件全览
9.1 视图容器
| 组件 | 说明 | 重要属性 |
|---|---|---|
<view> |
块级容器(相当于 div) | hover-class、hover-stay-time |
<scroll-view> |
可滚动视图区域 | scroll-y/scroll-x、@scrolltolower、scroll-top |
<swiper> |
滑块视图容器(轮播图) | indicator-dots、autoplay、circular、current |
<swiper-item> |
滑块容器(swiper 子组件) | --- |
<cover-view> |
覆盖在原生组件上的文本 | 用于覆盖 map/video/canvas 等原生组件 |
<cover-image> |
覆盖在原生组件上的图片 | --- |
<movable-area> |
可移动区域 | --- |
<movable-view> |
在 movable-area 内可移动的视图 | direction、x/y |
vue
<!-- scroll-view 完整示例 -->
<scroll-view
scroll-y
:scroll-top="scrollTop"
@scrolltolower="loadMore"
@scrolltoupper="refresh"
@scroll="onScroll"
:style="{ height: '80vh' }"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<view v-for="item in list" :key="item.id">{{ item.name }}</view>
</scroll-view>
<!-- swiper 轮播图示例 -->
<swiper
indicator-dots
autoplay
:interval="3000"
:duration="500"
circular
@change="onSwiperChange"
>
<swiper-item v-for="img in banners" :key="img.id">
<image :src="img.url" mode="widthFix" @click="goLink(img.url)" />
</swiper-item>
</swiper>
9.2 基础内容
| 组件 | 说明 | 重要属性 |
|---|---|---|
<text> |
文本(相当于 span) | selectable、space、decode |
<rich-text> |
富文本(渲染 HTML) | nodes(Array/String) |
<image> |
图片 | src、mode、lazy-load、@error、@load |
<icon> |
图标 | type、size、color |
<progress> |
进度条 | percent、show-info、stroke-width |
vue
<!-- image 的 mode 值 -->
<!-- scaleToFill:缩放到完全填充(可能变形) -->
<!-- aspectFit:保持纵横比,完整显示(两侧可能留白) -->
<!-- aspectFill:保持纵横比,裁剪填充(不变形,推荐) -->
<!-- widthFix:宽度固定,高度自适应 -->
<!-- heightFix:高度固定,宽度自适应 -->
<image src="/static/logo.png" mode="aspectFill" lazy-load />
<!-- rich-text 渲染 HTML -->
<rich-text :nodes="htmlContent" />
9.3 表单组件
| 组件 | 说明 |
|---|---|
<button> |
按钮 |
<input> |
输入框(单行) |
<textarea> |
多行文本输入框 |
<checkbox-group> / <checkbox> |
复选框 |
<radio-group> / <radio> |
单选框 |
<switch> |
开关选择器 |
<slider> |
滑动选择器 |
<picker> |
从底部弹起的滚动选择器 |
<picker-view> |
嵌入页面的滚动选择器 |
<form> |
表单(配合 submit 事件) |
<label> |
标签(点击关联组件) |
vue
<!-- button 类型和属性 -->
<button type="primary" size="mini" :disabled="loading" :loading="loading" @click="submit">
提交
</button>
<!-- 微信小程序开放能力(open-type) -->
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">获取用户信息</button>
<button open-type="getPhoneNumber" @getphonenumber="onGetPhone">获取手机号</button>
<button open-type="share">分享</button>
<!-- input 完整示例 -->
<input
v-model="phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
:disabled="false"
confirm-type="done"
@input="onInput"
@confirm="onSubmit"
@focus="onFocus"
@blur="onBlur"
/>
<!-- picker 选择器 -->
<picker mode="date" :value="date" start="2020-01-01" end="2030-12-31" @change="onDateChange">
<view>{{ date || '请选择日期' }}</view>
</picker>
<picker mode="multiSelector" :range="cities" @change="onCityChange">
<view>{{ selectedCity || '请选择城市' }}</view>
</picker>
9.4 媒体组件
| 组件 | 说明 |
|---|---|
<video> |
视频播放 |
<audio> |
音频播放(已废弃,推荐用 API) |
<camera> |
系统相机(小程序端) |
<live-player> |
实时音视频播放(微信小程序) |
vue
<!-- video 示例 -->
<video
id="myVideo"
src="https://example.com/video.mp4"
:controls="true"
:autoplay="false"
:loop="false"
poster="https://example.com/cover.jpg"
object-fit="contain"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
@error="onError"
/>
<script setup>
const videoContext = uni.createVideoContext('myVideo')
// 控制视频播放
videoContext.play()
videoContext.pause()
videoContext.seek(30) // 跳转到 30 秒
</script>
9.5 地图组件
vue
<map
id="myMap"
:latitude="latitude"
:longitude="longitude"
:scale="16"
:markers="markers"
:polyline="polyline"
show-location
@markertap="onMarkerTap"
@callouttap="onCalloutTap"
style="width: 100%; height: 400rpx;"
/>
<script setup>
const mapContext = uni.createMapContext('myMap')
// 获取地图信息
mapContext.getCenterLocation({
success(res) {
console.log('中心坐标:', res.latitude, res.longitude)
}
})
// 移动到某个位置
mapContext.moveToLocation({ latitude: 30.0, longitude: 120.0 })
</script>
10. 样式与布局
10.1 尺寸单位
uni-app 推荐使用 rpx(响应式像素),以 750rpx 为屏幕宽度基准自动换算:
css
/* rpx:响应式像素,根据屏幕宽度自动换算 */
/* 以 iPhone 6(375px)为基准:1rpx = 0.5px = 1物理像素 */
.box {
width: 750rpx; /* 占满屏幕宽度 */
height: 200rpx;
font-size: 28rpx;
}
/* px:固定像素,不随屏幕缩放 */
.nav {
height: 44px; /* 导航栏高度,不跟随屏幕变化 */
}
/* % vh vw:相对单位 */
.full-height {
height: 100vh;
}
rpx 换算规则:
- 设计稿宽度 750px → 1:1 对应 rpx
- iPhone 6(375px 物理宽度):1rpx = 0.5px
- iPhone Plus(414px 物理宽度):1rpx ≈ 0.552px
10.2 Flexbox 布局
uni-app 推荐使用 Flexbox(小程序原生不支持 Grid):
css
/* 水平居中对齐 */
.row-center {
display: flex;
flex-direction: row;
justify-content: center; /* 主轴(水平)对齐 */
align-items: center; /* 交叉轴(垂直)对齐 */
}
/* 垂直居中 */
.col-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 两端对齐 */
.space-between {
display: flex;
justify-content: space-between;
}
/* 弹性伸缩 */
.flex-grow {
flex: 1; /* 等比填充剩余空间 */
flex-shrink: 0; /* 不收缩 */
flex-basis: auto;
}
/* 换行 */
.wrap {
display: flex;
flex-wrap: wrap;
}
10.3 内置 CSS 变量
uni-app 提供了一些内置 CSS 变量,用于处理安全区域等:
css
/* 状态栏高度(刘海屏) */
.status-bar {
height: var(--status-bar-height);
}
/* 底部安全区(iPhone 底部小条) */
.safe-area-bottom {
padding-bottom: var(--window-bottom);
}
/* 导航栏高度 */
.nav-placeholder {
height: calc(var(--status-bar-height) + 44px);
}
/* 自定义导航栏时常用 */
.custom-nav {
height: 88rpx;
padding-top: var(--status-bar-height);
}
10.4 媒体查询(H5 端)
css
/* H5 端可使用媒体查询 */
@media (min-width: 768px) {
.container {
max-width: 750px;
margin: 0 auto;
}
}
10.5 样式注意事项
css
/* ❌ 不支持:不能使用通配符选择器 */
* { box-sizing: border-box; }
/* ❌ 不支持:不能用 tagName 选择器(小程序端) */
div { color: red; }
/* ✅ 正确:使用 class 选择器 */
.box { color: red; }
/* ✅ 正确:page 相当于 body */
page {
background-color: #f5f5f5;
height: 100%;
}
/* ❌ 不支持(小程序端):::before ::after 伪元素 */
/* ✅ 可以(H5 端)使用 */
11. uni API 常用接口
11.1 网络请求
javascript
// 基础请求
uni.request({
url: 'https://api.example.com/user',
method: 'GET', // GET POST PUT DELETE PATCH
data: { id: 1 },
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
timeout: 10000, // 超时(毫秒)
withCredentials: false, // H5 端是否携带 cookie
success(res) {
console.log('状态码:', res.statusCode)
console.log('数据:', res.data)
console.log('响应头:', res.header)
},
fail(err) {
console.error('请求失败:', err)
},
complete(res) {
// 无论成功失败都会触发
}
})
// Promise 化写法(推荐)
const [err, res] = await uni.request({
url: 'https://api.example.com/user',
method: 'POST',
data: { name: 'Alice', age: 25 }
})
if (!err) {
console.log(res.data)
}
// 上传文件
uni.uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePath,
name: 'file',
formData: { type: 'image' },
success(res) {
const result = JSON.parse(res.data)
console.log('上传成功:', result.url)
}
})
// 下载文件
uni.downloadFile({
url: 'https://example.com/file.pdf',
success(res) {
if (res.statusCode === 200) {
uni.openDocument({ filePath: res.tempFilePath })
}
}
})
11.2 数据缓存
javascript
// 同步存储(同步操作,用于简单场景)
uni.setStorageSync('userToken', 'eyJhbG...')
const token = uni.getStorageSync('userToken')
uni.removeStorageSync('userToken')
uni.clearStorageSync() // 清空所有缓存
// 异步存储(推荐,不阻塞 JS 线程)
uni.setStorage({
key: 'userInfo',
data: { name: 'Alice', age: 25 },
success() { console.log('存储成功') }
})
uni.getStorage({
key: 'userInfo',
success(res) { console.log('获取成功:', res.data) },
fail() { console.log('key 不存在') }
})
uni.removeStorage({ key: 'userInfo' })
// 查看缓存信息
uni.getStorageInfo({
success(res) {
console.log('已用空间:', res.currentSize, 'KB')
console.log('限制空间:', res.limitSize, 'KB')
console.log('所有 key:', res.keys)
}
})
// Promise 化(更简洁)
const [err, res] = await uni.getStorage({ key: 'userInfo' })
if (!err) {
const userInfo = res.data
}
11.3 媒体与文件
javascript
// 选择图片
uni.chooseImage({
count: 9, // 最多选择图片数量
sizeType: ['original', 'compressed'], // 原图/压缩图
sourceType: ['album', 'camera'], // 相册/相机
success(res) {
console.log('图片临时路径:', res.tempFilePaths)
console.log('图片文件信息:', res.tempFiles)
}
})
// 选择视频
uni.chooseVideo({
sourceType: ['camera', 'album'],
maxDuration: 60, // 最大录制时长(秒)
camera: 'back', // 默认后置摄像头
success(res) {
console.log('视频路径:', res.tempFilePath)
console.log('视频大小:', res.size, 'bytes')
console.log('时长:', res.duration, '秒')
}
})
// 预览图片
uni.previewImage({
urls: ['https://...1.jpg', 'https://...2.jpg'],
current: 0, // 当前显示第几张
longPressActions: {
itemList: ['保存图片', '发送给朋友'],
success({ tapIndex, index }) {
if (tapIndex === 0) {
uni.saveImageToPhotosAlbum({ filePath: urls[index] })
}
}
}
})
// 保存图片到相册
uni.saveImageToPhotosAlbum({
filePath: localImagePath,
success() {
uni.showToast({ title: '保存成功' })
},
fail() {
// 引导用户开启相册权限
uni.openSetting({ withSubscriptions: true })
}
})
// 扫码
uni.scanCode({
onlyFromCamera: true, // 仅从相机扫码
scanType: ['qrCode', 'barCode'], // 扫码类型
success(res) {
console.log('扫码结果:', res.result)
console.log('码的类型:', res.scanType)
}
})
11.4 设备信息
javascript
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
console.log('品牌:', systemInfo.brand) // 'Apple' / 'xiaomi'
console.log('型号:', systemInfo.model) // 'iPhone 13'
console.log('系统:', systemInfo.system) // 'iOS 16.0' / 'Android 12'
console.log('屏幕宽度:', systemInfo.screenWidth) // 375(px)
console.log('屏幕高度:', systemInfo.screenHeight) // 812
console.log('设备像素比:', systemInfo.pixelRatio) // 3
console.log('状态栏高度:', systemInfo.statusBarHeight) // 44
console.log('平台:', systemInfo.platform) // 'ios' / 'android' / 'devtools'
console.log('微信版本:', systemInfo.version) // '8.0.24'(小程序)
console.log('基础库版本:', systemInfo.SDKVersion) // '2.24.1'(小程序)
// 获取网络状态
uni.getNetworkType({
success(res) {
// none / wifi / 2g / 3g / 4g / 5g / unknown
console.log('网络类型:', res.networkType)
}
})
// 监听网络变化
uni.onNetworkStatusChange((res) => {
console.log('网络状态变化:', res.isConnected, res.networkType)
})
// 获取位置
uni.getLocation({
type: 'wgs84', // 'wgs84' 或 'gcj02'(国内地图用 gcj02)
altitude: false,
success(res) {
console.log('纬度:', res.latitude)
console.log('经度:', res.longitude)
console.log('速度:', res.speed)
console.log('精确度:', res.accuracy)
},
fail(err) {
// 引导用户开启位置权限
if (err.errCode === 1) {
uni.showModal({
title: '提示',
content: '需要获取您的位置信息,请在设置中开启位置权限',
success({ confirm }) {
if (confirm) uni.openSetting()
}
})
}
}
})
// 剪切板
uni.setClipboardData({
data: '要复制的文本',
success() { uni.showToast({ title: '复制成功' }) }
})
uni.getClipboardData({
success(res) { console.log('剪切板内容:', res.data) }
})
// 获取电量
uni.getBatteryInfo({
success(res) {
console.log('电量:', res.level, '%')
console.log('是否充电:', res.isCharging)
}
})
// 震动
uni.vibrateShort({ type: 'medium' }) // 短震
uni.vibrateLong() // 长震
11.5 界面交互
javascript
// Toast 轻提示
uni.showToast({
title: '操作成功',
icon: 'success', // success / error / loading / none
duration: 2000, // 显示时长(毫秒)
mask: false, // 是否显示透明蒙层(防止点击穿透)
image: '/static/custom-icon.png' // 自定义图标
})
uni.hideToast()
// Loading 加载提示
uni.showLoading({ title: '加载中...', mask: true })
uni.hideLoading()
// Modal 弹出框
uni.showModal({
title: '提示',
content: '确定要删除吗?',
showCancel: true,
cancelText: '取消',
confirmText: '确定',
confirmColor: '#FF0000',
success({ confirm, cancel }) {
if (confirm) {
// 点击确定
}
}
})
// ActionSheet 操作菜单
uni.showActionSheet({
title: '选择操作',
itemList: ['拍照', '从相册选择', '取消'],
success(res) {
console.log('点击了第', res.tapIndex, '项')
}
})
// 导航栏
uni.setNavigationBarTitle({ title: '新标题' })
uni.setNavigationBarColor({
frontColor: '#ffffff', // 必须为 #ffffff 或 #000000
backgroundColor: '#007AFF',
animation: { duration: 400, timingFunc: 'easeIn' }
})
uni.showNavigationBarLoading() // 显示导航栏 loading
uni.hideNavigationBarLoading() // 隐藏导航栏 loading
// TabBar 操作
uni.setTabBarBadge({ index: 0, text: '3' }) // 设置角标
uni.removeTabBarBadge({ index: 0 }) // 移除角标
uni.showTabBarRedDot({ index: 1 }) // 显示红点
uni.hideTabBarRedDot({ index: 1 }) // 隐藏红点
uni.showTabBar({ animation: true }) // 显示 TabBar
uni.hideTabBar({ animation: true }) // 隐藏 TabBar
11.6 导航栏操作
javascript
// 自定义导航栏按钮(App 端)
// 在 pages.json 中配置
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情",
"app-plus": {
"titleNView": {
"buttons": [
{
"text": "\ue534", // 图标字体
"fontSrc": "/static/iconfont.ttf",
"fontSize": "22px",
"color": "#ffffff",
"float": "right",
"onclick": "onNavRightButtonClick" // 回调方法名
}
]
}
}
}
}
12. 条件编译
12.1 什么是条件编译?
条件编译允许开发者用同一套代码,为不同平台编写差异化的实现。利用注释实现,不同平台编译时只保留对应平台的代码,其余代码被过滤掉。
12.2 条件编译语法
javascript
// #ifdef 平台名称 → 仅在该平台生效
// 平台特有代码
// #endif
// #ifndef 平台名称 → 除该平台外均生效
// 非该平台的代码
// #endif
// 多平台用 || 分隔
// #ifdef APP-PLUS || H5
// 在 App 和 H5 上生效
// #endif
12.3 平台名称速查
| 平台名称 | 说明 |
|---|---|
APP-PLUS |
App(包含 iOS 和 Android,包含 nvue 和 vue) |
APP-PLUS-NVUE |
App nvue 页面 |
APP-ANDROID |
App Android 端(HBuilderX 3.4.1+) |
APP-IOS |
App iOS 端(HBuilderX 3.4.1+) |
H5 |
H5(Web 端) |
MP |
所有小程序平台 |
MP-WEIXIN |
微信小程序 |
MP-ALIPAY |
支付宝小程序 |
MP-BAIDU |
百度小程序 |
MP-TOUTIAO |
抖音/头条小程序 |
MP-LARK |
飞书小程序 |
MP-QQ |
QQ 小程序 |
MP-KUAISHOU |
快手小程序 |
MP-DINGTALK |
钉钉小程序 |
QUICKAPP-WEBVIEW |
快应用(Webview 渲染) |
12.4 各文件类型的条件编译
JS/TS 中的条件编译
javascript
// #ifdef APP-PLUS
// 仅 App 端执行
const appVersion = plus.runtime.version
console.log('App 版本:', appVersion)
// #endif
// #ifdef MP-WEIXIN
// 仅微信小程序执行
wx.login({
success(res) { console.log('微信登录 code:', res.code) }
})
// #endif
// #ifndef H5
// 非 H5 端执行(即 App 和小程序)
uni.getSystemInfo({ success(res) { console.log(res) } })
// #endif
// 多平台
// #ifdef APP-PLUS || MP-WEIXIN
console.log('App 或微信小程序')
// #endif
Vue/HTML 模板中的条件编译
vue
<template>
<view>
<!-- #ifdef APP-PLUS -->
<button @click="callPhone">拨打电话(仅 App)</button>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">
微信授权登录
</button>
<!-- #endif -->
<!-- #ifndef MP -->
<!-- 非小程序平台显示(App + H5) -->
<view class="pc-layout">宽屏布局</view>
<!-- #endif -->
</view>
</template>
CSS/SCSS 中的条件编译
css
/* #ifdef APP-PLUS */
/* 仅 App 端生效的样式 */
.app-only {
/* 使用 App 特有的 CSS 属性 */
}
/* #endif */
/* #ifdef H5 */
.h5-cursor {
cursor: pointer;
}
/* #endif */
/* 注意:CSS 中必须用 /* */ 注释,不能用 // */
pages.json 中的条件编译
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
// #ifdef MP-WEIXIN
"mp-weixin": {
"usingComponents": {}
}
// #endif
}
}
]
}
static 目录的条件编译
static/
├── mp-weixin/ # 只在微信小程序中编译
│ └── icon.png
├── app-plus/ # 只在 App 中编译
│ └── icon.png
└── logo.png # 所有平台都编译
13. 状态管理(Pinia)
13.1 安装与配置
uni-app Vue3 版本内置了 Pinia,无需额外安装:
javascript
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
return { app, pinia }
}
13.2 创建 Store
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 选项式 Store(类似 Vuex)
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: '',
isLogin: false
}),
getters: {
userName: (state) => state.userInfo?.name || '未登录',
avatar: (state) => state.userInfo?.avatar || '/static/default-avatar.png'
},
actions: {
async login(credentials) {
try {
const res = await uni.request({
url: '/api/login',
method: 'POST',
data: credentials
})
this.token = res.data.token
this.userInfo = res.data.userInfo
this.isLogin = true
uni.setStorageSync('token', this.token)
} catch (e) {
throw e
}
},
logout() {
this.token = ''
this.userInfo = null
this.isLogin = false
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
},
// 从本地存储恢复登录状态
restoreSession() {
const token = uni.getStorageSync('token')
if (token) {
this.token = token
this.isLogin = true
}
}
}
})
// 组合式 Store(推荐,更灵活)
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.count, 0)
)
const count = computed(() =>
items.value.reduce((sum, item) => sum + item.count, 0)
)
function addItem(product) {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.count++
} else {
items.value.push({ ...product, count: 1 })
}
}
function removeItem(productId) {
const index = items.value.findIndex(item => item.id === productId)
if (index > -1) {
items.value.splice(index, 1)
}
}
function clearCart() {
items.value = []
}
return { items, total, count, addItem, removeItem, clearCart }
})
13.3 在组件中使用 Store
vue
<template>
<view>
<text>欢迎,{{ userStore.userName }}</text>
<text>购物车商品数:{{ cartStore.count }}</text>
<text>购物车总价:{{ cartStore.total }}</text>
<button @click="logout">退出登录</button>
<button @click="addToCart">加入购物车</button>
</view>
</template>
<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const cartStore = useCartStore()
// storeToRefs:解构 store 时保持响应式
const { userName, avatar, isLogin } = storeToRefs(userStore)
// 注意:actions 直接解构即可,不需要 storeToRefs
const { logout } = userStore
const addToCart = () => {
cartStore.addItem({ id: 1, name: '商品A', price: 99 })
}
</script>
13.4 持久化(配合本地存储)
javascript
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '', // 初始化时从缓存读取
userInfo: JSON.parse(uni.getStorageSync('userInfo') || 'null')
}),
actions: {
setToken(token) {
this.token = token
uni.setStorageSync('token', token) // 同步写入缓存
},
setUserInfo(info) {
this.userInfo = info
uni.setStorageSync('userInfo', JSON.stringify(info))
},
clear() {
this.token = ''
this.userInfo = null
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
}
}
})
14. 自定义组件开发
14.1 组件创建规范(easycom)
将组件放在 components/<组件名>/<组件名>.vue 路径下,uni-app 会自动注册 ,无需手动 import:
components/
├── my-button/
│ └── my-button.vue ← 自动引入,直接使用 <my-button />
├── custom-list/
│ └── custom-list.vue
└── upload-image/
└── upload-image.vue
14.2 基础组件示例
vue
<!-- components/my-button/my-button.vue -->
<template>
<button
class="my-button"
:class="[`my-button--${type}`, { 'my-button--disabled': disabled, 'my-button--loading': loading }]"
:disabled="disabled || loading"
:style="{ width: width }"
@click="handleClick"
>
<view v-if="loading" class="my-button__loading">
<image src="/static/loading.gif" class="loading-icon" />
</view>
<slot>{{ text }}</slot>
</button>
</template>
<script setup>
const props = defineProps({
text: {
type: String,
default: '按钮'
},
type: {
type: String,
default: 'primary',
validator: (val) => ['primary', 'success', 'warning', 'danger', 'default'].includes(val)
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
width: {
type: String,
default: '100%'
}
})
const emit = defineEmits(['click'])
const handleClick = (event) => {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<style lang="scss" scoped>
.my-button {
height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
&--primary {
background-color: $primary-color;
color: #fff;
}
&--default {
background-color: #fff;
color: $text-primary;
border: 1rpx solid $border-color;
}
&--disabled {
opacity: 0.5;
}
}
</style>
14.3 父子组件通信
vue
<!-- 父组件 -->
<template>
<child-component
:title="parentTitle"
:list="items"
@update="onChildUpdate"
@close="onClose"
ref="childRef"
/>
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
const parentTitle = ref('父组件标题')
const items = ref([1, 2, 3])
const childRef = ref(null)
const onChildUpdate = (data) => {
console.log('子组件传来的数据:', data)
}
const onClose = () => {
console.log('子组件触发了关闭')
}
const callChildMethod = () => {
childRef.value?.refresh() // 调用子组件暴露的方法
}
</script>
vue
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const props = defineProps({
title: String,
list: {
type: Array,
default: () => []
}
})
const emit = defineEmits(['update', 'close'])
const refresh = () => {
console.log('子组件 refresh 方法被调用')
}
// 暴露方法给父组件通过 ref 调用
defineExpose({ refresh })
const handleAction = () => {
emit('update', { data: '子组件的数据' })
}
</script>
14.4 插槽(Slot)
vue
<!-- 组件定义 -->
<template>
<view class="card">
<!-- 默认插槽 -->
<slot>默认内容(父组件未传入时显示)</slot>
<!-- 具名插槽 -->
<view class="card-header">
<slot name="header" />
</view>
<view class="card-body">
<slot />
</view>
<view class="card-footer">
<slot name="footer" />
</view>
<!-- 作用域插槽:向父组件传数据 -->
<slot name="item" v-for="item in list" :item="item" :index="index" />
</view>
</template>
<!-- 使用组件 -->
<template>
<my-card>
<!-- 默认插槽内容 -->
<text>默认内容</text>
<!-- 具名插槽 -->
<template #header>
<text class="title">卡片标题</text>
</template>
<template #footer>
<button>操作按钮</button>
</template>
<!-- 作用域插槽 -->
<template #item="{ item, index }">
<view>{{ index }}. {{ item.name }}</view>
</template>
</my-card>
</template>
15. 网络请求封装与拦截器
15.1 基础请求封装
javascript
// utils/request.js
const BASE_URL = 'https://api.example.com'
class Request {
constructor(config = {}) {
this.config = {
baseURL: BASE_URL,
timeout: 10000,
header: {
'Content-Type': 'application/json'
},
...config
}
}
// 请求拦截器列表
requestInterceptors = []
// 响应拦截器列表
responseInterceptors = []
// 添加请求拦截器
addRequestInterceptor(onFulfilled, onRejected) {
this.requestInterceptors.push({ onFulfilled, onRejected })
}
// 添加响应拦截器
addResponseInterceptor(onFulfilled, onRejected) {
this.responseInterceptors.push({ onFulfilled, onRejected })
}
async request(options) {
// 合并配置
let config = {
...this.config,
...options,
url: options.url.startsWith('http') ? options.url : this.config.baseURL + options.url,
header: { ...this.config.header, ...(options.header || {}) }
}
// 执行请求拦截器
for (const interceptor of this.requestInterceptors) {
try {
config = await interceptor.onFulfilled(config)
} catch (e) {
if (interceptor.onRejected) interceptor.onRejected(e)
throw e
}
}
// 发送请求
let response
try {
response = await new Promise((resolve, reject) => {
uni.request({
...config,
success: resolve,
fail: reject
})
})
} catch (err) {
throw err
}
// 执行响应拦截器
for (const interceptor of this.responseInterceptors) {
try {
response = await interceptor.onFulfilled(response)
} catch (e) {
if (interceptor.onRejected) interceptor.onRejected(e)
throw e
}
}
return response
}
get(url, params, options = {}) {
return this.request({ ...options, url, method: 'GET', data: params })
}
post(url, data, options = {}) {
return this.request({ ...options, url, method: 'POST', data })
}
put(url, data, options = {}) {
return this.request({ ...options, url, method: 'PUT', data })
}
delete(url, params, options = {}) {
return this.request({ ...options, url, method: 'DELETE', data: params })
}
}
// 创建实例
const http = new Request()
// 添加请求拦截器
http.addRequestInterceptor(
(config) => {
// 自动携带 token
const token = uni.getStorageSync('token')
if (token) {
config.header.Authorization = `Bearer ${token}`
}
return config
},
(err) => Promise.reject(err)
)
// 添加响应拦截器
http.addResponseInterceptor(
(response) => {
const { statusCode, data } = response
if (statusCode === 401) {
// token 过期,跳转登录
uni.reLaunch({ url: '/pages/login/login' })
return Promise.reject(new Error('登录已过期'))
}
if (statusCode !== 200) {
const message = data?.message || `请求失败(${statusCode})`
uni.showToast({ title: message, icon: 'none' })
return Promise.reject(new Error(message))
}
if (data.code !== 0 && data.code !== 200) {
uni.showToast({ title: data.message || '操作失败', icon: 'none' })
return Promise.reject(data)
}
return data.data || data
},
(err) => {
uni.showToast({ title: '网络请求失败,请检查网络', icon: 'none' })
return Promise.reject(err)
}
)
export default http
15.2 API 接口模块化
javascript
// api/user.js
import http from '@/utils/request'
export const userApi = {
// 登录
login: (data) => http.post('/auth/login', data),
// 获取用户信息
getUserInfo: () => http.get('/user/info'),
// 修改用户信息
updateUserInfo: (data) => http.put('/user/info', data),
// 上传头像
uploadAvatar: (filePath) =>
new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://api.example.com/upload/avatar',
filePath,
name: 'file',
header: { Authorization: `Bearer ${uni.getStorageSync('token')}` },
success: (res) => resolve(JSON.parse(res.data)),
fail: reject
})
}),
// 退出登录
logout: () => http.post('/auth/logout')
}
// api/product.js
import http from '@/utils/request'
export const productApi = {
getList: (params) => http.get('/products', params),
getDetail: (id) => http.get(`/products/${id}`),
search: (keyword) => http.get('/products/search', { keyword })
}
15.3 在组件中使用
vue
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { userApi } from '@/api/user'
import { productApi } from '@/api/product'
const userInfo = ref(null)
const products = ref([])
const loading = ref(false)
onLoad(async () => {
loading.value = true
try {
// 并行请求
const [user, productList] = await Promise.all([
userApi.getUserInfo(),
productApi.getList({ page: 1, size: 20 })
])
userInfo.value = user
products.value = productList.items
} catch (e) {
console.error('请求失败:', e)
} finally {
loading.value = false
}
})
</script>
16. nvue 原生渲染页面
16.1 nvue 简介
nvue(native vue)是 uni-app 的原生渲染页面,基于 Weex 引擎改造,渲染出来的是原生控件而非 WebView。
使用场景:
- 需要高性能滚动列表(长列表)
- 需要跟手流畅的手势动画
- 部分只能在 App 端实现的高级 UI
16.2 nvue 与 vue 的主要差异
| 特性 | vue(WebView) | nvue(原生渲染) |
|---|---|---|
| CSS 支持 | 近完整 CSS | 仅 Flexbox,不支持 float/position:fixed 等 |
| 动画性能 | 受限于 JS-视图通信 | 更流畅(原生层直接操作) |
| 滚动列表 | scroll-view/v-for | 专用 list/cell/recycle-list 组件 |
| 大部分组件 | 全支持 | 部分不支持 |
| 选择器 | 支持 class/id | 仅支持 class |
16.3 nvue 基础用法
vue
<!-- pages/my-nvue.nvue -->
<template>
<!-- nvue 中必须有且只有一个根节点 -->
<list class="list" :loadmoreoffset="100" @loadmore="loadMore">
<!-- cell 是 list 的子元素,等同于列表项 -->
<cell v-for="item in list" :key="item.id" class="list-item">
<image :src="item.avatar" class="avatar" />
<view class="info">
<text class="name">{{ item.name }}</text>
<text class="desc">{{ item.desc }}</text>
</view>
</cell>
<!-- 底部加载提示 -->
<cell class="loading-cell">
<loading-indicator v-if="isLoading" color="#007AFF" size="50" />
<text v-else class="no-more">没有更多了</text>
</cell>
</list>
</template>
<style>
/* nvue 中只能使用 class 选择器 */
/* 必须使用 Flexbox 布局 */
.list {
flex: 1;
}
.list-item {
flex-direction: row;
align-items: center;
padding: 20px;
border-bottom-width: 1px;
border-bottom-color: #e5e5e5;
border-bottom-style: solid;
}
.avatar {
width: 100px;
height: 100px;
border-radius: 50px;
}
</style>
17. uniCloud 云开发
17.1 uniCloud 简介
uniCloud 是 DCloud 联合阿里云、腾讯云提供的Serverless 云开发平台,让前端开发者无需学习后端知识即可快速构建完整应用。
核心组成:
- 云函数(Cloud Function):Node.js 运行的服务端逻辑
- 云数据库(Cloud Database):MongoDB 风格的 JSON 数据库
- 云存储(Cloud Storage):文件上传和 CDN 加速
- uni-id:统一的用户体系(登录、注册、鉴权)
- JQL(JSON Query Language):前端直接操作数据库的权限控制语言
17.2 在客户端调用云函数
javascript
// 调用云函数
const result = await uniCloud.callFunction({
name: 'user-center', // 云函数名称
data: {
action: 'getInfo',
uid: '123'
}
})
console.log('云函数返回:', result.result)
// 使用 clientDB 直接操作数据库(需配置安全规则)
const db = uniCloud.database()
// 查询
const res = await db.collection('articles')
.where({ status: 'published' })
.orderBy('createTime', 'desc')
.limit(20)
.get()
// 新增
await db.collection('articles').add({
title: '文章标题',
content: '文章内容',
createTime: Date.now()
})
// 上传文件
const uploadResult = await uniCloud.uploadFile({
filePath: tempFilePath,
cloudPath: `images/${Date.now()}.jpg`
})
console.log('文件 CDN 地址:', uploadResult.fileID)
17.3 云函数示例
javascript
// uniCloud/cloudfunctions/user-center/index.js
'use strict'
const db = uniCloud.database()
exports.main = async (event, context) => {
const { action, uid } = event
switch (action) {
case 'getInfo':
const user = await db.collection('users').doc(uid).get()
return { code: 0, data: user.data }
case 'updateInfo':
await db.collection('users').doc(uid).update(event.data)
return { code: 0, message: '更新成功' }
default:
return { code: -1, message: '未知操作' }
}
}
18. 插件市场与 uni_modules
18.1 DCloud 插件市场
DCloud 插件市场(https://ext.dcloud.net.cn/)提供:
- 前端组件(UI 库、功能组件)
- JS SDK(第三方服务集成)
- 原生插件(封装原生 Android/iOS 能力)
- uniCloud 云函数模板
- 项目模板
18.2 安装 uni_modules 插件
bash
# 方式1:HBuilderX 中直接点击"导入插件"
# 方式2:CLI 项目通过 uni_modules 安装
# 在 HBuilderX 中安装
# 1. 访问 https://ext.dcloud.net.cn
# 2. 找到目标插件,点击"使用 HBuilderX 导入插件"
# 3. 选择项目,确认导入
# CLI 项目安装插件
# 在项目根目录执行(需 HBuilderX CLI)
hx publish uni_modules install <plugin-id>
18.3 常用组件库推荐
| 组件库 | 特点 | 适用场景 |
|---|---|---|
| uni-ui | 官方出品,全端兼容 | 基础组件补充 |
| uView UI(uv-ui) | 功能丰富,API 完善,nvue 兼容 | 企业级应用 |
| Wot Design Uni | 高质量,支持 Vue3 + TS | Vue3 项目 |
| First UI | 美观,组件齐全 | 商业项目 |
| TuniaoUI | 华为风格 | HarmonyOS 适配 |
18.4 uni-ui 使用示例
bash
# 安装 uni-ui
npm install @dcloudio/uni-ui
json
// pages.json 中配置 easycom
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}
vue
<template>
<!-- uni-ui 组件无需手动引入,easycom 自动处理 -->
<uni-list>
<uni-list-item title="列表项" note="描述" link />
</uni-list>
<uni-card title="卡片标题" sub-title="副标题">
<text>卡片内容</text>
</uni-card>
<uni-calendar v-model="date" />
<uni-forms :model="form" rules="rules">
<uni-forms-item name="username" label="用户名">
<uni-easyinput v-model="form.username" placeholder="请输入用户名" />
</uni-forms-item>
</uni-forms>
<uni-load-more :status="loadStatus" />
<uni-popup ref="popup" type="bottom">
<view class="popup-content">弹窗内容</view>
</uni-popup>
</template>
19. 打包发布全流程
19.1 发布到 H5(Web)
bash
# CLI 方式打包
npm run build:h5
# 打包产物在 dist/build/h5/ 目录下
# 直接将 h5/ 目录内容部署到 Web 服务器即可
H5 注意事项:
- 配置服务器路由(SPA),所有路径都返回 index.html
- 开启 HTTPS(微信授权、定位等功能需要)
- 配置跨域代理(开发时在 manifest.json 的 h5.devServer.proxy)
19.2 发布到微信小程序
bash
# CLI 打包
npm run build:mp-weixin
# 打包产物在 dist/build/mp-weixin/ 目录下
# 用微信开发者工具打开该目录,上传发布
微信小程序发布步骤:
- 在 manifest.json 填写微信小程序 AppID
- 执行
npm run build:mp-weixin或 HBuilderX 发行 - 用微信开发者工具打开
dist/build/mp-weixin - 微信开发者工具 → 上传 → 填写版本号和描述
- 在微信公众平台提交审核
19.3 发布到 App
云端打包(HBuilderX,推荐)
1. 在 manifest.json 配置:
- App 基础配置(名称、版本、图标)
- Android 包名、签名证书
- iOS Bundle ID、证书
2. HBuilderX → 发行 → 原生App-云端打包
3. 等待云端打包完成,下载 apk/ipa 文件
4. Android:直接分发或上传应用市场
iOS:上传到 App Store Connect,提交审核
本地离线打包
bash
# 1. 使用 HBuilderX 生成 App 资源包
# 发行 → 原生App-本地打包 → 生成本地打包App资源
# 2. 下载 Android/iOS 离线 SDK
# https://nativesupport.dcloud.net.cn/
# 3. 集成到 Android Studio / Xcode 项目中编译
19.4 热更新(wgt 更新)
bash
# 打包 wgt 资源包(不含原生层)
# HBuilderX → 发行 → 原生App-制作移动App资源升级包
# 生成 __UNI_XXXXXXX.wgt 文件
javascript
// App 内检测并下载更新
const checkUpdate = async () => {
// 检查版本
const serverVersion = await getServerVersion()
const localVersion = plus.runtime.version
if (serverVersion > localVersion) {
uni.showModal({
title: '发现新版本',
content: `发现新版本 ${serverVersion},是否立即更新?`,
success: async ({ confirm }) => {
if (confirm) {
uni.showLoading({ title: '下载更新中...' })
// 下载 wgt 包
const downloadTask = uni.downloadFile({
url: 'https://cdn.example.com/update/app.wgt',
success(res) {
uni.hideLoading()
// 安装 wgt 包
plus.runtime.install(
res.tempFilePath,
{ force: false },
() => {
plus.runtime.restart() // 重启应用
},
(err) => {
uni.showToast({ title: '更新失败', icon: 'none' })
}
)
}
})
downloadTask.onProgressUpdate((res) => {
console.log('下载进度:', res.progress)
})
}
}
})
}
}
19.5 发布到各小程序平台对比
| 平台 | 单包大小 | 总大小 | 审核时间 | 特殊要求 |
|---|---|---|---|---|
| 微信小程序 | 2MB | 20MB | 1-7 天 | 企业认证,60元/年 |
| 支付宝小程序 | 2MB | 8MB | 1-3 天 | 企业账号 |
| 百度小程序 | 2MB | 8MB | 1-3 天 | 需百度账号 |
| 抖音小程序 | 2MB | 20MB | 1-5 天 | 企业认证 |
| QQ 小程序 | 2MB | 20MB | 1-3 天 | --- |
20. 性能优化实战
20.1 逻辑层与视图层通信优化
javascript
// ❌ 错误:onPageScroll 中频繁操作数据
onPageScroll((e) => {
// 每次滚动都触发数据更新,大量通信
this.scrollTop = e.scrollTop
this.isFixed = e.scrollTop > 200
})
// ✅ 正确:使用 CSS 实现或减少数据更新频率
// 方案一:使用 position: sticky 替代 JS 固定头部
// 方案二:节流处理
import { throttle } from '@/utils/common'
const onPageScrollThrottled = throttle((e) => {
isFixed.value = e.scrollTop > 200
}, 100)
20.2 长列表优化
vue
<!-- ✅ 推荐:虚拟列表(recycle-list 在 nvue 中,或使用 uni-ui 的虚拟列表) -->
<!-- 方案一:nvue 中使用 recycle-list -->
<recycle-list class="list" :list-data="list" switch-page-url="/pages/detail/detail">
<cell-slot :item-key="1">
<view class="item">
<text>{{ item.title }}</text>
</view>
</cell-slot>
</recycle-list>
<!-- 方案二:vue 页面中使用分段渲染 -->
<script setup>
import { ref } from 'vue'
const visibleList = ref([])
const allList = ref([])
let page = 1
// 首次只渲染前20条
const initList = (data) => {
allList.value = data
visibleList.value = data.slice(0, 20)
}
// 上拉触底追加
const loadMore = () => {
const start = page * 20
const end = start + 20
visibleList.value.push(...allList.value.slice(start, end))
page++
}
</script>
20.3 图片优化
vue
<template>
<!-- ✅ 懒加载 -->
<image :src="item.imgUrl" lazy-load />
<!-- ✅ 使用 CDN + webp 格式 -->
<image :src="item.imgUrl + '?format=webp&quality=80'" />
<!-- ✅ 缩略图先展示,点击后加载原图 -->
<image
:src="isClicked ? item.originalUrl : item.thumbUrl"
@click="isClicked = true"
/>
</template>
<script setup>
// ✅ 图片预加载(提前加载下一页图片)
uni.getImageInfo({
src: nextImageUrl,
success() {
console.log('图片已缓存')
}
})
</script>
20.4 分包加载
json
// pages.json 中配置分包
{
"pages": [
{ "path": "pages/index/index" },
{ "path": "pages/user/user" }
],
"subPackages": [
{
"root": "subpkg-order",
"name": "订单模块",
"pages": [
{ "path": "pages/order/list" },
{ "path": "pages/order/detail" }
]
},
{
"root": "subpkg-product",
"name": "商品模块",
"pages": [
{ "path": "pages/product/list" },
{ "path": "pages/product/detail" }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["subpkg-order"]
}
}
}
20.5 避免频繁 setData
javascript
// ❌ 错误:循环中多次更新数据
list.forEach((item) => {
// 每次赋值都触发视图更新
items.value.push(item) // 触发 N 次更新
})
// ✅ 正确:一次性更新数据
items.value = [...items.value, ...list] // 只触发 1 次更新
20.6 减少组件嵌套
vue
<!-- ❌ 深层嵌套 -->
<view>
<view>
<view>
<view>
<text>内容</text>
</view>
</view>
</view>
</view>
<!-- ✅ 减少层级 -->
<view>
<text>内容</text>
</view>
20.7 首屏加载优化
javascript
// 1. 骨架屏(Skeleton Screen)
// 在数据加载完成前显示占位 UI
// 2. 关键数据提前加载
// 在 onLoad 而非 onReady 中发起请求
// 3. 页面初始化时减少不必要的操作
onLoad(async (options) => {
// 优先级 1:加载关键数据
await loadCriticalData()
// 优先级 2:加载非关键数据(延迟)
setTimeout(loadNonCriticalData, 100)
})
// 4. 使用分包:控制主包大小 < 1MB
// 5. 减少 globalStyle 中的 JavaScript 初始化逻辑
21. 常见问题与跨端兼容技巧
21.1 样式兼容问题
css
/* 问题1:小程序中不支持 position: fixed */
/* ✅ 解决:使用 cover-view 组件覆盖在地图/视频上 */
/* 或者使用 page 的 page-meta 组件 */
/* 问题2:App 端 css filter 不支持 */
/* ✅ 解决:使用条件编译,App 端用 nvue + 原生方式 */
/* 问题3:小程序中 ::before ::after 不支持 */
/* ✅ 解决:用 <view> 代替 */
/* 问题4:不同平台 1px 边框显示粗细不一致 */
/* ✅ 解决:使用 transform: scaleY(0.5) */
.border-1px::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: #e5e5e5;
transform: scaleY(0.5);
transform-origin: bottom center;
}
21.2 API 兼容问题
javascript
// 判断当前平台
const platform = uni.getSystemInfoSync().platform
// 'ios' | 'android' | 'devtools'(微信开发者工具)| 'mac' | 'windows'
// 条件编译判断平台
// #ifdef APP-PLUS
const appVersion = plus.runtime.version
// #endif
// #ifdef MP-WEIXIN
const wxVersion = wx.version
// #endif
// 判断是否是 iOS
const isIOS = uni.getSystemInfoSync().platform === 'ios'
21.3 小程序常见限制
javascript
// 微信小程序不支持动态导入(import())
// ✅ 解决:使用条件编译为不同平台提供不同实现
// 微信小程序 web-view 限制(H5 URL 需在小程序后台配置业务域名)
// ✅ 解决:在微信公众平台 → 开发 → 开发管理 → 业务域名 中添加
// 小程序包大小限制(主包 2MB,总包 20MB)
// ✅ 解决:使用分包 + CDN 托管大文件
// 小程序中 window 对象不存在
// ✅ 解决:使用条件编译 + uni API 替代
// 微信小程序 v-html 不支持
// ✅ 解决:使用 rich-text 组件
21.4 App 端常见问题
javascript
// 问题:App 端获取状态栏高度
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight
// 问题:App 端软键盘弹起时,页面上移
// ✅ 解决:在 pages.json 中配置
{
"path": "pages/xxx",
"style": {
"app-plus": {
"softinputMode": "adjustResize" // 或 "adjustPan"(默认)
}
}
}
// 问题:App 端返回按钮处理
// ✅ 解决:使用 onBackPress
onBackPress(() => {
if (showDialog.value) {
showDialog.value = false
return true // 返回 true 阻止默认返回行为
}
return false
})
// 问题:App 端与 webview 通信
// ✅ 在 App 端
const webviewContext = uni.createWebviewContext('webviewId', this)
webviewContext.evalJS("alert('hello from App')")
// ✅ 在 webview 中
window.uni.webView.postMessage({ data: { key: 'value' } })
21.5 H5 端常见问题
javascript
// 问题:H5 端跨域
// ✅ 解决:在 manifest.json 中配置开发代理
{
"h5": {
"devServer": {
"proxy": {
"/api": {
"target": "http://backend.example.com",
"changeOrigin": true,
"pathRewrite": { "^/api": "/api" }
}
}
}
}
}
// 问题:H5 端 history 路由刷新 404
// ✅ 解决:Nginx 配置
// location / {
// try_files $uri $uri/ /index.html;
// }
// 问题:H5 端微信登录
// ✅ 解决:使用条件编译
// #ifdef H5
// 使用微信 JSSDK 进行 OAuth2 授权
// #endif
21.6 数据传递限制
javascript
// 页面跳转传参大小限制(小程序:URL 参数长度有限制)
// ✅ 解决:大数据存 Storage,只传 key
// 正确方式:
uni.setStorageSync('productDetail', JSON.stringify(largeData))
uni.navigateTo({ url: `/pages/detail?dataKey=productDetail` })
// 目标页面取出
const key = options.dataKey
const data = JSON.parse(uni.getStorageSync(key))
22. uni-app x 新架构简介
22.1 什么是 uni-app x?
uni-app x 是 DCloud 推出的下一代跨平台开发框架,彻底解决了 uni-app 的几个核心痛点:
- 引入 uts(UTS = Uni Type Script):类 TypeScript 的强类型语言,编译为各平台原生代码
- 引入 uvue 渲染引擎:基于 uts 版 Vue 框架 + 跨平台 CSS 引擎
- Android 端不再内置 JS 引擎:直接编译为 Java/Kotlin 原生代码
- 性能对齐原生 App,动画帧率更高
22.2 uts 语言示例
typescript
// uts 是强类型的 TypeScript 超集
// 可以调用原生平台 API
// Android 端示例(编译为 Kotlin)
import { SmsManager } from 'android.telephony.SmsManager'
import { BitmapFactory } from 'android.graphics.BitmapFactory'
export function sendSms(phoneNumber: string, content: string) {
const smsManager = SmsManager.getDefault()
smsManager.sendTextMessage(phoneNumber, null, content, null, null)
}
// iOS 端示例(编译为 Swift)
import { UIDevice } from 'UIKit'
export function getDeviceId(): string {
return UIDevice.current.identifierForVendor?.uuidString ?? ''
}
22.3 uvue 页面示例
vue
<!-- uni-app x 中的 uvue 页面 -->
<template>
<view class="content">
<button @click="buttonClick">{{ title }}</button>
</view>
</template>
<script setup lang="uts">
// uts 语法
let title = ref<string>("Hello uni-app x")
const buttonClick = () => {
title.value = "已点击"
console.log("按钮被点了")
}
onReady(() => {
console.log("页面 onReady")
})
</script>
<style>
.content {
width: 750rpx;
background-color: white;
}
</style>
23. 工程化最佳实践
23.1 推荐项目结构
my-uni-app/
├── src/
│ ├── api/ # API 层(按业务模块拆分)
│ │ ├── index.js # 统一导出
│ │ ├── user.js
│ │ └── product.js
│ ├── components/ # 组件(easycom 规范)
│ │ └── xxx/xxx.vue
│ ├── composables/ # 组合式函数(Vue3)
│ │ ├── useUser.js
│ │ └── usePagination.js
│ ├── pages/ # 页面文件
│ ├── static/ # 静态资源
│ ├── stores/ # 状态管理(Pinia)
│ ├── uni_modules/ # uni-app 插件
│ └── utils/ # 工具函数
│ ├── request.js # 请求封装
│ ├── auth.js # 鉴权工具
│ └── common.js # 公共工具
├── App.vue
├── main.js
├── manifest.json
├── pages.json
└── uni.scss
23.2 实用组合式函数
javascript
// composables/usePagination.js
import { ref } from 'vue'
export function usePagination(fetchFn, pageSize = 20) {
const list = ref([])
const page = ref(1)
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const loadData = async (isRefresh = false) => {
if (loading.value) return
if (!isRefresh && finished.value) return
if (isRefresh) {
page.value = 1
finished.value = false
}
loading.value = true
try {
const result = await fetchFn({ page: page.value, size: pageSize })
const newList = result.list || result.data || []
if (isRefresh) {
list.value = newList
} else {
list.value.push(...newList)
}
if (newList.length < pageSize) {
finished.value = true
} else {
page.value++
}
} catch (e) {
console.error(e)
} finally {
loading.value = false
if (isRefresh) {
refreshing.value = false
uni.stopPullDownRefresh()
}
}
}
const refresh = () => {
refreshing.value = true
loadData(true)
}
const loadMore = () => loadData()
return { list, loading, finished, refreshing, loadData, refresh, loadMore }
}
javascript
// composables/useUser.js
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'
export function useUser() {
const userStore = useUserStore()
const isLogin = computed(() => userStore.isLogin)
const userInfo = computed(() => userStore.userInfo)
// 需要登录时调用
const requireLogin = (callback) => {
if (!isLogin.value) {
uni.showModal({
title: '提示',
content: '请先登录后继续操作',
confirmText: '去登录',
success({ confirm }) {
if (confirm) {
uni.navigateTo({ url: '/pages/login/login' })
}
}
})
return false
}
callback?.()
return true
}
return { isLogin, userInfo, requireLogin }
}
23.3 路由守卫实现
javascript
// utils/router.js
import { useUserStore } from '@/stores/user'
// 需要登录才能访问的页面列表
const authPages = [
'/pages/order/list',
'/pages/user/profile',
'/pages/cart/cart'
]
// 重写路由方法,添加守卫
const navigateTo = uni.navigateTo.bind(uni)
uni.navigateTo = (options) => {
const url = options.url.split('?')[0]
if (authPages.includes(url)) {
const userStore = useUserStore()
if (!userStore.isLogin) {
uni.showToast({ title: '请先登录', icon: 'none' })
navigateTo({ url: `/pages/login/login?redirect=${encodeURIComponent(options.url)}` })
return
}
}
navigateTo(options)
}
23.4 环境变量配置
javascript
// utils/env.js
// 通过 process.env 获取环境变量
const env = {
// 当前环境(development / production)
NODE_ENV: process.env.NODE_ENV,
// API 基础地址
baseUrl: process.env.NODE_ENV === 'development'
? 'http://localhost:8080'
: 'https://api.example.com',
// 其他配置
cdnUrl: 'https://cdn.example.com',
version: '1.0.0'
}
export default env
json
// package.json 中配置不同环境的构建命令
{
"scripts": {
"dev:h5": "uni",
"build:h5": "uni build",
"dev:mp-weixin": "uni -p mp-weixin",
"build:mp-weixin": "uni build -p mp-weixin",
"dev:app": "uni -p app-plus",
"build:app": "uni build -p app-plus"
}
}
24. 附录:常用 API 速查 & 官方资源
24.1 路由 API 速查
| API | 说明 |
|---|---|
uni.navigateTo(options) |
跳转到新页面(保留当前页,最多 10 层) |
uni.redirectTo(options) |
关闭当前页,跳转到新页面 |
uni.reLaunch(options) |
关闭所有页面,打开新页面 |
uni.switchTab(options) |
跳转到 tabBar 页面 |
uni.navigateBack(options) |
返回上一页(delta 指定返回层数) |
getCurrentPages() |
获取当前页面栈 |
getApp() |
获取 App 实例(访问 globalData) |
24.2 存储 API 速查
| API | 说明 |
|---|---|
uni.setStorageSync(key, data) |
同步存储 |
uni.getStorageSync(key) |
同步获取 |
uni.removeStorageSync(key) |
同步删除 |
uni.clearStorageSync() |
清空所有缓存 |
uni.getStorageInfoSync() |
获取缓存信息 |
24.3 界面 API 速查
| API | 说明 |
|---|---|
uni.showToast(options) |
显示轻提示 |
uni.hideToast() |
隐藏轻提示 |
uni.showLoading(options) |
显示加载中 |
uni.hideLoading() |
隐藏加载中 |
uni.showModal(options) |
弹出对话框 |
uni.showActionSheet(options) |
操作菜单 |
uni.setNavigationBarTitle(options) |
设置标题 |
uni.setNavigationBarColor(options) |
设置导航栏颜色 |
uni.startPullDownRefresh() |
触发下拉刷新 |
uni.stopPullDownRefresh() |
停止下拉刷新 |
uni.pageScrollTo(options) |
滚动到指定位置 |
24.4 媒体 API 速查
| API | 说明 |
|---|---|
uni.chooseImage(options) |
选择图片 |
uni.previewImage(options) |
预览图片 |
uni.saveImageToPhotosAlbum(options) |
保存图片到相册 |
uni.chooseVideo(options) |
选择视频 |
uni.scanCode(options) |
扫码 |
uni.createVideoContext(id) |
创建视频上下文 |
uni.createInnerAudioContext() |
创建音频上下文 |
uni.createCameraContext() |
创建相机上下文(小程序) |
24.5 网络 API 速查
| API | 说明 |
|---|---|
uni.request(options) |
发起网络请求 |
uni.uploadFile(options) |
上传文件 |
uni.downloadFile(options) |
下载文件 |
uni.connectSocket(options) |
创建 WebSocket |
uni.sendSocketMessage(options) |
发送 WebSocket 消息 |
uni.closeSocket(options) |
关闭 WebSocket |
uni.onSocketMessage(callback) |
监听 WebSocket 消息 |
uni.getNetworkType(options) |
获取网络类型 |
24.6 设备 API 速查
| API | 说明 |
|---|---|
uni.getSystemInfo(options) |
获取系统信息 |
uni.getSystemInfoSync() |
同步获取系统信息 |
uni.getLocation(options) |
获取当前位置 |
uni.openLocation(options) |
打开内置地图 |
uni.chooseLocation(options) |
选择地点 |
uni.setClipboardData(options) |
设置剪贴板 |
uni.getClipboardData(options) |
获取剪贴板 |
uni.vibrateShort(options) |
短震动 |
uni.vibrateLong() |
长震动 |
uni.makePhoneCall(options) |
拨打电话 |
uni.openSetting(options) |
打开系统权限设置 |
uni.getSetting(options) |
获取已授权的权限 |
24.7 官方资源链接
| 资源 | URL |
|---|---|
| uni-app 官网 | https://uniapp.dcloud.net.cn/ |
| 官方文档 | https://uniapp.dcloud.net.cn/tutorial/ |
| 组件文档 | https://uniapp.dcloud.net.cn/component/ |
| API 文档 | https://uniapp.dcloud.net.cn/api/ |
| pages.json 文档 | https://uniapp.dcloud.net.cn/collocation/pages |
| manifest.json 文档 | https://uniapp.dcloud.net.cn/collocation/manifest |
| 条件编译文档 | https://uniapp.dcloud.net.cn/tutorial/platform |
| 性能优化 | https://uniapp.dcloud.net.cn/tutorial/performance |
| DCloud 插件市场 | https://ext.dcloud.net.cn/ |
| HBuilderX 下载 | https://www.dcloud.io/hbuilderx.html |
| uni-app x 文档 | https://doc.dcloud.net.cn/uni-app-x/ |
| uniCloud 文档 | https://doc.dcloud.net.cn/uniCloud/ |
| uni-ui 组件库 | https://uniapp.dcloud.net.cn/component/uniui/uni-ui.html |
| GitHub 仓库 | https://github.com/dcloudio/uni-app |
| 社区论坛 | https://ask.dcloud.net.cn/ |
24.8 常用第三方资源
| 资源 | URL | 说明 |
|---|---|---|
| uView UI(uv-ui) | https://www.uvui.cn/ | 功能最丰富的 UI 库 |
| Wot Design Uni | https://wot-design-uni.netlify.app/ | Vue3 + TS 支持好 |
| unibest 脚手架 | https://www.unibest.tech/ | Vue3 工程化模板 |
| uni-helper | https://uni-helper.js.org/ | 类型提示增强 |
| uniapp 面试题 | https://interview.poetries.top/ | 学习参考 |