uni-app 的定位是「一套代码,多端发行」——用 Vue 语法写一次,编译到微信小程序、支付宝小程序、H5、Android / iOS App 等十几个平台。这篇记录从环境搭建到多端发布的主线,以及踩过的坑。

一、它凭什么能跨端

uni-app 在 Vue 之上做了两件事:

  1. 统一组件与 API:把各端原生的组件和接口抽象成一套 <view>、<text>、uni.request() 这样的通用写法,编译时再翻译到目标平台
  2. 条件编译:遇到平台差异(比如只有微信小程序有开放能力),用注释标记让某些代码只在指定平台生效

所以它不是「写一次完全不用管差异」,而是「大部分代码共用,差异处单独处理」。

可以直接对标理解:

Web 写法 uni-app 写法 说明
<div> <view> 块级容器
<span> <text> 文字,只有它才能长按选中
<img> <image> 图片,默认有尺寸,需写 mode
<a> / 路由跳转 uni.navigateTo() 页面跳转
axios / fetch uni.request() 网络请求
localStorage uni.setStorageSync() 本地缓存

二、开发环境

两种方式,选一个即可:

官方 IDE,开箱即用、内置真机运行与云打包,适合新手和 App 开发。

  1. 下载安装 HBuilderX(选 App 开发版)
  2. 文件 → 新建 → 项目 → 选择 uni-app → 选默认模板
  3. 运行 → 运行到浏览器 / 小程序模拟器 / 手机

适合熟悉 Vue 生态、想用 VS Code + 自定义构建流程的开发者。

1
2
3
4
5
6
7
8
# Vue3 + Vite 版(推荐新项目)
npx degit dcloudio/uni-preset-vue#vite my-project

cd my-project
npm install
npm run dev:mp-weixin # 编译到微信小程序
npm run dev:h5 # 编译到 H5
npm run build:mp-weixin # 生产打包

微信小程序端还需:把 dist/dev/mp-weixin 用微信开发者工具打开,并在工具里开启「服务端口」。

三、目录结构

1
2
3
4
5
6
7
8
9
10
my-project/
├── pages/ # 页面,每个页面一个目录
│ └── index/index.vue
├── components/ # 可复用组件
├── static/ # 静态资源(图片等,不会编译)
├── App.vue # 应用入口,全局样式与生命周期
├── main.js # 初始化
├── pages.json # ⭐ 页面路由与导航栏配置
├── manifest.json # ⭐ 各端打包配置(AppID、权限、图标)
└── uni.scss # 全局样式变量

pages.json 里的 pages 数组第一项就是首页,顺序别搞错;新增页面必须在这里注册,否则跳不过去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
}
],
"globalStyle": {
"navigationBarBackgroundColor": "#5b5bd6",
"navigationBarTextStyle": "white"
},
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/my/my", "text": "我的" }
]
}
}

四、生命周期

分三层,别记混:

层级 常用钩子 触发时机
应用 App.vue onLaunch / onShow / onHide 启动 / 切前台 / 切后台
页面 onLoad / onShow / onReady / onHide / onUnload 加载 / 显示 / 首次渲染完 / 隐藏 / 卸载
页面 onPullDownRefresh / onReachBottom 下拉刷新 / 触底加载
组件 Vue 原生的 created / mounted 等 同 Vue
1
2
3
4
5
6
7
8
9
10
11
12
export default {
onLoad(options) {
// options 就是上个页面传来的参数(URL 上的查询串)
console.log(options.id)
},
onReachBottom() {
this.loadMore() // 触底加载下一页
},
onPullDownRefresh() {
this.reload().finally(() => uni.stopPullDownRefresh())
}
}

五、样式与单位 rpx

uni-app 用 rpx(responsive pixel)做自适应:设计稿按 750rpx = 屏幕宽度 出图。

1
2
3
4
5
/* 750rpx 宽的屏幕上,1rpx = 0.5px;换算比例自动适配各机型 */
.box {
width: 375rpx; /* 占屏幕一半 */
font-size: 28rpx;
}

750 这个基准来自 iPhone 6(375pt × 2)。设计稿如果是 750px 宽,标注多少 px 就写多少 rpx,不用换算,这是最舒服的地方。

样式注意点:

  • 页面样式写在 <style> 里默认只对当前页面生效(已自动加 scope)
  • 想写全局样式放 App.vue
  • 单位别混用,项目里统一 rpx
  • 小程序端不支持 * 选择器、不支持部分 CSS3 高级选择器

六、条件编译(核心技能)

同一份代码里为不同平台写不同逻辑,用注释包裹:

1
2
3
4
5
6
7
8
9
10
11
// #ifdef MP-WEIXIN
wx.login({ success: res => console.log(res.code) }) // 只有微信小程序执行
// #endif

// #ifdef H5
console.log('这段代码只在 H5 生效')
// #endif

// #ifndef APP-PLUS
console.log('除 App 端外都执行')
// #endif

模板和样式里同样可以用:

1
2
3
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo">微信授权登录</button>
<!-- #endif -->
1
2
3
/* #ifdef H5 */
.box { height: calc(100vh - 44px); }
/* #endif */

常用平台标识:

标识 平台 标识 平台
MP-WEIXIN 微信小程序 H5 网页
MP-ALIPAY 支付宝小程序 APP-PLUS App
MP 所有小程序 MP-BAIDU 百度小程序

七、路由与页面跳转

五个 API,区别在于是否保留当前页和能否跳 tabBar:

API 行为 能否跳 tabBar 页面栈
uni.navigateTo 保留当前页,打开新页 ❌ 入栈
uni.redirectTo 关闭当前页,打开新页 ❌ 替换
uni.switchTab 切换 tabBar 页 ✅ 清空非 tab 页
uni.reLaunch 关闭所有页,打开新页 ✅ 清空
uni.navigateBack 返回上一页 — 出栈
1
2
3
4
5
6
7
8
uni.navigateTo({ url: '/pages/detail/detail?id=123' })
uni.switchTab({ url: '/pages/index/index' })
uni.navigateBack({ delta: 1 }) // 返回上一页

// 目标页接收参数
export default {
onLoad(options) { console.log(options.id) } // "123"
}

navigateTo 最多只能再开 9 层页面栈,层级深了会失败。表单流程这类场景改用 redirectTo。

八、请求封装

原生 uni.request 是回调风格,用起来很啰嗦,自己封一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// utils/request.js
const BASE_URL = 'https://api.example.com'

export function request({ url, method = 'GET', data = {}, header = {} }) {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + url,
method,
data,
header: { 'Authorization': uni.getStorageSync('token'), ...header },
success: res => {
if (res.statusCode === 200) resolve(res.data)
else if (res.statusCode === 401) { /* 跳登录 */ reject(res) }
else reject(res)
},
fail: reject
})
})
}

// 用起来
const list = await request({ url: '/articles', data: { page: 1 } })

九、数据缓存

1
2
3
4
5
6
7
// 同步(简单场景直接用)
uni.setStorageSync('token', 'abc123')
const token = uni.getStorageSync('token')
uni.removeStorageSync('token')

// 异步
uni.setStorage({ key: 'token', data: 'abc123' })

小程序端单个 key 上限 1MB、总容量约 10MB,别往里塞大对象。图片路径、长列表这类数据不要缓存,token 和用户信息这类小数据可以。

十、组件库推荐

库 特点 适用
uni-ui 官方出品,按需引入,跨端一致 求稳、官方项目
uView Plus 组件丰富、文档友好(Vue3 版) 业务系统、后台类
uv-ui uView 的 Vue3 兼容分支 老项目迁移

按需引入(uni_modules 方式)直接在插件市场导入即可,不用手动配置。

十一、打包发布

目标端 操作
微信小程序 npm run build:mp-weixin → 微信开发者工具打开 dist/build/mp-weixin → 上传 → 后台提交审核
H5 npm run build:h5 → 把 dist/build/h5 传到服务器/CDN(注意路由 history 模式需配置重定向)
App HBuilderX → 发行 → 云打包(无需 Mac 也能出 iOS 包)

发行前检查清单:

  • manifest.json 里填好各端 AppID
  • 关闭 debug、移除 console
  • 接口域名已加入小程序后台的服务器域名白名单(必须是 https)
  • 隐私协议已配置(现在审核必查)

十二、常见坑速查

现象 原因 解法
页面白屏 / 找不到 没在 pages.json 注册 补上路由配置
跳转 tabBar 页失败 用了 navigateTo 改用 switchTab
网络请求失败 域名未在小程序后台配置 加白名单,开发时可勾选「不校验合法域名」
图片不显示 static 路径写错,或用了本地绝对路径 用 /static/xxx.png,网络图要配白名单
样式不生效 用了一端不支持的选择器 避免 * 选择器、简化选择器
真机正常但模拟器报错 部分 API 模拟器不支持 以真机为准
scroll-view 无法滚动 没设固定高度 给它设明确 height(如 100vh 减去头部)

一句话总结:uni-app 的核心成本不在语法(就是 Vue),而在各端差异处理。把条件编译和平台 API 差异摸清,剩下的就是普通 Vue 开发。