uni-app跨平台开发实战:一套代码,发布6平台
一、为什么选择uni-app?
在移动互联网时代,一个产品往往需要同时覆盖Android、iOS、H5、以及微信/支付宝/百度等多个小程序平台。传统开发模式下,每个平台都需要独立开发团队,成本高、周期长、维护难。
uni-app 是DCloud公司基于Vue.js开发的跨端框架,一套代码可以编译发布到 iOS、Android、H5、以及各类小程序(微信/支付宝/百度/字节/QQ/快手),真正做到 “Write once, run anywhere”。
核心优势
| 维度 | 传统多端开发 | uni-app |
|---|---|---|
| 团队配置 | 每个平台2-3人 | 1个前端团队 |
| 代码复用率 | 低于20% | 80%以上 |
| 开发周期 | 3-6个月 | 1-2个月 |
| 维护成本 | 多套代码并行 | 一套代码统一维护 |
技术原理
uni-app 使用 Vue.js 语法规范 + Weex 渲染引擎 + 小程序组件化 的混合架构:
-
开发者编写
.vue文件 -
编译时根据
pages.json和manifest.json配置 -
按目标平台输出对应的代码(H5、小程序、原生应用)
二、环境搭建与项目创建
2.1 安装脚手架
bash
复制
下载
# 安装vue-cli npm install -g @vue/cli # 创建uni-app项目(选择默认模板或TypeScript模板) vue create -p dcloudio/uni-preset-vue my-project # 进入项目目录 cd my-project # 安装依赖 npm install
2.2 运行到不同平台
bash
复制
下载
# 运行到H5(浏览器自动打开) npm run dev:h5 # 运行到微信小程序(需先打开微信开发者工具并导入unpackage目录) npm run dev:mp-weixin # 运行到支付宝小程序 npm run dev:mp-alipay # 运行到百度小程序 npm run dev:mp-baidu
2.3 使用HBuilderX(推荐)
HBuilderX是uni-app官方IDE,提供可视化创建、一键真机调试、云端打包等功能:
-
下载HBuilderX官网版本
-
文件 → 新建 → 项目 → uni-app
-
运行 → 运行到浏览器/手机/小程序模拟器
三、项目结构与核心配置
3.1 目录结构详解
my-project/
├── pages/ # 页面目录(每个.vue文件自动生成对应页面)
│ ├── index/
│ │ └── index.vue # 首页
│ └── user/
│ └── user.vue # 用户中心页
├── components/ # 公共组件目录
├── static/ # 静态资源目录(图片/字体等,不参与编译)
├── unpackage/ # 构建输出目录(默认git忽略)
├── App.vue # 应用主入口(生命周期/全局样式)
├── pages.json # 全局路由配置(路由/TabBar/窗口样式)
├── manifest.json # 应用配置(AppID/权限/平台特性)
├── uni.scss # 全局样式变量(所有页面可引用)
└── main.js # 入口文件(Vue挂载/插件引入)
3.2 pages.json 完整配置
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#007AFF",
"enablePullDownRefresh": true
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "详情页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#007AFF",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/home.png",
"selectedIconPath": "static/home-active.png"
},
{
"pagePath": "pages/user/user",
"text": "我的",
"iconPath": "static/user.png",
"selectedIconPath": "static/user-active.png"
}
]
},
"condition": {
"current": 0,
"list": [
{
"name": "详情页调试",
"path": "pages/detail/detail",
"query": "id=1001"
}
]
}
}
四、完整实战代码
4.1 新闻列表页面(完整功能)
<template>
<view class="container">
<!-- 轮播图区域 -->
<swiper
:indicator-dots="true"
:autoplay="true"
:interval="3000"
class="banner"
>
<swiper-item v-for="(item, idx) in banners" :key="idx">
<image
:src="item.image"
mode="aspectFill"
class="banner-img"
/>
</swiper-item>
</swiper>
<!-- 分类导航 -->
<scroll-view scroll-x class="category-scroll">
<view
v-for="(cat, idx) in categories"
:key="idx"
:class="['category-item', { active: currentCategory === cat.id }]"
@click="switchCategory(cat.id)"
>
{{ cat.name }}
</view>
</scroll-view>
<!-- 新闻列表 -->
<view class="news-list">
<view
v-for="item in newsList"
:key="item.id"
class="news-item"
@click="goDetail(item.id)"
>
<image
:src="item.cover"
lazy-load
mode="aspectFill"
class="news-cover"
/>
<view class="news-info">
<text class="title">{{ item.title }}</text>
<view class="meta">
<text>{{ item.author }}</text>
<text>{{ formatTime(item.createTime) }}</text>
<text>{{ item.viewCount }}阅读</text>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="load-more" v-if="isLoading">
<uni-load-more :status="loadingStatus" />
</view>
<!-- 空状态提示 -->
<view class="empty" v-if="!isLoading && !newsList.length">
<image src="/static/empty.png" mode="aspectFit" />
<text>暂无数据</text>
</view>
</view>
</template>
<script>
import { formatTime } from '@/utils/date'
import { getNewsList, getBanners } from '@/api/news'
export default {
data() {
return {
banners: [],
categories: [
{ id: 1, name: '推荐' },
{ id: 2, name: '最新' },
{ id: 3, name: '热点' }
],
currentCategory: 1,
newsList: [],
pageNum: 1,
pageSize: 15,
hasMore: true,
isLoading: false,
loadingStatus: 'loading'
}
},
onLoad() {
this.init()
},
onReachBottom() {
if (this.hasMore && !this.isLoading) {
this.pageNum++
this.fetchNews()
}
},
onPullDownRefresh() {
this.reset()
this.init().then(() => {
uni.stopPullDownRefresh()
})
},
methods: {
formatTime,
async init() {
uni.showLoading({ title: '加载中' })
await Promise.all([this.fetchBanners(), this.fetchNews()])
uni.hideLoading()
},
reset() {
this.pageNum = 1
this.newsList = []
this.hasMore = true
},
async fetchBanners() {
try {
const res = await getBanners()
this.banners = res.data
} catch (err) {
console.error('获取轮播图失败', err)
}
},
async fetchNews() {
this.isLoading = true
this.loadingStatus = 'loading'
try {
const res = await getNewsList({
categoryId: this.currentCategory,
pageNum: this.pageNum,
pageSize: this.pageSize
})
const { list, total } = res.data
this.newsList = this.pageNum === 1 ? list : [...this.newsList, ...list]
this.hasMore = this.newsList.length < total
this.loadingStatus = this.hasMore ? 'more' : 'noMore'
} catch (err) {
uni.showToast({ title: '加载失败', icon: 'none' })
this.loadingStatus = 'error'
} finally {
this.isLoading = false
}
},
switchCategory(categoryId) {
if (this.currentCategory === categoryId) return
this.currentCategory = categoryId
this.reset()
this.fetchNews()
},
goDetail(id) {
uni.navigateTo({
url: `/pages/detail/detail?id=${id}`
})
}
}
}
</script>
<style lang="scss" scoped>
.container {
background: #f5f5f5;
min-height: 100vh;
}
.banner {
height: 360rpx;
&-img {
width: 100%;
height: 100%;
}
}
.category-scroll {
white-space: nowrap;
background: white;
padding: 20rpx 0;
}
.category-item {
display: inline-block;
padding: 10rpx 30rpx;
margin: 0 10rpx;
font-size: 28rpx;
border-radius: 40rpx;
background: #f0f0f0;
&.active {
background: #007AFF;
color: white;
}
}
.news-item {
display: flex;
margin: 20rpx;
padding: 24rpx;
background: white;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
.news-cover {
width: 200rpx;
height: 140rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
.news-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.meta {
display: flex;
gap: 20rpx;
font-size: 24rpx;
color: #999;
}
</style>
4.2 条件编译(平台差异化)
<template>
<view>
<!-- H5平台特有内容 -->
<!-- #ifdef H5 -->
<view class="h5-only">这是H5特有的内容</view>
<!-- #endif -->
<!-- 微信小程序特有内容 -->
<!-- #ifdef MP-WEIXIN -->
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">微信授权登录</button>
<!-- #endif -->
<!-- App平台特有内容 -->
<!-- #ifdef APP-PLUS -->
<button @click="appLogin">APP一键登录</button>
<!-- #endif -->
</view>
</template>
<script>
export default {
methods: {
// 微信小程序方法
// #ifdef MP-WEIXIN
onGetUserInfo(e) {
if (e.detail.userInfo) {
uni.setStorageSync('userInfo', e.detail.userInfo)
}
},
// #endif
// App方法
// #ifdef APP-PLUS
appLogin() {
plus.oauth.getServices(services => {
// 苹果/安卓原生登录处理
})
}
// #endif
}
}
</script>
<style>
/* H5平台特有样式 */
/* #ifdef H5 */
.h5-only {
background: yellow;
}
/* #endif */
/* 微信小程序特有样式 */
/* #ifdef MP-WEIXIN */
/* 微信小程序样式 */
/* #endif */
</style>
4.3 封装请求拦截器
// 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',
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 401) {
// 未授权,跳转登录页
uni.reLaunch({ url: '/pages/login/login' });
reject(res.data);
} else {
resolve(res.data);
}
} else {
reject(res);
}
},
fail: reject
});
});
};
// 使用示例
// const res = await request({
// url: '/news/list',
// data: { page: 1 }
// });
五、发布打包流程
5.1 H5发布
npm run build:h5
# 编译结果输出至 unpackage/dist/build/h5 目录
# 可将生成的静态资源部署至Nginx服务器或OSS存储
5.2 微信小程序发布
# 构建微信小程序生产环境代码
npm run build:mp-weixin
# 使用微信开发者工具打开构建目录
open unpackage/dist/build/mp-weixin
# 上传并提交审核代码
5.3 原生App发布
-
HBuilderX → 发行 → 原生App-云打包
-
选择Android/iOS证书
-
等待云端编译 → 下载安装包
六、常见踩坑与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 图片不显示 | 路径问题或大小超限 | 使用 /static/ 绝对路径,小程序图片≤2M |
| 页面栈溢出 | 频繁navigateTo超过10层 | 使用 uni.switchTab 或 uni.reLaunch |
| 样式不生效 | 小程序不支持某些CSS | 使用 display: flex + position 替代复杂布局 |
| 跨域请求失败 | 微信小程序限制 | 配置合法域名或使用云函数转发 |
| 条件编译无效 | 注释格式错误 | 必须使用 #ifdef #endif 且无空格 |
七、学习路径与资源
推荐学习顺序
-
Vue.js基础(指令、组件、生命周期、Vuex)
-
uni-app官方文档(重点:pages.json、条件编译、组件)
-
仿写实战项目(新闻资讯、电商、社交)
-
多平台测试(真机调试、不同分辨率适配)
-
性能优化(分包加载、图片懒加载、骨架屏)
官方资源
-
案例展示:uni-app官网
八、总结
uni-app的核心价值:
-
效率提升:一套代码覆盖6+平台,开发效率提升3-5倍
-
生态丰富:插件市场有大量现成组件,开箱即用
-
学习成本低:基于Vue语法,前端开发者快速上手
-
社区活跃:文档完善,遇到问题容易找到解决方案
适合场景:
-
中小型创业公司快速MVP验证
-
企业内外部管理工具
-
内容型、工具型小程序/H5应用
不适合场景:
-
对性能要求极高的3D游戏
-
需要大量原生交互的复杂应用
一句话总结:学会uni-app,你就拿到了通往全栈+跨端开发的入场券。一套代码,多端覆盖,这就是未来前端开发的效率之道。
更多推荐



所有评论(0)