一、为什么选择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、条件编译、组件)
仿写实战项目(新闻资讯、电商、社交)
多平台测试(真机调试、不同分辨率适配)
性能优化(分包加载、图片懒加载、骨架屏)
官方资源
官网:https://uniapp.dcloud.io
插件市场:https://ext.dcloud.net.cn
案例展示:uni-app官网
八、总结
uni-app的核心价值:
效率提升:一套代码覆盖6+平台,开发效率提升3-5倍
生态丰富:插件市场有大量现成组件,开箱即用
学习成本低:基于Vue语法,前端开发者快速上手
社区活跃:文档完善,遇到问题容易找到解决方案
适合场景:
中小型创业公司快速MVP验证
企业内外部管理工具
内容型、工具型小程序/H5应用
不适合场景:
对性能要求极高的3D游戏
需要大量原生交互的复杂应用
一句话总结:学会uni-app,你就拿到了通往全栈+跨端开发的入场券。一套代码,多端覆盖,这就是未来前端开发的效率之道。