uni-app 跨端开发完全实战指南:从入门到工程化,一套代码打通多端全流程
前言
在小程序、H5、App 多端并行的业务场景下,前端团队常陷入「一套需求、多端开发、重复维护」的效率困境。由 DCloud 推出的 uni-app 框架,基于 Vue.js 技术栈构建,支持一套代码同时发布到 iOS/Android App、H5、微信 / 支付宝 / 抖音等十余种小程序平台,凭借极低的学习成本、成熟的生态体系与贴近原生的性能表现,成为目前国内跨端开发领域的主流选型。
本文将从环境搭建、核心配置、业务实战、跨端兼容、性能优化到工程化踩坑,系统梳理 uni-app 全链路开发要点。全文代码均来自企业级项目沉淀,兼顾入门友好度与进阶深度,帮助你从零搭建可落地的高质量跨端应用。
一、uni-app 基础认知与环境搭建
1.1 什么是 uni-app
uni-app 是一个基于 Vue.js 的全端开发框架,开发者编写一套代码,可编译发布到 iOS、Android、Web(响应式)、微信 / 支付宝 / 百度 / 字节 / QQ / 快手等各类小程序,以及快应用等多个平台。
核心优势:
- 多端覆盖,成本可控:一套源码适配 10+ 平台,开发与维护成本降低 70% 以上
- 技术栈统一,学习门槛低:完全兼容 Vue 2 / Vue 3 语法,前端开发者可无缝上手
- 原生能力兼容,生态丰富:深度适配各端原生 API,配套 uni-ui 组件库、插件市场与 uniCloud 云开发能力
- 开发调试高效:配套 HBuilderX 编辑器,支持热更新、真机调试与云打包一站式流程
1.2 开发环境搭建
开发 uni-app 推荐使用官方编辑器 HBuilderX,工程化项目也支持 VS Code + 命令行的开发方式。
方式一:HBuilderX 可视化创建(新手推荐)
- 前往 DCloud 官网 下载 HBuilderX 正式版
- 打开编辑器 → 文件 → 新建 → 项目 → 选择
uni-app - 填写项目名称与存储路径,选择默认模板即可一键创建
方式二:Vite 命令行创建(Vue3 推荐,工程化首选)
目前官方已全面主推 Vite + Vue3 方案,编译速度与开发体验更优:
bash
运行
# 使用 npx 直接创建(无需全局安装)
npx degit dcloudio/uni-preset-vue#vite my-uniapp-project
# 进入项目安装依赖
cd my-uniapp-project
npm install
# 运行对应平台
npm run dev:mp-weixin # 编译为微信小程序
npm run dev:h5 # 编译为 H5
npm run dev:app # 编译为 App 端
注:Vue2 版本可继续使用 Vue CLI 方案,新项目优先选择 Vue3 + Vite 以获得更好的性能与生态支持。
二、项目目录结构与核心概念
2.1 标准目录结构
一个规范的 uni-app 项目目录如下,理解各文件职责是高效开发的基础:
plaintext
┌─ common # 公共资源、通用工具函数
│ └─ request.js # 网络请求封装
├─ components # 自定义公共组件
│ └─ my-card
│ └─ my-card.vue
├─ pages # 业务页面目录,所有页面必须在此注册
│ └─ index
│ └─ index.vue
├─ static # 静态资源(图片、字体等,编译时直接拷贝)
├─ uni_modules # uni-app 官方插件模块,自动按需引入
├─ App.vue # 应用根组件,全局配置、全局样式、应用生命周期
├─ main.js # 项目入口文件,挂载 Vue 实例与全局插件
├─ manifest.json # 应用配置文件,各端打包参数、权限、AppID 配置
├─ pages.json # 页面路由、导航栏、tabBar、分包等全局配置
└─ uni.scss # 全局 SCSS 变量,自动注入到所有页面
目录规范注意事项:
static目录仅存放静态资源,编译时不会进行压缩处理,大体积图片建议上传 CDN- 业务页面必须全部注册在
pages目录下,否则无法被路由系统识别- 符合规范的
uni_modules插件无需手动注册,框架会自动按需引入
2.2 三大核心配置文件
1. pages.json —— 页面路由与全局外观
pages.json 是 uni-app 中最核心的配置文件,负责页面注册、导航栏样式、底部 tabBar、分包规则等全局外观与路由配置。
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#2979ff",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app 演示",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f8f8f8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#2979ff",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home-active.png",
"text": "首页"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tab/user.png",
"selectedIconPath": "static/tab/user-active.png",
"text": "我的"
}
]
}
}
进阶配置:针对小程序端体积限制,可配合
subPackages分包与preloadRule分包预下载优化首屏速度,具体配置会在性能优化章节详细说明。
2. manifest.json —— 应用与各端专属配置
用于配置应用名称、版本号、AppID、权限声明、各平台专属编译参数。例如微信小程序的 AppID、App 端的权限申请、H5 端的代理配置等,均在此文件中管理。
3. App.vue —— 全局入口组件
App.vue 是 uni-app 的根组件,所有页面都在其下进行切换,常用于定义全局样式、监听应用级生命周期。
vue
<script>
export default {
// 应用生命周期——仅在 App.vue 中生效
onLaunch: function() {
console.log('应用初始化完成,全局仅执行一次')
// 可在此做登录态校验、全局数据初始化、版本更新检测
},
onShow: function() {
console.log('应用从后台切入前台')
},
onHide: function() {
console.log('应用从前台切至后台')
},
onError: function(err) {
console.error('应用全局异常捕获:', err)
}
}
</script>
<style>
/* 全局样式,作用于所有页面 */
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
padding: 20rpx;
box-sizing: border-box;
}
</style>
注意:
onLaunch、onShow、onHide属于应用生命周期,仅在根组件App.vue中生效;页面级生命周期(如onLoad、onPullDownRefresh)仅在页面组件中生效,不可在 App.vue 中使用。
三、核心开发实战:常用 API 与组件封装
3.1 页面生命周期与数据绑定
uni-app 页面同时兼容 Vue 原生生命周期与 uni-app 专属页面生命周期,业务开发中最常用的为 onLoad(页面加载)、onShow(页面显示)、onReady(页面初次渲染完成)、onReachBottom(页面触底)等。
vue
<template>
<view class="container">
<view class="title">{{ title }}</view>
<view class="list">
<view
v-for="(item, index) in list"
:key="item.id"
class="list-item"
@click="goDetail(item.id)"
>
{{ item.name }}
</view>
</view>
<button @click="loadMore" :disabled="loading" class="load-btn">
{{ loading ? '加载中...' : '加载更多' }}
</button>
</view>
</template>
<script>
export default {
data() {
return {
title: '商品列表',
list: [],
page: 1,
loading: false
}
},
onLoad(options) {
// 页面加载时触发,可获取上一页传递的路由参数
console.log('页面入参:', options)
this.getList()
},
onPullDownRefresh() {
// 下拉刷新回调,需在 pages.json 中开启 enablePullDownRefresh
this.page = 1
this.getList().finally(() => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
// 页面滚动触底回调,用于分页加载
if (!this.loading) {
this.page++
this.loadMore()
}
},
methods: {
async getList() {
this.loading = true
// 模拟接口请求
const res = await this.$request.get('/api/goods/list', { page: this.page })
if (res.code === 0) {
this.list = this.page === 1 ? res.data : [...this.list, ...res.data]
}
this.loading = false
},
goDetail(id) {
// 页面跳转,保留当前页,可返回
uni.navigateTo({
url: `/pages/detail/detail?id=${id}`
})
},
loadMore() {
this.getList()
}
}
}
</script>
<style scoped>
.title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333;
}
.list-item {
padding: 24rpx;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
transition: opacity 0.2s;
}
.list-item:active {
opacity: 0.7;
}
.load-btn {
margin-top: 20rpx;
width: 100%;
}
</style>
新手避坑:Vue 的
created生命周期执行早于 uni-app 的onLoad,若需获取页面跳转参数,必须在onLoad中接收,不可在created中获取。
3.2 网络请求统一封装
原生 uni.request 缺少统一的异常处理、鉴权注入与交互反馈,重复代码多。企业级项目中通常会对其进行二次封装,实现接口请求的标准化管理。
在 common/request.js 中创建封装方法:
javascript
运行
// 基础配置
const BASE_URL = 'https://api.example.com'
const TIME_OUT = 10000
const request = (url, data = {}, method = 'GET') => {
return new Promise((resolve, reject) => {
// 请求前展示加载态
uni.showLoading({ title: '加载中', mask: true })
uni.request({
url: BASE_URL + url,
method,
data,
timeout: TIME_OUT,
header: {
'Content-Type': 'application/json',
'Authorization': uni.getStorageSync('token') || ''
},
success: (res) => {
uni.hideLoading()
const { statusCode, data } = res
// 统一 HTTP 状态码处理
if (statusCode === 200) {
// 统一业务状态码处理
if (data.code === 0) {
resolve(data)
} else if (data.code === 401) {
// Token 失效,清空登录态并跳转登录页
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
uni.removeStorageSync('token')
uni.redirectTo({ url: '/pages/login/login' })
reject(data)
} else {
uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
reject(data)
}
} else {
uni.showToast({ title: `网络错误 ${statusCode}`, icon: 'none' })
reject(res)
}
},
fail: (err) => {
uni.hideLoading()
uni.showToast({ title: '网络连接失败,请检查网络', icon: 'none' })
reject(err)
}
})
})
}
// 导出 RESTful 风格方法
export default {
get: (url, data) => request(url, data, 'GET'),
post: (url, data) => request(url, data, 'POST'),
put: (url, data) => request(url, data, 'PUT'),
delete: (url, data) => request(url, data, 'DELETE')
}
在 main.js 中挂载到 Vue 原型,全局可用:
javascript
运行
import request from './common/request.js'
Vue.prototype.$request = request
页面中直接调用即可:
javascript
运行
// GET 请求
const res = await this.$request.get('/api/user/info', { id: 1001 })
// POST 请求
const res = await this.$request.post('/api/login', { username: 'admin', password: '123456' })
进阶优化:可通过请求计数器实现多请求合并 loading,避免频繁触发弹窗闪烁;也可增加请求取消、重复请求拦截等能力,适配更复杂的业务场景。
3.3 自定义组件封装
以通用卡片组件为例,展示 uni-app 组件的封装规范与父子通信方式。
创建 components/my-card/my-card.vue:
vue
<template>
<view class="my-card" @click="handleClick">
<view class="card-header">
<text class="card-title">{{ title }}</text>
<text v-if="extra" class="card-extra">{{ extra }}</text>
</view>
<view class="card-content">
<!-- 默认插槽 -->
<slot></slot>
</view>
<view v-if="showFooter" class="card-footer">
<!-- 具名插槽 -->
<slot name="footer"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'MyCard',
props: {
title: {
type: String,
default: ''
},
extra: {
type: String,
default: ''
},
showFooter: {
type: Boolean,
default: false
}
},
methods: {
handleClick() {
// 向父组件触发事件,支持传参
this.$emit('click')
}
}
}
</script>
<style scoped>
.my-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.card-extra {
font-size: 26rpx;
color: #999;
}
.card-content {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.card-footer {
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
</style>
页面中使用组件:
vue
<template>
<view class="container">
<!-- 基础用法 -->
<my-card title="用户信息" extra="编辑" @click="editUser">
<view>姓名:张三</view>
<view>手机号:138****8888</view>
</my-card>
<!-- 带底部具名插槽 -->
<my-card title="订单详情" :show-footer="true">
<view>商品名称:uni-app 开发实战</view>
<view>订单金额:¥99.00</view>
<template v-slot:footer>
<button size="mini" type="primary" @click.stop="payOrder">立即支付</button>
</template>
</my-card>
</view>
</template>
<script>
import MyCard from '@/components/my-card/my-card.vue'
export default {
components: { MyCard },
methods: {
editUser() {
uni.showToast({ title: '点击了编辑', icon: 'none' })
},
payOrder() {
uni.showToast({ title: '支付成功', icon: 'success' })
}
}
}
</script>
高效技巧:如果组件存放路径符合
components/组件名/组件名.vue的规范,uni-app 会通过 easycom 机制自动注册组件,无需在页面中手动 import 和 components 声明,直接在模板中使用即可,大幅简化开发流程。注意:小程序端不支持
$refs直接操作 DOM,组件间通信优先使用props+$emit,全局状态可使用 Vuex/Pinia。
四、跨端兼容核心:条件编译
跨端开发最大的痛点在于各平台 API 与组件能力的差异。uni-app 提供了条件编译机制,可在编译阶段根据平台剔除无关代码,是实现跨端兼容的核心手段。
4.1 条件编译语法
以 #ifdef(if defined,仅在某平台生效)和 #endif 包裹代码块,指定对应平台:
表格
| 平台标识 | 对应平台 |
|---|---|
MP-WEIXIN |
微信小程序 |
MP-ALIPAY |
支付宝小程序 |
MP-BAIDU |
百度小程序 |
MP-TOUTIAO |
字节跳动 / 抖音小程序 |
MP-QQ |
QQ 小程序 |
H5 |
H5 网页端 |
APP-PLUS |
App 端(iOS/Android) |
APP-PLUS-NVUE |
App 端 nvue 原生渲染页面 |
支持逻辑运算:
#ifndef MP-WEIXIN:除微信小程序外,其他端均生效#ifdef H5 || MP-WEIXIN:H5 或微信小程序端生效
4.2 三种核心使用场景
1. 模板中使用
vue
<template>
<view>
<!-- 仅 H5 端显示分享按钮 -->
<!-- #ifdef H5 -->
<button @click="shareH5">分享到朋友圈</button>
<!-- #endif -->
<!-- 仅微信小程序显示原生分享按钮 -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="share">微信分享</button>
<!-- #endif -->
</view>
</template>
2. 脚本中使用
javascript
运行
methods: {
initShare() {
// 通用逻辑,全端执行
const shareConfig = { title: 'uni-app 实战指南', path: '/pages/index/index' }
// 微信小程序专属 API
// #ifdef MP-WEIXIN
wx.updateShareMenu({ withShareTicket: true })
// #endif
// App 端专属原生 API
// #ifdef APP-PLUS
plus.navigator.setStatusBarStyle('dark')
// #endif
}
}
3. 样式中使用
css
/* 通用样式,全端生效 */
.box {
padding: 20rpx;
}
/* 仅 H5 端生效,添加 hover 交互 */
/* #ifdef H5 */
.box {
cursor: pointer;
transition: all 0.3s ease;
}
.box:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.1);
}
/* #endif */
4.3 配置文件与整目录级条件编译
除了页面内代码,条件编译也可用于配置文件,控制特定平台的页面注册:
json
{
"pages": [
{
"path": "pages/index/index",
"style": {}
}
// #ifdef MP-WEIXIN
,{
"path": "pages/wx-subscribe/wx-subscribe",
"style": {
"navigationBarTitleText": "订阅消息授权"
}
}
// #endif
]
}
五、uni-app 性能优化方案
跨端应用的性能瓶颈多集中在小程序端,以下优化方案均来自项目实战,可显著提升页面流畅度与加载速度。
5.1 页面渲染优化
-
合理使用
v-if与v-show- 频繁切换的场景用
v-show,减少节点销毁与重建开销 - 条件不常变化的场景用
v-if,降低初始渲染压力
- 频繁切换的场景用
-
控制响应式数据量级
- 仅将需要驱动视图更新的数据放入
data,静态常量可定义在组件外部 - 大体积数组可使用
Object.freeze()冻结,取消响应式监听,减少内存占用 - 小程序端底层依赖
setData渲染,建议批量更新数据,避免频繁零散修改
- 仅将需要驱动视图更新的数据放入
-
长列表专项优化
- 常规列表使用
scroll-view+ 分页加载,避免一次性渲染成百上千节点 - 超大数据量(上千条)场景,使用官方
<recycle-list>组件实现虚拟列表渲染 - 保证
v-for的key唯一且稳定,避免列表重排时的性能损耗
- 常规列表使用
-
计算属性缓存 对于复杂计算的派生数据,使用
computed计算属性缓存结果,避免重复计算。
5.2 包体积优化
-
启用分包加载与预下载 小程序端有严格的体积限制(主包 2MB,总包 20MB),分包是最有效的体积优化手段:
json
{ "pages": [ {"path": "pages/index/index"}, {"path": "pages/mine/mine"} ], "subPackages": [ { "root": "pages-goods", "name": "goods", "pages": [ {"path": "list/list"}, {"path": "detail/detail"} ] } ], "preloadRule": { "pages/index/index": { "network": "wifi", "packages": ["goods"] } } }配合
preloadRule可实现:进入首页后,在 WiFi 环境自动预下载商品分包,用户跳转时无感加载。 -
静态资源瘦身
- 图片优先使用 WebP 格式,单张图片建议控制在 40KB 以内
- 大体积图片、音视频资源上传 CDN,使用网络地址,不占用包体积
- 图标优先使用 iconfont 字体图标,替代多套切图
-
代码与组件按需引入
- uni_modules 插件按需安装,避免未使用的组件被打包
- 第三方工具库按需引入,减少冗余代码
5.3 网络性能优化
- 接口请求合并,减少并发请求数量,避免页面加载时请求阻塞
- 合理使用本地缓存(
uni.setStorageSync),不常变化的数据优先读缓存、后台异步更新 - 图片开启懒加载:
<image lazy-load="true" :src="item.img" />,降低首屏带宽压力 - 接口数据按需返回,剔除冗余字段,减小数据包体积
六、高频踩坑与解决方案
6.1 样式单位适配问题
uni-app 推荐使用 rpx 作为尺寸单位,它会根据屏幕宽度自适应换算:
- 以 750px 宽度的设计稿为基准时,1rpx = 0.5px = 1 物理像素
- 字体大小也可使用 rpx,但需注意大屏设备下文字过大的问题,重要文案可搭配
px使用 - 小程序端不支持 CSS 百分比高度计算,固定高度请使用 rpx 或 flex 布局
6.2 H5 端接口跨域问题
开发 H5 时,本地请求后端接口会出现跨域,需在 manifest.json 中配置开发代理:
json
{
"h5": {
"devServer": {
"proxy": {
"/api": {
"target": "https://api.example.com",
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
}
}
}
6.3 小程序端背景图限制
微信小程序不支持本地图片作为 background-image,解决方案:
- 使用网络图片地址
- 将小图片转为 base64 编码写入样式
- 使用
<image>标签绝对定位,模拟背景图效果
6.4 页面跳转传参长度限制
小程序端 URL 参数长度存在上限,传递大量数据时不建议拼接在 URL 中:
- 简单数据传递 id,详情页重新请求接口获取完整数据
- 复杂数据可存入全局状态(Vuex/Pinia)或本地缓存,目标页再读取
6.5 生命周期执行顺序问题
Vue 生命周期与 uni-app 页面生命周期并行执行,执行顺序为:created → onLoad → onShow → mounted → onReady。
- 页面路由参数只能在
onLoad中获取,created阶段尚未注入 - H5 端 DOM 操作需在
onReady或mounted中执行
6.6 小程序端 DOM/BOM 限制
小程序端无浏览器环境,禁止直接使用 window、document、localStorage 等 Web API,对应能力需使用 uni 官方 API 替代:
- 本地存储:
uni.setStorageSync/uni.getStorageSync - 系统信息:
uni.getSystemInfoSync - 定时器:可正常使用
setTimeout/setInterval
七、打包发布全流程
7.1 微信小程序发布
- HBuilderX 顶部菜单 → 运行 → 运行到小程序模拟器 → 微信开发者工具
- 微信开发者工具中调试无误后,点击「上传」,填写版本号与项目备注
- 登录微信公众平台 → 版本管理 → 提交审核 → 审核通过后点击发布上线
7.2 H5 发布
bash
运行
# 执行生产环境打包命令
npm run build:h5
打包产物位于 dist/build/h5 目录,将该目录部署到 Nginx、静态服务器或对象存储服务即可。
部署提示:若部署在网站子目录下,需在
manifest.json的 h5 配置中修改router.base路径,避免资源引用 404。
7.3 App 打包
- HBuilderX 顶部菜单 → 发行 → 原生 App - 云打包
- 选择 Android /iOS 平台,填写证书信息与打包配置
- 提交云打包,完成后下载 apk/ipa 文件,即可上架对应应用市场
八、总结与进阶学习路径
uni-app 的核心价值,在于用一套熟悉的 Vue 技术栈,解决了多端业务的开发效率问题。对于中小型项目、快速验证的业务需求、企业内部工具类应用,uni-app 都是投入产出比极高的技术选型。
想要从「能用」到「用好」uni-app,建议沿着以下路径持续深耕:
- 夯实基础:先吃透 Vue 核心语法与 uni-app 生命周期、API 体系,这是跨端开发的根基
- 理解差异:深入了解各端(尤其是小程序)的运行机制与限制,跨端兼容的本质是补齐平台差异
- 工程化落地:逐步引入分包、组件库、请求封装、状态管理、自动化构建等工程化能力
- 原生扩展:遇到框架能力边界时,学习原生插件开发、nvue 原生渲染等进阶能力,突破性能瓶颈
目前 uni-app 已全面迭代至 Vue3 + Vite 架构,新一代 uni-app x 也在持续演进,跨端性能与开发体验还在不断提升。掌握好这套技术栈,无论是个人独立开发还是企业级项目落地,都能成为你前端能力体系中的重要加分项。
更多推荐



所有评论(0)