基础组件
欢迎回到《uni-app跨平台开发完全指南》系列!在之前的文章中,我们搭好了开发环境,了解了项目目录结构、Vue基础以及基本的样式,这一章节带大家了解基础组件如何使用。掌握了基础组件的使用技巧,就能独立拼装出应用的各个页面了!
一、 初识uni-app组件
在开始之前,先自问下什么是组件?
你可以把它理解为一个封装了结构(WXML)、样式(WXSS)和行为(JS)的、可复用的自定义标签。比如一个按钮、一个导航栏、一个商品卡片,都可以是组件。
uni-app的组件分为两类:
- 基础组件 :框架内置的,如
<view>,<text>,<image>等。这些是官方为我们准备好的标准组件。 - 自定义组件:开发者自己封装的,用于实现特定功能或UI的组件,可反复使用。
就是这些基础组件,它们遵循小程序规范,同时被映射到各端,是实现"一套代码,多端运行"的基础。
为了让大家对基础组件有个全面的认识,参考下面的知识脉络图:
接下来,我们详细介绍下这些内容。
二、 视图与内容:View、Text、Image
这三个组件是构建页面最基础、最核心的部分,几乎无处不在。
2.1 一切的容器:View
<view> 组件是一个视图容器 。它相当于传统HTML中的 <div> 标签,是一个块级元素,主要用于布局和包裹其他内容。
核心特性:
- 块级显示:默认独占一行。
- 样式容器 :通过为其添加
class或style,可以轻松实现Flex布局、Grid布局等。 - 事件容器 :可以绑定各种触摸事件,如
@tap(点击)、@touchstart(触摸开始)等。
以一个简单的Flex布局为例:
html
<!-- 模板部分 -->
<template>
<view class="container">
<view class="header">我是头部</view>
<view class="content">
<view class="left-sidebar">左边栏</view>
<view class="main-content">主内容区</view>
</view>
<view class="footer">我是底部</view>
</view>
</template>
<style scoped>
/* 样式部分 */
.container {
display: flex;
flex-direction: column; /* 垂直排列 */
height: 100vh; /* 满屏高度 */
}
.header, .footer {
height: 50px;
background-color: #007AFF;
color: white;
text-align: center;
line-height: 50px; /* 垂直居中 */
}
.content {
flex: 1; /* 占据剩余所有空间 */
display: flex; /* 内部再启用Flex布局 */
}
.left-sidebar {
width: 100px;
background-color: #f0f0f0;
}
.main-content {
flex: 1; /* 占据content区域的剩余空间 */
background-color: #ffffff;
}
</style>
以上代码:
- 我们通过多个
<view>的嵌套,构建了一个经典的"上-中-下"布局。 - 外层的
.container使用flex-direction: column实现垂直排列。 - 中间的
.content自己也是一个Flex容器,实现了内部的水平排列。 flex: 1是Flex布局的关键,表示弹性扩展,填满剩余空间。
小结一下View:
- 它是布局的骨架,万物皆可
<view>。 - 熟练掌握Flex布局,再复杂的UI也能用
<view>拼出来。
2.2 Text
<text> 组件是一个文本容器 。它相当于HTML中的 <span> 标签,是行内元素。最重要的特点是:只有 <text> 组件内部的文字才是可选中的、长按可以复制!
核心特性:
- 行内显示:默认不会换行。
- 文本专属:用于包裹文本,并对文本设置样式和事件。
- 选择与复制 :支持
user-select属性控制文本是否可选。 - 嵌套与富文本:内部可以嵌套,自身也支持部分HTML实体和富文本。
以一个文本样式与事件为例:
html
<template>
<view>
<!-- 普通的view里的文字无法长按复制 -->
<view>这段文字在view里,无法长按复制。</view>
<!-- text里的文字可以 -->
<text user-select @tap="handleTextTap" class="my-text">
这段文字在text里,可以长按复制!点击我也有反应。
<text style="color: red; font-weight: bold;">我是嵌套的红色粗体文字</text>
</text>
</view>
</template>
<script>
export default {
methods: {
handleTextTap() {
uni.showToast({
title: '你点击了文字!',
icon: 'none'
});
}
}
}
</script>
<style>
.my-text {
color: #333;
font-size: 16px;
/* 注意:text组件不支持设置宽高和margin-top/bottom,因为是行内元素 */
/* 如果需要,可以设置 display: block 或 inline-block */
}
</style>
以上代码含义:
user-select属性开启了文本的可选状态。<text>组件可以绑定@tap事件,而<view>里的纯文字不能。- 内部的
<text>嵌套展示了如何对部分文字进行特殊样式处理。
Text使用小技巧:
- 何时用? 只要是涉及交互(点击、长按)或需要复制功能的文字,必须用
<text>包裹。 - 样式注意 :它是行内元素,设置宽高和垂直方向的margin/padding可能不生效,可通过
display: block改变。 - 性能:避免深度嵌套,尤其是与富文本一起使用时。
2.3 Image
<image> 组件用于展示图片 。它相当于一个增强版的HTML <img>标签,提供了更丰富的功能和更好的性能优化。
核心特性与原理:
- 多种模式 :通过
mode属性控制图片的裁剪、缩放模式,这是它的灵魂所在! - 懒加载 :
lazy-load属性可以在页面滚动时延迟加载图片,提升性能。 - 缓存与 headers:支持配置网络图片的缓存策略和请求头。
mode属性详解(非常重要!) mode属性决定了图片如何适应容器的宽高。我们来画个图理解一下:
下面用一段代码来展示不同Mode的效果
html
<template>
<view>
<view class="image-demo">
<text>scaleToFill (默认,拉伸):</text>
<!-- 容器 200x100,图片会被拉伸 -->
<image src="/static/logo.png" mode="scaleToFill" class="img-container"></image>
</view>
<view class="image-demo">
<text>aspectFit (适应):</text>
<!-- 图片完整显示,上下或左右留白 -->
<image src="/static/logo.png" mode="aspectFit" class="img-container"></image>
</view>
<view class="image-demo">
<text>aspectFill (填充):</text>
<!-- 图片填满容器,但可能被裁剪 -->
<image src="/static/logo.png" mode="aspectFill" class="img-container"></image>
</view>
<view class="image-demo">
<text>widthFix (宽度固定,高度自适应):</text>
<!-- 非常常用!高度会按比例自动计算 -->
<image src="/static/logo.png" mode="widthFix" class="img-auto-height"></image>
</view>
</view>
</template>
<style>
.img-container {
width: 200px;
height: 100px; /* 固定高度的容器 */
background-color: #eee; /* 用背景色看出aspectFit的留白 */
border: 1px solid #ccc;
}
.img-auto-height {
width: 200px;
/* 不设置height,由图片根据widthFix模式自动计算 */
}
.image-demo {
margin-bottom: 20rpx;
}
</style>
Image使用注意:
-
首选
widthFix:在需要图片自适应宽度(如商品详情图、文章配图)时,mode="widthFix"是神器,无需计算高度。 -
** 必设宽高**:无论是直接设置还是通过父容器继承,必须让
<image>有确定的宽高,否则可能显示异常。 -
加载失败处理 :使用
@error事件监听加载失败,并设置默认图。html<image :src="avatarUrl" @error="onImageError" class="avatar"></image>javascriptonImageError() { this.avatarUrl = '/static/default-avatar.png'; // 替换为默认头像 } -
性能优化 :对于列表图片,务必加上
lazy-load。
三、 按钮与表单组件
应用不能只是展示,更需要与用户交互。
3.1 Button
<button> 组件用于捕获用户的点击操作。它功能强大,样式多样,甚至能直接调起系统的某些功能。
核心特性
- 多种类型 :通过
type属性控制基础样式,如default(默认)、primary(主要)、warn(警告)。 - 开放能力 :通过
open-type属性可以直接调起微信的获取用户信息、分享、客服等功能。 - 样式自定义 :虽然提供了默认样式,但可以通过
hover-class等属性实现点击反馈,也可以通过CSS完全自定义。
用一段代码来展示各种按钮:
html
<template>
<view class="button-group">
<!-- 基础样式按钮 -->
<button type="default">默认按钮</button>
<button type="primary">主要按钮</button>
<button type="warn">警告按钮</button>
<!-- 禁用状态 -->
<button :disabled="true" type="primary">被禁用的按钮</button>
<!-- 加载状态 -->
<button loading type="primary">加载中...</button>
<!-- 获取用户信息 -->
<button open-type="getUserInfo" @getuserinfo="onGetUserInfo">获取用户信息</button>
<!-- 分享 -->
<button open-type="share">分享</button>
<!-- 自定义样式 - 使用 hover-class -->
<button class="custom-btn" hover-class="custom-btn-hover">自定义按钮</button>
</view>
</template>
<script>
export default {
methods: {
onGetUserInfo(e) {
console.log('用户信息:', e.detail);
// 在这里处理获取到的用户信息
}
}
}
</script>
<style>
.button-group button {
margin-bottom: 10px; /* 给按钮之间加点间距 */
}
.custom-btn {
background-color: #4CD964; /* 绿色背景 */
color: white;
border: none; /* 去除默认边框 */
border-radius: 10px; /* 圆角 */
}
.custom-btn-hover {
background-color: #2AC845; /* hover时更深的绿色 */
}
</style>
Button要点:
open-type:这是uni-app和小程序生态打通的关键,让你能用一行代码实现复杂的原生功能。- 自定义样式 :默认按钮样式可能不符合设计,记住一个原则:先重置,再定义 。使用
border: none; background: your-color;来覆盖默认样式。 - 表单提交 :在
<form>标签内,<button>的form-type属性可以指定为submit或reset。
3.2 表单组件 - Input, Checkbox, Radio, Picker...
表单用于收集用户输入。uni-app提供了一系列丰富的表单组件。
Input - 文本输入框
核心属性:
v-model:双向绑定输入值,最常用!type:输入框类型,如text,number,idcard,password等。placeholder:占位符。focus:自动获取焦点。@confirm:点击完成按钮时触发。
下面写一个登录输入框:
html
<template>
<view class="login-form">
<input v-model="username" type="text" placeholder="请输入用户名" class="input-field" />
<input v-model="password" type="password" placeholder="请输入密码" class="input-field" @confirm="onLogin" />
<button type="primary" @tap="onLogin">登录</button>
</view>
</template>
<script>
export default {
data() {
return {
username: '',
password: ''
};
},
methods: {
onLogin() {
// 验证用户名和密码
if (!this.username || !this.password) {
uni.showToast({ title: '请填写完整', icon: 'none' });
return;
}
console.log('登录信息:', this.username, this.password);
// 发起登录请求...
}
}
}
</script>
<style>
.input-field {
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
height: 40px;
}
</style>
Checkbox 与 Radio - 选择与单选
这两个组件需要和<checkbox-group>, <radio-group>一起使用,来管理一组选项。
代码实战:选择兴趣爱好
html
<template>
<view>
<text>请选择你的兴趣爱好:</text>
<checkbox-group @change="onHobbyChange">
<label class="checkbox-label">
<checkbox value="reading" :checked="true" /> 阅读
</label>
<label class="checkbox-label">
<checkbox value="music" /> 音乐
</label>
<label class="checkbox-label">
<checkbox value="sports" /> 运动
</label>
</checkbox-group>
<view>已选:{{ selectedHobbies.join(', ') }}</view>
<text>请选择性别:</text>
<radio-group @change="onGenderChange">
<label class="radio-label">
<radio value="male" /> 男
</label>
<label class="radio-label">
<radio value="female" /> 女
</label>
</radio-group>
<view>已选:{{ selectedGender }}</view>
</view>
</template>
<script>
export default {
data() {
return {
selectedHobbies: ['reading'], // 默认选中阅读
selectedGender: ''
};
},
methods: {
onHobbyChange(e) {
// e.detail.value 是一个数组,包含所有被选中的checkbox的value
this.selectedHobbies = e.detail.value;
console.log('兴趣爱好变化:', e.detail.value);
},
onGenderChange(e) {
// e.detail.value 是单个被选中的radio的value
this.selectedGender = e.detail.value;
console.log('性别变化:', e.detail.value);
}
}
}
</script>
<style>
.checkbox-label, .radio-label {
display: block;
margin: 5px 0;
}
</style>
表单组件使用技巧:
- 善用
v-model:能够极大简化双向数据绑定的代码。 - 理解事件 :
checkbox和radio的change事件发生在组(group) 上,通过e.detail.value获取所有值。 - UI统一:原生组件样式在各端可能略有差异,对于要求高的场景,可以考虑使用UI库(如uView)的自定义表单组件。
四、 导航与容器组件
当应用内容变多,我们需要更好的方式来组织页面结构和实现页面跳转。
4.1 Navigator
<navigator> 组件是一个页面链接 ,用于在应用内跳转到指定页面。它相当于HTML中的 <a> 标签,但功能更丰富。
核心属性与跳转模式:
url:必填,指定要跳转的页面路径。open-type:跳转类型 ,决定了跳转行为。navigate:默认值,保留当前页面,跳转到新页面(可返回)。redirect:关闭当前页面,跳转到新页面(不可返回)。switchTab:跳转到tabBar页面,并关闭所有非tabBar页面。reLaunch:关闭所有页面,打开到应用内的某个页面。navigateBack:关闭当前页面,返回上一页面或多级页面。
delta:当open-type为navigateBack时有效,表示返回的层数。
为了更清晰地理解这几种跳转模式对页面栈的影响,我画了下面这张图:

下面用代码实现一个简单的导航
html
<template>
<view class="nav-demo">
<!-- 普通跳转,可以返回 -->
<navigator url="/pages/about/about" hover-class="navigator-hover">
<button>关于我们(普通跳转)</button>
</navigator>
<!-- 重定向,无法返回 -->
<navigator url="/pages/index/index" open-type="redirect">
<button type="warn">回首页(重定向)</button>
</navigator>
<!-- 跳转到TabBar页面 -->
<navigator url="/pages/tabbar/my/my" open-type="switchTab">
<button type="primary">个人中心(Tab跳转)</button>
</navigator>
<!-- 返回上一页 -->
<navigator open-type="navigateBack">
<button>返回上一页</button>
</navigator>
<!-- 返回上两页 -->
<navigator open-type="navigateBack" :delta="2">
<button>返回上两页</button>
</navigator>
</view>
</template>
<style>
.nav-demo button {
margin: 10rpx;
}
.navigator-hover {
background-color: #f0f0f0; /* 点击时的反馈色 */
}
</style>
Navigator避坑:
url路径:必须以/开头,在pages.json中定义。- 跳转TabBar :必须使用
open-type="switchTab",否则无效。 - 传参 :可以在
url后面拼接参数,如/pages/detail/detail?id=1&name=test,在目标页面的onLoad生命周期中通过options参数获取。 - 跳转限制 :小程序中页面栈最多十层,注意使用
redirect避免层级过深。
4.2 Scroll-View
<scroll-view> 是一个可滚动的视图容器。当内容超过容器高度(或宽度)时,提供滚动查看的能力。
核心特性:
- 滚动方向 :通过
scroll-x(横向)和scroll-y(纵向)控制。 - 滚动事件 :可以监听
@scroll事件,获取滚动位置。 - 上拉加载/下拉刷新 :通过
@scrolltolower和@scrolltoupper等事件模拟,但更推荐使用页面的onReachBottom和onPullDownRefresh。
代码实现一个横向滚动导航和纵向商品列表
html
<template>
<view>
<!-- 横向滚动导航 -->
<scroll-view scroll-x class="horizontal-scroll">
<view v-for="(item, index) in navList" :key="index" class="nav-item">
{{ item.name }}
</view>
</scroll-view>
<!-- 纵向滚动商品列表 -->
<scroll-view scroll-y :style="{ height: scrollHeight + 'px' }" @scrolltolower="onLoadMore">
<view v-for="(product, idx) in productList" :key="idx" class="product-item">
<image :src="product.image" mode="aspectFill" class="product-img"></image>
<text class="product-name">{{ product.name }}</text>
</view>
<view v-if="loading" class="loading-text">加载中...</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
navList: [ /* ... 导航数据 ... */ ],
productList: [ /* ... 商品数据 ... */ ],
scrollHeight: 0,
loading: false
};
},
onLoad() {
// 动态计算scroll-view的高度,使其充满屏幕剩余部分
const sysInfo = uni.getSystemInfoSync();
// 假设横向导航高度为50px,需要根据实际情况计算
this.scrollHeight = sysInfo.windowHeight - 50;
},
methods: {
onLoadMore() {
// 加载更多
if (this.loading) return;
this.loading = true;
console.log('开始加载更多数据...');
// 请求数据
setTimeout(() => {
// ... 获取新数据并拼接到productList ...
this.loading = false;
}, 1000);
}
}
}
</script>
<style>
.horizontal-scroll {
white-space: nowrap; /* 让子元素不换行 */
width: 100%;
background-color: #f7f7f7;
}
.nav-item {
display: inline-block; /* 让子元素行内排列 */
padding: 10px 20px;
margin: 5px;
background-color: #fff;
border-radius: 15px;
}
.product-item {
display: flex;
padding: 10px;
border-bottom: 1px solid #eee;
}
.product-img {
width: 80px;
height: 80px;
border-radius: 5px;
}
.product-name {
margin-left: 10px;
align-self: center;
}
.loading-text {
text-align: center;
padding: 10px;
color: #999;
}
</style>
Scroll-View使用心得:
- 横向滚动 :牢记两个CSS:容器
white-space: nowrap;,子项display: inline-block;。 - 性能 :
<scroll-view>内不适合放过多或过于复杂的子节点,尤其是图片,可能导致滚动卡顿。对于长列表,应使用官方的<list>组件或社区的长列表组件。 - 高度问题 :纵向滚动的
<scroll-view>必须有一个固定的高度,否则会无法滚动。通常通过JS动态计算。
五、 自定义组件基础
当项目变得复杂,我们会发现很多UI模块或功能块在重复编写。这时,就该自定义组件了!它能将UI和功能封装起来,实现复用和解耦。
5.1 为什么要用自定义组件?
- 复用性:一次封装,到处使用。
- 可维护性:功能集中在一处,修改方便。
- 清晰性:将复杂页面拆分成多个组件,结构清晰,便于协作。
5.2 创建与使用一个自定义组件
让我们来封装一个简单的UserCard组件。
第一步:创建组件文件 在项目根目录创建components文件夹,然后在里面创建user-card/user-card.vue文件。uni-app会自动识别components目录下的组件。
第二步:编写组件模板、逻辑与样式
html
<!-- components/user-card/user-card.vue -->
<template>
<view class="user-card" @tap="onCardClick">
<image :src="avatarUrl" class="avatar" mode="aspectFill"></image>
<view class="info">
<text class="name">{{ name }}</text>
<text class="bio">{{ bio }}</text>
</view>
<view class="badge" v-if="isVip">VIP</view>
</view>
</template>
<script>
export default {
// 声明组件的属性,外部传入的数据
props: {
avatarUrl: {
type: String,
default: '/static/default-avatar.png'
},
name: {
type: String,
required: true
},
bio: String, // 简写方式,只定义类型
isVip: Boolean
},
// 组件内部数据
data() {
return {
// 这里放组件自己的状态
};
},
methods: {
onCardClick() {
// 触发一个自定义事件,通知父组件
this.$emit('cardClick', { name: this.name });
// 也可以在这里处理组件内部的逻辑
uni.showToast({
title: `点击了${this.name}的名片`,
icon: 'none'
});
}
}
}
</script>
<style scoped>
.user-card {
display: flex;
padding: 15px;
background-color: #fff;
border-radius: 8px;
margin: 10px;
position: relative;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.avatar {
width: 50px;
height: 50px;
border-radius: 25px;
}
.info {
display: flex;
flex-direction: column;
margin-left: 12px;
justify-content: space-around;
}
.name {
font-size: 16px;
font-weight: bold;
}
.bio {
font-size: 12px;
color: #999;
}
.badge {
position: absolute;
top: 10px;
right: 10px;
background-color: #ffd700;
color: #333;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
}
</style>
第三步:在页面中使用组件
html
<!-- pages/index/index.vue -->
<template>
<view>
<text>用户列表</text>
<!-- 使用自定义组件 -->
<!-- 1. 通过属性传递数据 -->
<user-card
name="码小明"
bio="热爱编程"
:is-vip="true"
avatar-url="/static/avatar1.jpg"
@cardClick="onUserCardClick" <!-- 2. 监听子组件发出的自定义事件 -->
/>
<user-card
name="产品经理小鱼儿"
bio="让世界更美好"
:is-vip="false"
@cardClick="onUserCardClick"
/>
</view>
</template>
<script>
// 2. 导入组件
// import UserCard from '@/components/user-card/user-card.vue';
export default {
// 3. 注册组件
// components: { UserCard },
methods: {
onUserCardClick(detail) {
console.log('父组件收到了卡片的点击事件:', detail);
// 这里可以处理跳转逻辑
// uni.navigateTo({ url: '/pages/user/detail?name=' + detail.name });
}
}
}
</script>
5.3 核心概念:Props, Events, Slots
一个完整的自定义组件通信机制,主要围绕这三者展开。它们的关系可以用下图清晰地表示:

- Props(属性) :由外到内 的数据流。父组件通过属性的方式将数据传递给子组件。子组件用
props选项声明接收。 - Events(事件) :由内到外 的通信。子组件通过
this.$emit('事件名', 数据)触发一个自定义事件,父组件通过v-on或@来监听这个事件。 - Slots(插槽) :内容分发。父组件可以将一段模板内容"插入"到子组件指定的位置。这极大地增强了组件的灵活性。
插槽(Slot)简单示例: 假设我们的UserCard组件,想在bio下面留一个区域给父组件自定义内容。
在子组件中:
html
<!-- user-card.vue -->
<view class="info">
<text class="name">{{ name }}</text>
<text class="bio">{{ bio }}</text>
<!-- 默认插槽,父组件传入的内容会渲染在这里 -->
<slot></slot>
<!-- 具名插槽 -->
<!-- <slot name="footer"></slot> -->
</view>
在父组件中:
html
<user-card name="小明" bio="...">
<!-- 传入到默认插槽的内容 -->
<view style="margin-top: 5px;">
<button size="mini">关注</button>
</view>
<!-- 传入到具名插槽footer的内容 -->
<!-- <template v-slot:footer> ... </template> -->
</user-card>
5.4 EasyCom
你可能会注意到,在上面的页面中,我们并没有import和components注册,但组件却正常使用了。这是因为uni-app的 easycom 规则。
规则 :只要组件安装在项目的components目录下,并符合components/组件名称/组件名称.vue的目录结构,就可以不用手动引入和注册,直接在页面中使用。极大地提升了开发效率!
六、 内容总结
至此基本组件内容就介绍完了,又到了总结的时候了,本节主要内容:
- View、Text、Image :构建页面的三大核心组件。注意图片Image的
mode属性。 - Button与表单组件 :与用户交互的核心。Button的
open-type能调起强大原生功能。表单组件用v-model实现数据双向绑定。 - Navigator与Scroll-View :组织页面和内容。Navigator负责路由跳转,要理解五种
open-type的区别。Scroll-View提供滚动区域,要注意它的高度和性能问题。 - 自定义组件 :必会内容。理解了
Props下行、Events上行、Slots分发的数据流,你就掌握了组件通信的精髓。easycom规则让组件使用更便捷。
如果你觉得这篇文章对你有所帮助,能够对uni-app的基础组件有更清晰的认识,不要吝啬你的"一键三连"(点赞、关注、收藏)哦(手动狗头)!你的支持是我持续创作的最大动力。 在学习过程中遇到任何问题,或者有哪里没看明白,都欢迎在评论区留言,我会尽力解答。
版权声明:本文为【《uni-app跨平台开发完全指南》】系列第五篇,原创文章,转载请注明出处。