UniApp跨端开发终极指南

UniApp 完整知识总结与使用教程

目录

  1. [uni-app 概述](#uni-app 概述)
  2. 跨端原理与架构
  3. 开发环境搭建
  4. 项目目录结构
  5. 核心配置文件详解
    • 5.1 pages.json
    • 5.2 manifest.json
    • 5.3 App.vue
    • 5.4 main.js
    • 5.5 uni.scss
  6. 页面与组件
    • 6.1 页面文件结构
    • 6.2 页面生命周期
    • 6.3 应用生命周期
    • 6.4 组件生命周期
  7. 模板语法与数据绑定
  8. 路由与页面跳转
  9. 基础组件全览
  10. 样式与布局
  11. [uni API 常用接口](#uni API 常用接口)
    • 11.1 网络请求
    • 11.2 数据缓存
    • 11.3 媒体与文件
    • 11.4 设备信息
    • 11.5 界面交互
    • 11.6 导航栏操作
  12. 条件编译
  13. 状态管理(Pinia)
  14. 自定义组件开发
  15. 网络请求封装与拦截器
  16. [nvue 原生渲染页面](#nvue 原生渲染页面)
  17. [uniCloud 云开发](#uniCloud 云开发)
  18. [插件市场与 uni_modules](#插件市场与 uni_modules)
  19. 打包发布全流程
  20. 性能优化实战
  21. 常见问题与跨端兼容技巧
  22. [uni-app x 新架构简介](#uni-app x 新架构简介)
  23. 工程化最佳实践
  24. [附录:常用 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

⚠️ 注意区分:页面生命周期(onLoadonShow 等)只在页面中使用,组件中不支持。组件中应使用 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>
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-classhover-stay-time
<scroll-view> 可滚动视图区域 scroll-y/scroll-x@scrolltolowerscroll-top
<swiper> 滑块视图容器(轮播图) indicator-dotsautoplaycircularcurrent
<swiper-item> 滑块容器(swiper 子组件) ---
<cover-view> 覆盖在原生组件上的文本 用于覆盖 map/video/canvas 等原生组件
<cover-image> 覆盖在原生组件上的图片 ---
<movable-area> 可移动区域 ---
<movable-view> 在 movable-area 内可移动的视图 directionx/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) selectablespacedecode
<rich-text> 富文本(渲染 HTML) nodes(Array/String)
<image> 图片 srcmodelazy-load@error@load
<icon> 图标 typesizecolor
<progress> 进度条 percentshow-infostroke-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/ 目录下
# 用微信开发者工具打开该目录,上传发布

微信小程序发布步骤:

  1. 在 manifest.json 填写微信小程序 AppID
  2. 执行 npm run build:mp-weixin 或 HBuilderX 发行
  3. 用微信开发者工具打开 dist/build/mp-weixin
  4. 微信开发者工具 → 上传 → 填写版本号和描述
  5. 在微信公众平台提交审核

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/ 学习参考
相关推荐
冬至喵喵2 小时前
构建 CLI 的 Python 框架:Typer技术介绍
开发语言·chrome·python
AbandonForce2 小时前
STL list
开发语言·c++
前端老石人2 小时前
HTML 入门指南:从规范视角建立正确知识体系
开发语言·前端·html
计算机学姐2 小时前
基于SpringBoot的高校餐饮档口管理系统
java·vue.js·spring boot·后端·spring·intellij-idea·mybatis
沐知全栈开发2 小时前
MySQL 索引
开发语言
Albert Edison2 小时前
【C++11】特殊类设计
开发语言·c++·单例模式·饿汉模式·懒汉模式
代码改善世界2 小时前
【C++初阶】vector 核心接口和模拟实现
开发语言·c++
Lyyaoo.2 小时前
【设计模式】工厂模式
java·开发语言·设计模式
Ruihong2 小时前
🚀 Vue 一键转 React!企业后台 VuReact 混写迁移实战
vue.js·react.js