简介
uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、鸿蒙Next、Web(响应式)、以及各种小程序(微信/支付宝/百度/抖音/飞书/QQ/快手/钉钉/淘宝/京东/小红书)、快应用、鸿蒙元服务等多个平台。
基础知识
CSS语法
更多内容可以查看官网
css预处理器
uni-app 支持less、sass、scss、stylus等预处理器。
flex布局
Flex布局(Flexible Box Layout)是CSS3中一种高效的布局模型,它通过灵活的容器和项目关系,让元素能自动适应不同屏幕尺寸,简化了复杂布局的实现。
尺寸单位
定义与适配性
px(像素)
是绝对单位,表示屏幕上的物理像素点。在不同设备上,1px 的实际显示大小可能不同(尤其是高分辨率屏幕),可能导致布局比例不一致。
rpx(响应式像素)
是微信小程序独有的相对单位,根据屏幕宽度自适应。规定 屏幕宽度 = 750rpx(无论设备屏幕实际宽度是多少)。例如:
- 在 iPhone6(屏幕宽度 375px)上,1rpx = 0.5px。
- 在 iPhone12(屏幕宽度 390px)上,1rpx ≈ 0.52px。
设计建议
换算关系
初始准备
编辑器下载
uni-app官方推荐的编辑器为HBuilderX,我们可以直接下载后安装使用;我认为其最主要的作用是其可以帮助我们将代码编译为跨平台的运行程序,包括‘浏览器’、‘iOS’、‘Android’、各种小程序等;这里有个问题,就是现在的ide越来越多,使用起来学习成本比较高,这里推荐编写代码时使用自己之前惯用的ide(如vscode等),在编译、打包时再使用HBuilderX,经过测试这是没问题的。
新建项目
使用HBuilderX新建项目
我们现在打开HBuilderX,选择新建项目
选择你需要的模版(这里是默认模版),填入‘项目名称’(my-uniapp-project),Vue版本选择3,点击‘创建’,就可以创建出我们的项目
可以看到uni-app框架已经帮我们添加了示例页面,我们直接选择‘运行’-‘运行到内置浏览器’,就可以编译并运行项目了,等程序运行起来,点击ide右上角的‘预览’,就可以看到运行效果
运行项目

运行效果
使用命令行新建项目
进入目录,打开终端,使用下面的命令
1 2 3 4
| # 新建JavaScript项目 npx degit dcloudio/uni-preset-vue#vite my-uniapp-project # 新建TypeStript项目 npx degit dcloudio/uni-preset-vue#vite-ts my-uniapp-project
|
进入项目目录并安装依赖
1 2
| cd my-uniapp-project npm install
|
如果,我们新建的是TypeScript项目,可以安装增强TypeScript支持的依赖
1 2 3 4
| # 更新TypeScript版本 npm install typescript@latest --save-dev # 安装增强TypeScript支持的依赖 npm install @uni-helper/uni-types
|
接下来我们使用HBuilderX打开项目,可以看到项目已经构建完毕
运行一个小程序
我们已经在内置浏览器运行了第一个程序,现在我们希望让HBuilderX帮我们将代码编译为微信小程序代码,并在‘微信开发者工具’中运行;
下载并安装‘微信开发者工具’,注册并登录‘微信公众平台’,申请一个‘小程序’(可以是测试号), 获取AppID
在HBuilderX中配置微信小程序AppID,在根目录下选择文件‘manifest.json’,在其中选择‘微信小程序配置’,将我们准备好的微信小程序AppID填入

- 选择‘运行’-‘运行到小程序模拟器’-‘微信开发者工具’(在微信开发者工具)

真机调试
在‘微信开发者工具’点击‘真机调试’按钮,选择‘二维码真机调试’,用我们用于调试的手机的微信扫描该二维码,即可在该手机上调试微信小程序

页面配置文件(pages.json)介绍
pages.json是 uni-app 项目的核心配置文件,它决定了应用的整体结构、页面路由、窗口样式和导航栏等全局表现。我们通过一个表格来介绍其主要配置项。
| 配置项 |
是否必填 |
说明 |
pages |
是 |
设置应用的页面路径及每个页面的窗口表现。数组的第一项为应用的入口页(首页) |
globalStyle |
否 |
设置默认页面的窗口表现,如状态栏、导航条、标题、窗口背景色等。 |
tabBar |
否 |
如果应用是多 Tab 架构(如微信小程序),配置底部或顶部 Tab 栏的表现 |
subPackages |
否 |
分包加载配置,用于优化大型应用的加载性能 |
preloadRule |
否 |
分包预加载规则,用于提升分包页面的打开速度 |
condition |
否 |
启动模式配置,仅开发期间生效,用于模拟直达特定页面的场景(如小程序转发) |
easycom |
否 |
组件自动引入规则,简化组件的使用 |
核心配置项详解
pages:定义应用页面
pages是一个数组,每一项都是一个页面配置对象。应用中所有页面都必须在这里声明,新增或减少页面都需要修改此数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页", "enablePullDownRefresh": true, "navigationStyle": "custom" } }, { "path": "pages/profile/profile", "style": { "navigationBarTitleText": "我的" } } ]
|
注意:pages数组的第一项即为应用的启动页(首页)。
globalStyle:全局窗口样式
globalStyle用于定义所有页面的默认样式。如果某个页面在自身的 style中配置了相同属性,则会覆盖这里的全局设置。
1 2 3 4 5 6 7 8
| "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "我的应用", "navigationBarBackgroundColor": "#FFFFFF", "backgroundColor": "#F8F8F8", "enablePullDownRefresh": false, "onReachBottomDistance": 50 }
|
tabBar:配置底部导航
当应用需要像微信那样有固定的底部导航栏时,就需要配置 tabBar。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| "tabBar": { "color": "#7A7E83", "selectedColor": "#007AFF", "backgroundColor": "#FFFFFF", "borderStyle": "black", "position": "bottom", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/icon_home.png", "selectedIconPath": "static/icon_home_active.png" }, { "pagePath": "pages/profile/profile", "text": "我的", "iconPath": "static/icon_profile.png", "selectedIconPath": "static/icon_profile_active.png" } ] }
|
subPackages:分包加载
对于大型项目,可以使用分包来优化。分包下的页面在初次加载时不会被打包到主包中,用户进入对应分包页面时才进行加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| "subPackages": [ { "root": "subpackages/news", "pages": [ { "path": "list", "style": { "navigationBarTitleText": "新闻列表" } }, { "path": "detail", "style": { "navigationBarTitleText": "新闻详情" } } ] } ]
|
实用技巧与注意事项
- 样式优先级:页面级
style配置的优先级高于 globalStyle中的全局配置。
- 自定义导航栏:将
navigationStyle设置为 "custom"可以隐藏默认导航栏,使用自定义导航栏,此配置通常在 globalStyle中设置生效。
- 条件编译:可以为不同平台(如 App、H5、微信小程序)设置不同的样式,实现差异化配置。
- 开发调试:使用
condition配置可以在开发阶段快速调试特定页面(如带参数的详情页)。
组件使用
uni-app框架是基于Vue框架开发的,但是两者还是有区别,有一些组件是uni-app独有的,我们介绍几个比较重要的
视图容器
view
类似于传统html中的div,用于包裹各种元素内容,属性说明可以查看官网;我们的示例页面‘pages/index/index.vue’中就使用了view组件
可滚动视图区域。用于区域滚动,属性说明可以查看官网;在页面中如果内容比较多,我们经常需要使用该组件,滚动查看内容,而且当滚动到底部时,我们通常需要进行懒加载(例如文章列表,加载更多文章信息);我们来写一个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <template> <view class="content"> <view class="banner">banner</view> <scroll-view scroll-y style="height: 300rpx;" @scrolltolower="doScrollToLower"> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> <view class="section">section</view> </scroll-view> </view> </template>
<script setup> const doScrollToLower = () => { console.log('doScrollToLower'); } </script>
<style> .content { display: flex; flex-direction: column; align-items: center; justify-content: center; }
.content .banner { width: 100%; height: 300rpx; background-color: red; }
.content .section { background-color: green; margin-bottom: 20rpx; } </style>
|
可以看到,我们使用<scroll-view>标签将需要滚动查看的内容包裹住,这里需要注意
- scroll-y设置允许纵向滚动
- 我们需要为该标签设置固定高度(style=”height: 300rpx;”),而且设置的高度要高于单独一行的高度
- 可以设置触底事件
@scrolltolower,该事件可以帮助我们触发懒加载
表单组件
按钮组件,更多属性可以查看官方文档;这个组件我们应该再熟悉不过,但在uni-app框架中,我们需要介绍一个属性open-type,这个属性提供了一系列方法帮助我们开发,例如getUserInfo,可以帮助我们获取用户信息;让我们来看看如何使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <view> <button size="mini" class="btn" @click="doClick" open-type="getUserInfo" @getuserinfo="doGetUserInfo">点击我</button> </view> </template>
<script setup> const doClick = () => { console.log('doClick'); };
const doGetUserInfo = (e) => { console.log(e); }; </script>
<style lang="scss"> .btn { background-color: blue; color: #fff; } </style>
|
在代码中我们通过open-type放开了@getuserinfo事件,我们通过该事件,可以获取当前用户(微信)的信息

打印出当前用户信息
文本输入框,更多的组件属性可以查看官网,这也是常用的组件,在移动端比较常用的事件属性有
@input,当键盘输入时,触发input事件,这个事件在校验输入文本时经常用到
@focus, 输入框聚焦时触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <view class="my-item"> <view class="myLabel">用户名</view> <input class="my-input" placeholder="请输入用户名" @foucs="handleFoucs" @input="handleInput"></input> </view> </template>
<script setup> const handleFoucs = () => { console.log() };
const handleInput = (e) => { console.log(e) } </script>
<style lang="scss"> .my-item { display: flex; align-items: center;
.my-input { border: 2rpx solid #000; padding-left: 10rpx; } } </style>
|
picker
从底部弹起的滚动选择器。支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器。更多组件属性可以查看官网
这个组件应该也是移动端经常使用的组件,这个组件需要关注的属性主要有:1. 选择内容的范围;2. 当前的选择内容;3. 选中时的触发事件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| <template> <view> <view>普通选择器</view> <picker :range="arr" :value="index" @change="handleChange"> <view>{{ arr[index] }}</view> </picker> </view> <view> <view>时间选择器</view> <picker mode="time" :value="time" start="00:00" end="23:59" @change="handleChangeTime"> <view>{{ time }}</view> </picker> </view> <view> <view>日期选择器</view> <picker mode="date" :value="date" :start="startDate" :end="endDate" @change="handleChangeDate"> <view>{{date}}</view> </picker> </view> </template>
<script setup> import { computed, ref } from 'vue'; const arr = ['中国', '美国', '巴西', '日本']; const index = ref(0);
const handleChange = (e) => { console.log(e) index.value = e.detail.value };
const time = ref('12:01');
const handleChangeTime = (e) => { console.log(e) time.value = e.detail.value };
const getDate = (type) => { const date = new Date(); let year = date.getFullYear(); let month = date.getMonth() + 1; let day = date.getDate();
if (type === 'start') { year = year - 10; } else if (type === 'end') { year = year + 10; } month = month > 9 ? month : '0' + month; day = day > 9 ? day : '0' + day; return `${year}-${month}-${day}`; };
const date = ref('1991-10-15')
// const curDate = computed(() => { // return getDate({ // format: true // }) // });
const startDate = computed(() => { return getDate('start') });
const endDate = computed(() => { return getDate('end') });
const handleChangeDate = (e) => { console.log(e) date.value = e.detail.value } </script>
<style lang="scss"></style>
|
路由与页面跳转
navigator
页面跳转。该组件类似HTML中的<a>组件,但只能跳转本地页面。目标页面必须在pages.json中注册。更多属性说明可以查看官网。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <view> <navigator url="/pages/myPage/myPage"> <button>跳转到我的页面</button> </navigator> </view> </template>
<script setup>
</script>
<style lang="scss"></style>
|
tabbar
如果应用是一个多 tab 应用,可以通过 tabBar 配置项指定一级导航栏,以及 tab 切换时显示的对应页。
在 pages.json 中提供 tabBar 配置,不仅仅是为了方便快速开发导航,更重要的是在 App 和小程序端提升性能。在这两个平台,底层原生引擎在启动时无需等待 js 引擎初始化,即可直接读取 pages.json 中配置的 tabBar 信息,渲染原生 tab。更多的配置信息可以参考官网;
1 2 3 4 5 6 7 8 9 10 11 12 13
| "tabBar": { "list": [{ "name": "首页", "pagePath": "pages/index/index", "text": "首页" }, { "name": "我的页面", "pagePath": "pages/myPage/myPage", "text": "我的页面" } ] }
|
需要注意的是:代码跳转到 tabbar 页面,api 只能使用uni.switchTab,不能使用 uni.navigateTo、uni.redirectTo;使用 navigator 组件跳转时必须设置open-type=”switchTab”;也就是说,如果一个页面被配置在tabbar中,之前用navigator配置的跳转有可能失效,需要从新配置open-type参数
组件引用
全局引入
uni-app 支持配置全局组件,需在 main.js 里进行全局注册,注册后就可在所有页面里使用该组件。
- 在
main.js 里进行全局导入和注册
1 2 3 4 5 6 7 8 9 10 11 12
| import App from './App' import {createSSRApp} from 'vue'
import myComponent from './components/my-component/my-component.vue' export function createApp() { const app = createSSRApp(App) app.component('my-component', myComponent) return { app } }
|
index.vue 里可直接使用组件
1 2 3 4 5
| <template> <view> <my-component></my-component> </view> </template>
|
局部引入
通过 import 方式引入组件 ,在 components 选项中定义你想要使用的组件。
1 2 3 4 5 6 7 8 9 10 11 12
| <!-- 在index.vue引入 uni-badge 组件--> <template> <view> <uni-badge text="1"></uni-badge><!-- 3.使用组件 --> </view> </template> <script> import uniBadge from '@/components/uni-badge/uni-badge.vue';//1.导入组件(这步属于传统vue规范,但在uni-app的easycom下可以省略这步) export default { components:{uniBadge }//2.注册组件(这步属于传统vue规范,但在uni-app的easycom下可以省略这步) } </script>
|
easycom方式引入
通过uni-app的easycom: 将组件引入精简为一步。只要组件安装在项目的 components 目录下,并符合 components/组件名称/组件名称.vue 目录结构。就可以不用引用、注册,直接在页面中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!-- 在index.vue引入 uni-badge 组件--> <template> <view> <uni-badge text="1"></uni-badge><!-- 3.使用组件 --> </view> </template> <script> // 这里不用import引入,也不需要在components内注册uni-badge组件。template里就可以直接用 export default { data() { return { } } } </script>
|
- 注意:有时我们需要取消勾选’微信开发者工具‘-’详情‘-’本地设置‘-’上传时过滤无依赖文件‘

组件通信
父组件向子组件传递数据
父组件向子组件传递数据可以使用Vue提供的defineProps方法;我们在父组件中引用子组件并传递数据
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <myPageVue name="张三" :content="content"></myPageVue> </template>
<script setup> import { ref } from 'vue'; import myPageVue from '../myPage/myPage.vue'; const content = ref("内容") </script>
<style lang="scss"></style>
|
在子组件中用该方法接收数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <view> 我的页面 </view> <view> <text>父组件传递的name:{{user.name}};</text> <text>父组件传递的content: {{user.content}}</text> </view> </template>
<script setup> import { defineProps } from 'vue'
const user = defineProps({ name: String, content: String }) </script>
<style></style>
|
编译后可以看到效果
子组件向父组件传递数据
实现子组件向父组件中传递数据,我们可以定义一个事件,在这个事件中修改数据的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <view>{{data}}</view> <myPageVue @changeData="changeData"></myPageVue> </template>
<script setup> import { ref } from 'vue'; import myPageVue from '../myPage/myPage.vue'; const data = ref("父组件的原始内容"); const changeData = (val) => { data.value = val } </script>
<style lang="scss"></style>
|
那么问题来了,这个事件中的入参(数据源)是从哪里来的呢?这就是我们接下来要做的事,我们要在子组件中用’defineEmits‘方法(也是由vue框架提供)来’捕获‘父组件中的事件,再编写一个事件,当触发子组件中的事件时,也会调用’defineEmits‘方法,利用该方法将数据传递过去,过程如图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <view> <button @click="handleChangeData">点击修改父组件中的数据</button> </view> </template>
<script setup> import { defineProps, defineEmits } from 'vue'
const user = defineProps({ name: String, content: String });
const emits = defineEmits(['@handleChangeData']); const handleChangeData = () => { emits("changeData", "子组件传递来的内容"); } </script>
<style></style>
|
状态管理 Pinia
前面我们实现了父子组件的相互通信,但是很多时候组件间不一定有依赖关系,而这种情况我们也需要通信;这里我们可以使用Pinia,它是uni-app已经引入的第三方依赖包,可以帮助我们管理项目中的存储数据;简单的说,它实现了数据的集中存储,我们可以在某个组件中修改数据,其他组件中就可以获取修改后的数据;具体使用可以查看官网;
接下来我们看看如何使用Pinia,首先我们需要在main.js中全局引入
1 2 3 4 5 6 7 8 9 10 11 12
| import App from './App' import { createSSRApp } from 'vue'; import * as Pinia from 'pinia';
export function createApp() { const app = createSSRApp(App); app.use(Pinia.createPinia()); return { app, Pinia, }; }
|
接下来,我们新建一个存储文件,在项目的根目录创建‘/stores/counter.js’文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { computed, ref } from 'vue' import { defineStore } from 'pinia';
export const useCounterStore = defineStore( 'counter', () => { const num = ref(0); const increment = () => { num.value++ }; return { num, increment, } } )
|
这里对这个文件进行一下说明,我们引入了pinia依赖,调用其中的defineStore方法返回一个对象useCounterStore,这个对象中有什么呢?首先这个对象有一个全局唯一的名称‘counter’,其次返回一个响应式变量num(初始值为0),一个方法increment(调用该方法可以让num加一);
接下来我们在组件中使用该存储,引入对象‘useCounterStore’,使用其中的变量‘num’和方法‘increment’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <view> cur counter : {{counter.num}} <button @click="doIncrement">点击增加counter</button> </view> </template>
<script setup> import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore();
const doIncrement = () => { counter.increment() } </script>
<style lang="scss"></style>
|
让我们来看看效果
点击按钮,变量将会加1
插槽
我们可以把插槽理解为子组件给父组件留的一块空地,父组件可以随时填充内容。我们在子组件中添加插槽:
1 2 3 4 5 6 7 8
| <template> <view> 。。。其他代码 <slot> <p>这是插槽</p> </slot> </view> </template>
|
在父组件中使用:
1 2 3 4 5 6 7 8
| <template> <view class="content"> 。。。其他代码 <navbar> 父组件通过插槽为子组件赋值 </navbar> </view> </template>
|
插件使用
uni-app框架有丰富的插件市场,我们可以利用来提高开发效率;我们现在来尝试引入商品导航组件,首先来到官网,点击‘下载&安装’

我们已经打开了HBuilderX,直接点击‘下载插件并导入HBuilderX’,

安装成功后,项目目录中会自动添加‘uni_modules’
我们回到组件页面,找到基本用法,将其复制到项目文件中(可以改为vue3的组合式写法,这个看个人习惯)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <template> <uni-goods-nav :fill="true" :options="options" :buttonGroup="buttonGroup" @click="onClick" @buttonClick="buttonClick" /> </template>
<script setup> import { reactive } from 'vue';
const options = reactive( [{ icon: 'headphones', text: '客服' }, { icon: 'shop', text: '店铺', info: 2, infoBackgroundColor: '#007aff', infoColor: "red" }, { icon: 'cart', text: '购物车', info: 2 }, ]);
const buttonGroup = reactive([{ text: '加入购物车', backgroundColor: '#ff0000', color: '#fff' }, { text: '立即购买', backgroundColor: '#ffa200', color: '#fff' } ]);
const onClick = (e) => { uni.showToast({ title: `点击${e.content.text}`, icon: 'none' }) };
const buttonClick = (e) => { console.log(e) options[2].info++ } </script>
<style lang="scss"></style>
|
运行后查看效果

打包上线
微信小程序
在‘微信开发者工具’中运行小程序
查看本文的章节:‘初始准备’-‘运行一个小程序’
上传
点击‘微信开发者工具’右上角的‘上传’按钮(我使用的是测试号,所以无法上传)

第一次上传需要填写版本号和项目备注
上传成功可以看到提示‘代码上传成功’

- 注意:根据微信官方的规定,微信小程序总包大小不能超过20M,分包大小不能超过2M,否则上传会失败;关于总分包的配置可以查看本文的章节:‘页面配置文件介绍’-‘核心配置项详解’-‘subPackages’ ;关于代码包的大小可以点击‘微信开发者工具’的右上角‘详情’-‘本地代码’-‘代码依赖分析’查看


提交审核
上传成功后我们登录微信公众平台,进入‘小程序’-‘版本管理’,找到对应版本,点击‘提交审核’即可提交给微信官方
