Uniapp
一、Uniapp 基础认知
1.1 什么是 Uniapp?
Uniapp = Vue 语法 + 跨端编译。 你写 Vue 代码,Uniapp 编译器帮你转成各平台代码。一套代码 → H5网页 + 微信小程序 + 支付宝小程序 + iOS App + Android App。
核心优势:
- 使用 Vue 语法开发,学习成本低
- 一套代码编译到 10+ 平台
- 丰富的插件市场(数千款插件)
- 内置组件和 API 自动适配各平台
- 支持条件编译,灵活处理平台差异
Uniapp vs Taro:
|
特性 |
Uniapp |
Taro |
|---|---|---|
|
语法 |
Vue 2/3 |
React / Vue 3 |
|
IDE |
HBuilderX(强绑定) |
VS Code 等任意编辑器 |
|
App 端 |
性能优异(独立渲染引擎) |
依赖 React Native |
|
小程序 |
支持微信/支付宝/百度/字节/QQ |
支持微信/支付宝/百度/字节/QQ |
|
社区生态 |
插件市场 + DCloud 生态 |
NPM 生态为主 |
|
适合人群 |
Vue 开发者 |
React 开发者 |
1.2 环境搭建
方式一:HBuilderX(推荐新手)
- 下载 HBuilderX:https://www.dcloud.io/hbuilderx.html
- 文件 → 新建 → 项目 → 选择 uni-app 模板
- 运行 → 运行到浏览器/小程序模拟器
方式二:CLI 命令行
# 创建项目(Vue 3 版本)
npx degit dcloudio/uni-preset-vue#vite-ts my-uni-app
cd my-uni-app
npm install
# 启动 H5 开发
npm run dev:h5
# 启动微信小程序开发(需打开微信开发者工具)
npm run dev:mp-weixin
# 启动 App 开发
npm run dev:app
# 构建各平台
npm run build:h5
npm run build:mp-weixin
微信小程序调试配置:
- 打开微信开发者工具 → 设置 → 安全设置 → 开启服务端口
- 运行
npm run dev:mp-weixin - 微信开发者工具会自动打开项目
1.3 项目结构
my-uni-app/
├── pages/ # 页面目录(自动注册路由)
│ ├── index/
│ │ └── index.vue
│ └── detail/
│ └── detail.vue
├── components/ # 组件目录
├── static/ # 静态资源(图片、字体等,不编译)
├── store/ # 状态管理(Pinia / Vuex)
├── utils/ # 工具函数
│ ├── request.js # 请求封装
│ └── auth.js # 认证工具
├── api/ # 接口API层
├── pages.json # ⭐ 核心配置:路由、窗口样式、tabBar
├── manifest.json # 应用配置:AppID、权限、SDK
├── uni.scss # 全局样式变量(无需引入即可使用)
├── App.vue # 应用入口:生命周期、全局样式
├── main.js # 主入口:注册插件
└── uni_modules/ # 插件市场下载的插件
二、pages.json — 路由与页面核心配置
pages.json 是 Uniapp 最核心的配置文件,所有页面必须在这里注册才能访问,它替代了 Vue Router 的功能。
2.1 基础配置示例
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#ffffff",
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情"
}
},
{
"path": "pages/user/login",
"style": {
"navigationBarTitleText": "登录"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "我的应用",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f5f5f5"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home-active.png"
},
{
"pagePath": "pages/mine/mine",
"text": "我的",
"iconPath": "static/tab/mine.png",
"selectedIconPath": "static/tab/mine-active.png"
}
]
},
"subPackages": [
{
"root": "pages-sub/order",
"pages": [
{ "path": "list", "style": { "navigationBarTitleText": "订单列表" } },
{ "path": "detail", "style": { "navigationBarTitleText": "订单详情" } }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pages-sub/order"]
}
}
}
2.2 pages.json 关键配置项说明
|
配置项 |
说明 |
关键属性 |
|---|---|---|
|
pages |
页面路由表(数组,第一项为首页) |
path, style(导航栏样式、下拉刷新等) |
|
globalStyle |
全局窗口样式 |
navigationBarBackgroundColor, enablePullDownRefresh |
|
tabBar |
底部导航栏(最少2项,最多5项) |
color, selectedColor, list |
|
subPackages |
分包配置(优化首屏加载) |
root(分包根目录), pages |
|
preloadRule |
分包预加载规则 |
network(all/wifi), packages |
|
condition |
启动模式配置(开发调试用) |
current(当前模式), list(模式列表) |
三、页面开发核心语法
3.1 页面跳转(5种方式)
// 1. navigateTo — 打开新页面(保留当前页,可返回,最多10层)
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=hello'
})
// 2. redirectTo — 关闭当前页,打开新页面(不可返回)
uni.redirectTo({
url: '/pages/login/login'
})
// 3. switchTab — 跳转到 tabBar 页面(关闭所有非 tabBar 页面)
uni.switchTab({
url: '/pages/index/index'
})
// 4. reLaunch — 关闭所有页面,打开新页面
uni.reLaunch({
url: '/pages/index/index'
})
// 5. navigateBack — 返回上一页
uni.navigateBack({
delta: 1 // 返回层数,默认1
})
// ========== 接收参数(目标页面) ==========
export default {
onLoad(options) {
console.log(options.id) // '123'
console.log(options.name) // 'hello'
}
}
跳转方法对比:
|
方法 |
是否保留当前页 |
是否可返回 |
适用场景 |
|---|---|---|---|
|
navigateTo |
✅ |
✅ |
列表→详情 |
|
redirectTo |
❌ |
❌ |
登录成功→首页 |
|
switchTab |
❌ |
❌ |
切换 tabBar 页 |
|
reLaunch |
❌ |
❌ |
重新启动应用 |
|
navigateBack |
- |
✅ |
返回上一页 |
3.2 页面生命周期(完整顺序)
export default {
// 1. 页面加载时触发(只触发一次,可获取页面参数)
onLoad(options) {
console.log('页面加载', options)
// 适合:发起初始数据请求
},
// 2. 页面显示时触发(每次显示都会触发)
onShow() {
console.log('页面显示')
// 适合:从其他页面返回时刷新数据
},
// 3. 页面首次渲染完成(只触发一次)
onReady() {
console.log('页面渲染完成')
// 适合:获取节点信息、初始化地图/图表
},
// 4. 页面隐藏时触发(跳转到其他页面或切后台)
onHide() {
console.log('页面隐藏')
// 适合:暂停播放/计时器
},
// 5. 页面卸载时触发(redirectTo / navigateBack / reLaunch)
onUnload() {
console.log('页面卸载')
// 适合:清除定时器、取消网络请求
},
// 下拉刷新(需在 pages.json 配置 enablePullDownRefresh)
onPullDownRefresh() {
// 刷新数据...
uni.stopPullDownRefresh() // 停止刷新动画
},
// 页面触底(可用来加载更多)
onReachBottom() {
console.log('触底了')
// 加载下一页数据
},
// 页面滚动
onPageScroll(e) {
console.log('滚动距离', e.scrollTop)
},
// 分享(小程序)
onShareAppMessage() {
return {
title: '分享标题',
path: '/pages/index/index'
}
}
}
生命周期触发顺序(完整链路):
进入页面: onLoad → onShow → onReady
切后台: onHide
切前台: onShow
跳转他页: onHide
返回本页: onShow
卸载页面: onUnload (redirectTo/reLaunch/navigateBack)
3.3 常用组件速查
<template>
<view>
<!-- 基础容器(类似 div) -->
<view class="box" @click="handleClick">
<text>纯文本(类似 span,长按可选)</text>
</view>
<!-- 图片(mode 控制裁剪方式) -->
<image
src="/static/logo.png"
mode="aspectFill"
style="width: 200rpx; height: 200rpx;"
/>
<!-- 输入框 -->
<input
v-model="form.name"
placeholder="请输入姓名"
type="text"
maxlength="20"
/>
<!-- 文本域 -->
<textarea
v-model="form.remark"
placeholder="请输入备注"
:maxlength="200"
/>
<!-- 按钮 -->
<button type="primary" @click="handleSubmit">提交</button>
<button type="default" plain>普通按钮</button>
<button type="warn" size="mini">警告按钮</button>
<!-- 滚动区域(scroll-view) -->
<scroll-view scroll-y style="height: 500rpx;" @scrolltolower="loadMore">
<view v-for="item in list" :key="item.id">
{{ item.name }}
</view>
</scroll-view>
<!-- 轮播图 -->
<swiper :indicator-dots="true" :autoplay="true" :interval="3000">
<swiper-item v-for="(item, index) in banners" :key="index">
<image :src="item.image" mode="aspectFill" />
</swiper-item>
</swiper>
<!-- 图标(内置图标库) -->
<uni-icons type="home" size="20" color="#333" />
<!-- 表单组件 -->
<picker mode="selector" :range="options" @change="onPickerChange">
<view>当前选择:{{ options[selected] }}</view>
</picker>
<picker mode="date" @change="onDateChange">
<view>日期:{{ date }}</view>
</picker>
</view>
</template>
图片 mode 属性值:
|
值 |
说明 |
|---|---|
|
scaleToFill |
拉伸填满(默认,会变形) |
|
aspectFit |
等比缩放,完整显示(留白) |
|
aspectFill |
等比缩放,填满裁剪(推荐) |
|
widthFix |
宽度不变,高度自适应 |
四、常用 API 调用
4.1 网络请求(request)
// 基础请求
uni.request({
url: 'https://api.example.com/users',
method: 'GET',
data: { page: 1, size: 10 },
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + uni.getStorageSync('token')
},
success: (res) => {
console.log('成功', res.data)
},
fail: (err) => {
console.error('失败', err)
},
complete: () => {
console.log('请求完成(不管成功失败)')
}
})
// Promise 写法(推荐)
async function getUsers() {
try {
const res = await uni.request({
url: 'https://api.example.com/users',
method: 'GET'
})
console.log(res.data)
} catch (err) {
console.error(err)
}
}
请求拦截器封装(推荐在每个项目中使用):
// utils/request.js
const BASE_URL = 'https://api.example.com'
const request = (options) => {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token')
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': 'Bearer ' + token } : {}),
...options.header
},
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data.data)
} else {
uni.showToast({ title: res.data.msg, icon: 'none' })
reject(res.data)
}
} else if (res.statusCode === 401) {
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
reject(res)
} else {
uni.showToast({ title: '请求失败', icon: 'none' })
reject(res)
}
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' })
reject(err)
}
})
})
}
// 封装 GET / POST
export const get = (url, data) => request({ url, method: 'GET', data })
export const post = (url, data) => request({ url, method: 'POST', data })
export const put = (url, data) => request({ url, method: 'PUT', data })
export const del = (url, data) => request({ url, method: 'DELETE', data })
export default request
4.2 本地存储(Storage)
// 同步存储(简单直接)
uni.setStorageSync('token', 'abc123')
uni.setStorageSync('userInfo', { name: '张三', age: 25 })
// 读取
const token = uni.getStorageSync('token')
const userInfo = uni.getStorageSync('userInfo')
// 删除
uni.removeStorageSync('token')
// 清空
uni.clearStorageSync()
// ========== 异步存储 ==========
uni.setStorage({ key: 'cache', data: { ... }, success: () => {} })
uni.getStorage({ key: 'cache', success: (res) => { console.log(res.data) } })
uni.removeStorage({ key: 'cache' })
// ========== 存储信息 ==========
uni.getStorageInfoSync()
// 返回: { keys: ['token', 'userInfo'], currentSize: 5, limitSize: 10240 }
// limitSize 单位 KB(小程序限制 10MB)
4.3 常用 UI API
// 提示框(Toast)
uni.showToast({
title: '操作成功',
icon: 'success', // success / error / loading / none
duration: 2000,
mask: true // 是否透明遮罩(防止重复点击)
})
uni.hideToast()
// 加载中
uni.showLoading({ title: '加载中...', mask: true })
uni.hideLoading()
// 确认弹窗
uni.showModal({
title: '提示',
content: '确定要删除吗?',
success: (res) => {
if (res.confirm) {
console.log('用户点击确定')
} else {
console.log('用户点击取消')
}
}
})
// ActionSheet(底部菜单)
uni.showActionSheet({
itemList: ['拍照', '从相册选择', '取消'],
success: (res) => {
console.log('选择了第' + res.tapIndex + '项')
}
})
// 导航栏加载
uni.showNavigationBarLoading()
uni.hideNavigationBarLoading()
// 设置导航栏标题
uni.setNavigationBarTitle({ title: '新标题' })
// 页面滚动到顶部
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
// 拨打电话
uni.makePhoneCall({ phoneNumber: '10086' })
// 复制到剪贴板
uni.setClipboardData({ data: '要复制的文本' })
4.4 媒体 API
// 选择图片
uni.chooseImage({
count: 1, // 最多选择数量
sizeType: ['compressed'], // original 原图 / compressed 压缩
sourceType: ['album', 'camera'], // album 相册 / camera 拍照
success: (res) => {
const tempFilePaths = res.tempFilePaths
// 上传图片
uni.uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePaths[0],
name: 'file',
success: (uploadRes) => {
console.log(JSON.parse(uploadRes.data))
}
})
}
})
// 预览图片
uni.previewImage({
urls: ['https://xxx.com/img1.jpg', 'https://xxx.com/img2.jpg'],
current: 0 // 当前显示的索引
})
4.5 位置与地图
// 获取位置(需用户授权)
uni.getLocation({
type: 'gcj02', // wgs84(GPS坐标)/ gcj02(国测局坐标)
success: (res) => {
console.log('经度', res.longitude)
console.log('纬度', res.latitude)
}
})
// 选择位置(打开地图选点)
uni.chooseLocation({
success: (res) => {
console.log('位置名称', res.name)
console.log('详细地址', res.address)
console.log('经纬度', res.longitude, res.latitude)
}
})
五、条件编译:一套代码适配多端
条件编译 让同一份代码在不同平台执行不同逻辑,是 UniApp 的核心能力。
// ========== JS 中的条件编译 ==========
// #ifdef H5
console.log('这段只在 H5 平台执行')
// #endif
// #ifdef MP-WEIXIN
console.log('这段只在微信小程序执行')
// #endif
// #ifdef MP-ALIPAY
console.log('这段只在支付宝小程序执行')
// #endif
// #ifdef APP-PLUS
console.log('这段只在 App 端执行')
// #endif
// #ifndef H5
console.log('非 H5 平台执行')
// #endif
// ========== 模板中的条件编译 ==========
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo">微信专属按钮</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<a href="/login">H5 专属链接</a>
<!-- #endif -->
<!-- 各平台都会显示 -->
<view>通用内容</view>
// ========== 样式中的条件编译 ==========
/* #ifdef MP-WEIXIN */
.title { font-size: 28rpx; }
/* #endif */
/* #ifdef H5 */
.title { font-size: 14px; }
/* #endif */
平台代码对照表:
|
代码 |
平台 |
|---|---|
|
H5 |
移动端网页 |
|
MP-WEIXIN |
微信小程序 |
|
MP-ALIPAY |
支付宝小程序 |
|
MP-BAIDU |
百度小程序 |
|
MP-TOUTIAO |
字节小程序 |
|
MP-QQ |
QQ小程序 |
|
APP-PLUS |
App 端(Android + iOS) |
|
APP-PLUS-NVUE |
App 端 weex 模式 |
六、状态管理
6.1 Pinia(Vue 3,推荐)
npm install pinia
// main.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
app.use(createPinia())
return { app }
}
// store/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: null
}),
getters: {
isLogin: (state) => !!state.token
},
actions: {
async login(username, password) {
// 小程序端使用 uni.login()
// #ifdef MP-WEIXIN
const { code } = await uni.login()
// #endif
const res = await uni.request({
url: 'https://api.example.com/login',
method: 'POST',
data: { username, password }
})
this.token = res.data.token
this.userInfo = res.data.userInfo
uni.setStorageSync('token', this.token)
},
logout() {
this.token = ''
this.userInfo = null
uni.removeStorageSync('token')
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
6.2 Vuex(Vue 2)
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: uni.getStorageSync('token') || '',
userInfo: null
},
getters: {
isLogin: (state) => !!state.token
},
mutations: {
SET_TOKEN(state, token) {
state.token = token
uni.setStorageSync('token', token)
},
SET_USER(state, user) {
state.userInfo = user
},
LOGOUT(state) {
state.token = ''
state.userInfo = null
uni.removeStorageSync('token')
}
},
actions: {
async login({ commit }, { username, password }) {
const res = await uni.request({
url: 'https://api.example.com/login',
method: 'POST',
data: { username, password }
})
commit('SET_TOKEN', res.data.token)
commit('SET_USER', res.data.userInfo)
}
}
})
七、组件化开发
7.1 自定义组件
<!-- components/UserCard.vue -->
<template>
<view class="user-card" @click="handleClick">
<image :src="avatar" class="avatar" mode="aspectFill" />
<view class="info">
<text class="name">{{ name }}</text>
<text class="desc">{{ desc }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'UserCard',
props: {
name: { type: String, default: '' },
avatar: { type: String, default: '/static/default-avatar.png' },
desc: { type: String, default: '' }
},
// 声明组件内事件
emits: ['click'],
methods: {
handleClick() {
this.$emit('click', { name: this.name })
}
}
}
</script>
<style scoped>
.user-card {
display: flex;
align-items: center;
padding: 20rpx;
}
.avatar { width: 80rpx; height: 80rpx; border-radius: 50%; }
.name { font-size: 30rpx; font-weight: bold; }
</style>
7.2 组件通信方式
|
方式 |
方向 |
示例 |
|---|---|---|
|
props |
父→子 |
|
|
$emit |
子→父 |
|
|
$refs |
父→子(调用子方法) |
|
|
provide/inject |
祖先→后代 |
|
|
uni.$on / $emit |
全局 |
|
// 全局事件总线(使用 uni 内置的事件系统)
// A 页面触发
uni.$emit('userUpdated', { id: 1, name: '张三' })
// B 页面监听
export default {
onLoad() {
uni.$on('userUpdated', (data) => {
console.log('收到更新', data)
})
},
onUnload() {
uni.$off('userUpdated') // 页面卸载时移除监听,防止内存泄漏
}
}
八、常用插件与组件库
8.1 社区推荐组件库
|
组件库 |
说明 |
安装 |
|---|---|---|
|
uni-ui |
DCloud 官方组件库,与 UniApp 深度集成 |
HBuilderX 插件市场导入 |
|
uView UI |
最流行的 UniApp 组件库 |
|
|
FirstUI |
高性能跨端组件库 |
|
|
ThorUI |
轻量组件库 |
插件市场安装 |
8.2 uView UI 使用
npm install uview-ui
// main.js
import uView from 'uview-ui'
Vue.use(uView)
// uni.scss 中引入
@import 'uview-ui/theme.scss';
// pages.json 配置 easycom 自动引入
{
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
}
}
<template>
<!-- 之后可以在任意页面直接使用 uView 组件,无需 import -->
<u-button type="primary">确定</u-button>
<u-input v-model="value" placeholder="请输入" />
<u-cell-group>
<u-cell title="设置" icon="setting" />
</u-cell-group>
<u-empty text="暂无数据" />
<u-loadmore :status="loadStatus" />
</template>
九、打包发布
9.1 各平台发布命令
# H5 网页 — 打包到 dist/build/h5
npm run build:h5
# 微信小程序 — 打包到 dist/dev/mp-weixin(开发)/ dist/build/mp-weixin(生产)
npm run build:mp-weixin
# 然后在微信开发者工具中上传
# App 端
npm run build:app-plus
# 使用 HBuilderX 云打包或本地打包(需 Android Studio / Xcode)
9.2 manifest.json 关键配置
{
"mp-weixin": {
"appid": "wx你的小程序AppID", // 微信小程序 AppID
"setting": {
"es6": true,
"minified": true,
"postcss": true
}
},
"h5": {
"title": "网站标题",
"router": {
"mode": "hash", // hash / history
"base": "/"
}
},
"app-plus": {
"distribute": {
"android": {
"packageName": "com.example.app",
"permissions": []
},
"ios": {
"bundleIdentifier": "com.example.app"
}
}
}
}
9.3 小程序发布流程
- 配置 AppID:在
manifest.json中填写微信小程序 AppID - 构建生产版本:
npm run build:mp-weixin - 微信开发者工具上传:打开 dist/build/mp-weixin → 点击"上传"
- 微信公众平台提交审核:登录 mp.weixin.qq.com → 版本管理 → 提交审核
- 审核通过 → 点击"发布"上线
十、UniApp 开发最佳实践
10.1 性能优化
- 分包加载:主包只放首页和公共组件,其余页面放分包
- 路由懒加载:pages.json 中无需特殊配置,UniApp 默认按页面拆分
- 长列表:使用
<recycle-view>虚拟列表组件 - 图片优化:使用 WebP 格式,合理使用 mode="aspectFill"
- 减少 setData:避免一次性 set 大量数据,数据做 diff 后再更新
10.2 适配方案
// 设计稿通常为 750px 宽度
// UniApp 推荐使用 rpx 单位:1rpx = 屏幕宽度/750
// 在 375px 屏幕上 1rpx = 0.5px
// 在 414px 屏幕上 1rpx ≈ 0.55px
// uni.scss 中定义全局变量
$primary-color: #007AFF;
$text-color: #333;
$bg-color: #f5f5f5;
$border-color: #eee;
$padding-base: 20rpx;
10.3 安全注意
- AppSecret 只能放在服务端,禁止前端硬编码
- 数据加密传输(HTTPS)
- 请求参数做好校验
- 敏感数据不要存在 Storage 中(小程序端 Storage 是明文存储)
10.4 学习资源
- UniApp 官方文档:https://uniapp.dcloud.net.cn
- 插件市场:https://ext.dcloud.net.cn
- uView UI:https://www.uviewui.com
- 微信小程序文档:https://developers.weixin.qq.com/miniprogram/dev
- HBuilderX 下载:https://www.dcloud.io/hbuilderx.html