UniApp 全栈开发万字指南:从入门到企业级实战 | 附完整代码与踩坑总结
前言
在跨端开发技术百花齐放的今天,UniApp 凭借「一套代码,多端运行」的核心优势,已成为中小企业快速落地多端产品的首选方案。从微信小程序、支付宝小程序到 H5、App、抖音小程序,UniApp 能够覆盖几乎所有主流应用平台,大幅降低开发与维护成本。
本文将从环境搭建、核心语法、进阶技巧到企业级封装,系统梳理 UniApp 开发的完整知识体系,配合可直接运行的代码示例与真实项目踩坑总结,帮助读者从入门快速进阶到独立负责项目开发。无论你是前端新手准备入门跨端开发,还是有 Vue 基础想快速上手 UniApp,本文都能为你提供体系化的参考。
一、UniApp 基础认知与环境搭建
1.1 什么是 UniApp
UniApp 是由 DCloud 推出的基于 Vue.js 的跨端开发框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信 / 支付宝 / 百度 / 头条 / 飞书 / QQ / 快手 / 钉钉 / 淘宝)等多个平台。
核心优势:
- 跨端能力强:一套代码同时支持 10+ 平台
- 学习成本低:完全兼容 Vue 语法,Vue 开发者可无缝上手
- 生态丰富:官方插件市场有大量现成组件与模板
- 性能优异:底层通过原生渲染,接近原生应用体验
- 工具链完善:配套 HBuilderX 编辑器,开箱即用
1.2 开发环境搭建
UniApp 主要有两种开发方式:HBuilderX 可视化开发(推荐新手)和 Vue-CLI 命令行开发(适合有工程化需求的团队)。
方式一:HBuilderX 搭建(官方推荐)
- 前往 DCloud 官网 下载 HBuilderX 正式版
- 安装后打开,依次点击「文件 → 新建 → 项目」
- 选择「uni-app」模板,填写项目名称与存储路径,选择 Vue 版本(推荐 Vue3)
- 点击创建,自动生成项目基础结构
方式二:Vue-CLI 脚手架搭建
适合习惯 VS Code 开发、需要自定义构建配置的开发者:
bash
运行
# 全局安装 vue-cli(已安装可跳过)
npm install -g @vue/cli
# 创建 uni-app 项目(Vue3 版本)
npx degit dcloudio/uni-preset-vue#vite-ts my-uniapp-project
# 进入项目目录
cd my-uniapp-project
# 安装依赖
npm install
# 运行到微信小程序
npm run dev:mp-weixin
# 运行到 H5
npm run dev:h5
1.3 项目目录结构详解
一个标准的 UniApp 项目目录如下,每个文件都有明确的职责:
plaintext
├── common # 公共资源:工具函数、全局样式
├── components # 自定义组件目录
├── pages # 页面目录,所有业务页面都放这里
│ └── index # 首页目录
│ └── index.vue # 首页文件
├── static # 静态资源:图片、字体等(注意:此目录不参与编译)
├── uni_modules # uni_modules 插件目录(插件市场下载的插件放这里)
├── App.vue # 应用配置文件:全局样式、应用生命周期
├── main.js # 入口文件:初始化 Vue 实例
├── manifest.json # 应用配置文件:各平台专属配置
├── pages.json # 页面路由配置:页面路径、导航栏、tabBar 等
└── uni.scss # 全局 SCSS 变量文件
💡 注意:
static目录下的文件不会被编译,图片、字体等静态资源必须放在这里;common目录下的 js、css 文件会被编译处理。
二、核心基础:页面、生命周期与路由
2.1 页面创建与路由配置
在 UniApp 中,每新增一个页面都需要在 pages.json 中注册,否则无法访问。
步骤 1:在 pages 目录下新建页面 在 pages 文件夹新建 detail/detail.vue:
vue
<template>
<view class="detail-container">
<text>这是详情页</text>
</view>
</template>
<script setup>
// Vue3 组合式 API 写法
</script>
<style scoped>
.detail-container {
padding: 30rpx;
}
</style>
步骤 2:在 pages.json 中注册页面
json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页",
"navigationBarBackgroundColor": "#409EFF",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "UniApp Demo",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F5F5"
}
}
pages 数组的第一项默认为应用首页,style 字段可以单独配置每个页面的导航栏样式、下拉刷新等属性。
2.2 应用生命周期与页面生命周期
UniApp 的生命周期分为应用生命周期(App.vue 中)和页面生命周期(页面组件中)两类。
应用生命周期(仅在 App.vue 中生效)
表格
| 生命周期 | 触发时机 |
|---|---|
| onLaunch | 应用初始化完成时触发,全局只触发一次 |
| onShow | 应用从后台进入前台显示时触发 |
| onHide | 应用从前台进入后台时触发 |
| onError | 应用发生脚本错误或 API 调用失败时触发 |
代码示例:App.vue
vue
<script>
export default {
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-size: 28rpx;
}
</style>
页面生命周期(常用)
表格
| 生命周期 | 触发时机 |
|---|---|
| onLoad | 页面加载时触发,可获取页面参数 |
| onShow | 页面显示时触发,每次切回页面都会执行 |
| onReady | 页面初次渲染完成时触发 |
| onHide | 页面隐藏时触发 |
| onUnload | 页面卸载时触发 |
| onPullDownRefresh | 下拉刷新时触发 |
| onReachBottom | 页面滚动到底部时触发 |
代码示例:页面生命周期完整演示
vue
<template>
<view class="page">
<text>页面生命周期演示</text>
</view>
</template>
<script>
export default {
data() {
return {}
},
onLoad(options) {
console.log('页面加载,参数:', options)
// 页面初始化请求数据通常放在这里
},
onShow() {
console.log('页面显示')
// 需要每次进入都刷新的数据放这里
},
onReady() {
console.log('页面渲染完成')
// 获取 dom 节点、canvas 绘制等放这里
},
onHide() {
console.log('页面隐藏')
},
onUnload() {
console.log('页面卸载')
// 清除定时器、取消事件监听等
},
onPullDownRefresh() {
console.log('触发下拉刷新')
// 刷新数据后调用 uni.stopPullDownRefresh() 结束刷新
},
onReachBottom() {
console.log('滚动到底部')
// 分页加载下一页数据
}
}
</script>
2.3 页面跳转与参数传递
UniApp 提供了丰富的路由 API,对应不同的跳转场景:
表格
| API | 说明 | 特点 |
|---|---|---|
| uni.navigateTo | 保留当前页面,跳转到新页面 | 可返回,页面栈最多 10 层 |
| uni.redirectTo | 关闭当前页面,跳转到新页面 | 不可返回 |
| uni.switchTab | 跳转到 tabBar 页面 | 只能用于 tabBar 页面跳转 |
| uni.navigateBack | 返回上一页 / 多级页面 | delta 参数控制返回层数 |
| uni.reLaunch | 关闭所有页面,打开指定页面 | 常用于登录后重置页面栈 |
跳转与传参示例:
javascript
运行
// 首页跳转详情页,携带 id 参数
uni.navigateTo({
url: '/pages/detail/detail?id=1001&name=测试商品'
})
// 详情页接收参数
onLoad(options) {
console.log(options.id) // 1001
console.log(options.name) // 测试商品
}
⚠️ 注意:传递参数如果包含特殊字符(如空格、&、中文),需要使用
encodeURIComponent编码,接收时解码。
三、进阶核心:条件编译与跨端适配
3.1 条件编译:跨端差异的核心解决方案
条件编译是 UniApp 最核心的特性之一,通过特殊的注释语法,让同一份代码在不同平台编译出不同的内容。
语法格式
plaintext
// #ifdef 平台标识
代码块(仅该平台生效)
// #endif
// #ifndef 平台标识
代码块(除了该平台都生效)
// #endif
常用平台标识
表格
| 标识 | 对应平台 |
|---|---|
| APP-PLUS | App 端 |
| H5 | H5 端 |
| MP-WEIXIN | 微信小程序 |
| MP-ALIPAY | 支付宝小程序 |
| MP-BAIDU | 百度小程序 |
| MP-TOUTIAO | 字节跳动小程序 |
实战场景示例
1. 模板中使用条件编译
vue
<template>
<view>
<!-- 仅微信小程序显示 -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo">微信一键登录</button>
<!-- #endif -->
<!-- 仅 H5 显示 -->
<!-- #ifdef H5 -->
<button @click="h5Login">账号密码登录</button>
<!-- #endif -->
<!-- 除了 App 端都显示 -->
<!-- #ifndef APP-PLUS -->
<text>非 App 环境提示</text>
<!-- #endif -->
</view>
</template>
2. JS 中使用条件编译
javascript
运行
export default {
methods: {
share() {
// #ifdef MP-WEIXIN
wx.showShareMenu({ withShareTicket: true })
// #endif
// #ifdef H5
navigator.clipboard.writeText('分享链接')
uni.showToast({ title: '链接已复制' })
// #endif
}
}
}
3. CSS 中使用条件编译
css
/* 微信小程序下的特殊样式 */
/* #ifdef MP-WEIXIN */
.container {
padding-top: 20rpx;
}
/* #endif */
3.2 跨端样式适配
rpx 响应式单位
UniApp 推荐使用 rpx 作为尺寸单位,屏幕宽度固定为 750rpx,会根据屏幕宽度自动换算,完美适配不同尺寸设备。
css
/* 设计稿 375px 宽度,1px = 2rpx */
.box {
width: 300rpx; /* 对应 150px */
height: 200rpx; /* 对应 100px */
margin: 20rpx;
}
样式注意事项
- 小程序端不支持
*通配符选择器 - 小程序端不支持部分 CSS 高级选择器(如相邻兄弟选择器部分场景有兼容问题)
- 背景图片建议使用网络图片或 base64,本地图片在部分小程序端有限制
- 使用
scoped隔离组件样式,避免全局污染
四、企业级封装:网络请求与状态管理
4.1 网络请求统一封装
真实项目中,绝对不能直接在页面里写 uni.request,必须统一封装,便于管理接口、处理错误、添加 loading 和 token 鉴权。
新建 common/request.js:
javascript
运行
// 基础配置
const BASE_URL = 'https://api.example.com'
const TIME_OUT = 10000
// 请求拦截器
const requestInterceptor = (config) => {
// 从本地存储获取 token
const token = uni.getStorageSync('token')
if (token) {
config.header['Authorization'] = `Bearer ${token}`
}
// 添加请求标识
config.header['Content-Type'] = 'application/json'
return config
}
// 响应拦截器
const responseInterceptor = (response) => {
const { statusCode, data } = response
// HTTP 状态码判断
if (statusCode === 200) {
// 业务状态码判断
if (data.code === 200) {
return Promise.resolve(data.data)
} else if (data.code === 401) {
// token 过期,跳转登录
uni.showToast({ title: '登录已过期', icon: 'none' })
uni.reLaunch({ url: '/pages/login/login' })
return Promise.reject(data)
} else {
uni.showToast({ title: data.msg || '请求失败', icon: 'none' })
return Promise.reject(data)
}
} else {
uni.showToast({ title: `网络错误 ${statusCode}`, icon: 'none' })
return Promise.reject(response)
}
}
// 错误处理
const errorHandler = (error) => {
uni.hideLoading()
uni.showToast({ title: '网络连接失败', icon: 'none' })
return Promise.reject(error)
}
// 封装请求方法
const request = (options) => {
// 显示 loading(可配置是否显示)
if (options.loading !== false) {
uni.showLoading({ title: '加载中', mask: true })
}
return new Promise((resolve, reject) => {
let config = {
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: options.header || {},
timeout: TIME_OUT,
success: (res) => {
uni.hideLoading()
resolve(responseInterceptor(res))
},
fail: (err) => {
uni.hideLoading()
reject(errorHandler(err))
}
}
// 执行请求拦截
config = requestInterceptor(config)
uni.request(config)
})
}
// 封装常用方法
export const http = {
get: (url, data, options = {}) => request({ url, method: 'GET', data, ...options }),
post: (url, data, options = {}) => request({ url, method: 'POST', data, ...options }),
put: (url, data, options = {}) => request({ url, method: 'PUT', data, ...options }),
delete: (url, data, options = {}) => request({ url, method: 'DELETE', data, ...options })
}
export default http
使用示例:新建 api/goods.js 管理商品接口
javascript
运行
import http from '@/common/request.js'
// 获取商品列表
export const getGoodsList = (params) => {
return http.get('/goods/list', params)
}
// 获取商品详情
export const getGoodsDetail = (id) => {
return http.get(`/goods/detail/${id}`)
}
页面中调用:
javascript
运行
import { getGoodsList } from '@/api/goods.js'
export default {
data() {
return {
list: [],
page: 1
}
},
onLoad() {
this.loadList()
},
methods: {
async loadList() {
try {
const res = await getGoodsList({ page: this.page, pageSize: 10 })
this.list = res.records
} catch (err) {
console.error('加载失败', err)
}
}
}
}
4.2 Pinia 状态管理(Vue3 推荐)
Vue3 版本的 UniApp 官方推荐使用 Pinia 进行全局状态管理,比 Vuex 更简洁轻量。
安装 Pinia:
bash
运行
npm install pinia
main.js 中注册:
javascript
运行
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 }
}
创建用户状态模块:stores/user.js
javascript
运行
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: uni.getStorageSync('token') || '',
userInfo: uni.getStorageSync('userInfo') || {}
}),
getters: {
isLogin: (state) => !!state.token
},
actions: {
// 登录
login(token, userInfo) {
this.token = token
this.userInfo = userInfo
uni.setStorageSync('token', token)
uni.setStorageSync('userInfo', userInfo)
},
// 退出登录
logout() {
this.token = ''
this.userInfo = {}
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
页面中使用:
vue
<script setup>
import { useUserStore } from '@/stores/user.js'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构保持响应式
const { isLogin, userInfo } = storeToRefs(userStore)
const handleLogin = () => {
userStore.login('token_123', { nickname: '测试用户' })
}
</script>
五、实战案例:待办事项 TodoList
下面通过一个完整的 TodoList 案例,串联前面所学的知识点,包含列表展示、新增、删除、本地存储功能。
vue
<template>
<view class="todo-container">
<!-- 顶部输入区 -->
<view class="input-bar">
<input
class="todo-input"
v-model="inputValue"
placeholder="输入待办事项..."
@confirm="addTodo"
/>
<button class="add-btn" @click="addTodo">添加</button>
</view>
<!-- 统计信息 -->
<view class="stats">
<text>共 {{ total }} 项,已完成 {{ doneCount }} 项</text>
</view>
<!-- 待办列表 -->
<view class="todo-list">
<view
class="todo-item"
v-for="(item, index) in list"
:key="index"
@click="toggleTodo(index)"
>
<view class="checkbox" :class="{ checked: item.done }">
<text v-if="item.done">✓</text>
</view>
<text class="todo-text" :class="{ done: item.done }">{{ item.text }}</text>
<button class="delete-btn" size="mini" @click.stop="deleteTodo(index)">删除</button>
</view>
<view class="empty" v-if="list.length === 0">
<text>暂无待办事项</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
inputValue: '',
list: []
}
},
computed: {
total() {
return this.list.length
},
doneCount() {
return this.list.filter(item => item.done).length
}
},
onLoad() {
// 页面加载时从本地存储读取数据
const saved = uni.getStorageSync('todo_list')
if (saved) {
this.list = JSON.parse(saved)
}
},
methods: {
// 添加待办
addTodo() {
if (!this.inputValue.trim()) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
this.list.unshift({
text: this.inputValue.trim(),
done: false,
createTime: Date.now()
})
this.inputValue = ''
this.saveToStorage()
uni.showToast({ title: '添加成功', icon: 'success' })
},
// 切换完成状态
toggleTodo(index) {
this.list[index].done = !this.list[index].done
this.saveToStorage()
},
// 删除待办
deleteTodo(index) {
uni.showModal({
title: '提示',
content: '确定要删除这条待办吗?',
success: (res) => {
if (res.confirm) {
this.list.splice(index, 1)
this.saveToStorage()
}
}
})
},
// 保存到本地存储
saveToStorage() {
uni.setStorageSync('todo_list', JSON.stringify(this.list))
}
}
}
</script>
<style scoped>
.todo-container {
padding: 30rpx;
min-height: 100vh;
box-sizing: border-box;
}
.input-bar {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.todo-input {
flex: 1;
height: 80rpx;
padding: 0 20rpx;
background: #fff;
border-radius: 10rpx;
border: 1px solid #e4e7ed;
}
.add-btn {
width: 160rpx;
height: 80rpx;
line-height: 80rpx;
background: #409eff;
color: #fff;
border-radius: 10rpx;
font-size: 28rpx;
}
.stats {
margin-bottom: 20rpx;
color: #909399;
font-size: 26rpx;
}
.todo-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 20rpx;
background: #fff;
border-radius: 10rpx;
margin-bottom: 16rpx;
}
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2px solid #dcdfe6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #fff;
flex-shrink: 0;
}
.checkbox.checked {
background: #67c23a;
border-color: #67c23a;
}
.todo-text {
flex: 1;
font-size: 28rpx;
color: #303133;
}
.todo-text.done {
text-decoration: line-through;
color: #c0c4cc;
}
.delete-btn {
font-size: 24rpx;
color: #f56c6c;
}
.empty {
text-align: center;
padding: 100rpx 0;
color: #909399;
}
</style>
六、性能优化实战技巧
6.1 页面性能优化
- 分包加载:将不常用的页面放入分包,减少主包体积,提升启动速度
json
{
"subPackages": [
{
"root": "pages/sub",
"pages": [
{ "path": "setting/setting" }
]
}
]
}
- 图片懒加载:长列表图片使用
lazy-load属性,进入可视区再加载
html
预览
<image src="xxx.jpg" lazy-load mode="aspectFill"></image>
- 减少 setData 调用:小程序端频繁 setData 会导致卡顿,尽量批量更新数据
- 合理使用 onReachBottom:分页加载时加锁,防止滚动到底部重复触发请求
6.2 包体积优化
- 静态资源尽量使用 CDN,不要放本地
- 大图片压缩后再使用,推荐 WebP 格式
- 移除未使用的组件和插件,定期清理无用代码
- 小程序端开启代码压缩,在 manifest.json 中配置
- 合理使用条件编译,移除其他平台的冗余代码
七、高频踩坑与解决方案
坑 1:本地图片在小程序端不显示
原因:小程序对本地背景图有限制,不支持 css 中的本地背景图 解决:将图片转为 base64,或上传到 CDN 使用网络地址
坑 2:H5 端跨域问题
原因:浏览器同源策略限制,直接请求后端接口会跨域 解决:在 manifest.json 中配置代理
json
"h5": {
"devServer": {
"proxy": {
"/api": {
"target": "https://api.example.com",
"changeOrigin": true,
"pathRewrite": { "^/api": "" }
}
}
}
}
坑 3:页面跳转传参丢失
原因:参数中包含特殊字符(&、=、中文)导致解析错误 解决:传递时编码,接收时解码
javascript
运行
// 传递
const data = encodeURIComponent(JSON.stringify(obj))
uni.navigateTo({ url: `/pages/detail/detail?data=${data}` })
// 接收
onLoad(options) {
const obj = JSON.parse(decodeURIComponent(options.data))
}
坑 4:scroll-view 下拉刷新冲突
原因:页面同时开启了原生下拉刷新和 scroll-view,会出现手势冲突 解决:使用 scroll-view 时关闭页面的 enablePullDownRefresh,使用 scroll-view 自身的下拉刷新
坑 5:App 端软键盘遮挡输入框
解决:在 manifest.json 的 App 配置中开启 adjustResize
json
"app-plus": {
"softinput": {
"mode": "adjustResize"
}
}
八、总结与学习建议
UniApp 的核心价值在于「一次开发,多端运行」,但这并不意味着可以完全忽略平台差异。优秀的 UniApp 开发者,既要熟练运用条件编译处理跨端差异,也要懂得针对不同平台做性能优化和体验适配。
学习路线建议:
- 先掌握 Vue 基础,再入手 UniApp 会事半功倍
- 从简单页面入手,熟悉生命周期、路由、组件三大基础
- 深入学习条件编译,理解跨端原理
- 尝试封装通用组件和工具函数,培养工程化思维
- 研究官方插件市场,学习优秀开源项目的代码
- 真实项目中多踩坑、多总结,积累跨端兼容经验
UniApp 生态还在持续演进,建议多关注官方更新日志和社区动态。跨端开发的本质是在「开发效率」和「原生体验」之间寻找平衡,而 UniApp 无疑是目前性价比最高的方案之一。
更多推荐



所有评论(0)