3.1 为什么不用 div/span?
初次接触 uni-app 的开发者会有一个困惑:为什么不能用熟悉的 <div>、<span>、<p> 等 HTML 标签?
原因在于 uni-app 需要编译到多个平台。HTML 标签在 H5 中有效,但微信小程序没有 DOM,它有自己的 WXML 标签体系(<view>、<text>等)。uni-app 的解决方案是:定义一套跨平台组件规范,开发者使用这套规范编写代码,编译器再将其转换为各平台的原生实现。
HTML 标签(仅 H5)
div→ 块级容器span→ 行内文本img→ 图片input→ 输入框button→ 按钮
uni-app 内置组件(全平台)
view→ 块级容器text→ 行内文本image→ 图片input→ 输入框button→ 按钮
在 H5 编译目标下,uni-app 的内置组件会被编译为对应的 HTML 元素(view → div),因此即使你使用 uni-app 组件,H5 端的渲染结果仍然是标准 HTML,对 SEO 和无障碍访问没有影响。
3.2 核心容器组件
view — 万能容器
view 是使用频率最高的组件,相当于 HTML 的 div。它是一个块级容器,支持 Flexbox 布局。
<view
class="card"
hover-class="card--active"
hover-stay-time="70"
@tap="handleTap"
>
<text>点击我</text>
</view>
none 时禁用。默认值为 none。scroll-view — 可滚动容器
scroll-view 是实现局部滚动区域的核心组件,支持纵向和横向滚动。
<!-- 纵向滚动列表 -->
<scroll-view
scroll-y
:scroll-top="scrollTop"
:style="{ height: '500rpx' }"
@scrolltolower="loadMore"
@refresherrefresh="onRefresh"
refresher-enabled
:refresher-triggered="isRefreshing"
>
<view v-for="item in list" :key="item.id" class="list-item">
<text>{{ item.title }}</text>
</view>
<view v-if="loading"><text>加载中...</text></view>
</scroll-view>
<!-- 横向滚动标签栏 -->
<scroll-view scroll-x class="tab-bar">
<view v-for="tab in tabs" :key="tab.id" class="tab-item">
<text>{{ tab.name }}</text>
</view>
</scroll-view>
scroll-view 必须设置固定高度(或通过 flex 布局限定高度),否则内容会溢出而不产生滚动效果。在 App 端推荐使用 list-view(uni-app x 的高性能列表组件)代替 scroll-view 渲染大量数据。
3.3 文本与多媒体组件
text — 文本组件
text 是行内文本组件,对应 HTML 的 span。在小程序和 App 端,文本内容必须放在 text 组件中,不能直接放在 view 中(H5 允许,但跨端最佳实践不推荐)。
<text
selectable <!-- 允许用户长按选择文本 -->
:decode="true" <!-- 解码 HTML 实体,如 & → & -->
space="emsp" <!-- 连续空格的显示方式 -->
lines="2" <!-- 限制显示行数(nvue/uvue 支持)-->
>
这是可选中的文本,超长时会自动换行
</text>
image — 图片组件
image 是功能强大的图片组件,内置裁剪、缩放模式,以及懒加载支持。
<image
src="https://example.com/photo.jpg"
mode="aspectFill"
lazy-load
:style="{ width: '200rpx', height: '200rpx' }"
@load="onImageLoad"
@error="onImageError"
/>
mode 属性控制图片的裁剪和缩放行为:
| mode 值 | 说明 |
|---|---|
scaleToFill | 拉伸填满(默认,可能变形) |
aspectFit | 保持比例,长边对齐(可能有留白) |
aspectFill | 保持比例,短边对齐(常用于头像/封面,会裁剪) |
widthFix | 宽度固定,高度自动(适合文章图片) |
heightFix | 高度固定,宽度自动 |
3.4 表单组件
uni-app 提供了完整的表单组件体系,在各平台都有对应的原生实现:
<form @submit="handleSubmit">
<!-- 单行文本输入 -->
<input
v-model="form.name"
type="text"
placeholder="请输入姓名"
maxlength="20"
:adjust-position="true"
/>
<!-- 多行文本 -->
<textarea
v-model="form.content"
placeholder="请输入内容"
auto-height
:maxlength="500"
/>
<!-- Switch 开关 -->
<switch
:checked="form.notify"
@change="form.notify = $event.detail.value"
color="#0ea5e9"
/>
<!-- Slider 滑块 -->
<slider
:value="form.volume"
@change="form.volume = $event.detail.value"
min="0"
max="100"
show-value
/>
<!-- Picker 选择器 -->
<picker
mode="selector"
:range="cities"
:value="cityIndex"
@change="cityIndex = $event.detail.value"
>
<view><text>{{ cities[cityIndex] }}</text></view>
</picker>
<button form-type="submit">提交</button>
</form>
input 的 type 属性在不同平台调起的键盘不同。number(纯数字键盘)、digit(带小数点的数字键盘)、idcard(身份证键盘)、safe-password(密码键盘)等类型会调用平台的原生键盘,提升用户体验。
3.5 easycom — 组件自动引入
easycom 是 uni-app 的一项核心优化机制,允许开发者在无需手动 import 和注册的情况下直接使用组件。这极大地减少了模板文件顶部的 import 代码量。
工作原理
easycom 遵循两个规则:
- 规则一(自动扫描):凡是放在
components/组件名/组件名.vue路径下的组件,无需注册即可使用。例如components/MyButton/MyButton.vue可以直接用<MyButton />。 - 规则二(自定义规则):在
pages.json中配置easycom字段,通过正则匹配组件名与路径的对应关系。
// pages.json 中配置 easycom
{
"easycom": {
"autoscan": true,
"custom": {
// 正则匹配以 u- 开头的组件,映射到 uni-ui 库
"^u-(.*)": "@/uni_modules/uview-plus/components/u-$1/u-$1.vue",
// 匹配以 van- 开头的组件,映射到 vant-weapp
"^van-(.*)": "@/wxcomponents/vant/dist/$1/index"
}
}
}
使用效果对比
<!-- 没有 easycom:需要手动 import 和注册 -->
<script setup>
import MyButton from '@/components/MyButton/MyButton.vue'
import ProductCard from '@/components/ProductCard/ProductCard.vue'
import UAvatar from '@/components/UAvatar/UAvatar.vue'
</script>
<!-- 有 easycom:直接使用,零配置 -->
<template>
<MyButton>按钮</MyButton>
<ProductCard :data="item" />
<UAvatar :src="user.avatar" />
</template>
<!-- <script setup> 无需任何 import -->
3.6 列表渲染最佳实践
在 uni-app 中渲染列表时,除了基础的 v-for,还需要关注性能优化:
<template>
<scroll-view scroll-y style="height: 100vh" @scrolltolower="loadMore">
<!-- 使用稳定的 key,避免使用 index -->
<ProductCard
v-for="item in products"
:key="item.id"
:product="item"
@click="goDetail(item.id)"
/>
<!-- 加载状态 -->
<view class="loading-footer">
<text v-if="loading">加载中...</text>
<text v-else-if="noMore">— 已加载全部 —</text>
</view>
</scroll-view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
interface Product {
id: number
name: string
price: number
}
const products = ref<Product[]>([])
const loading = ref(false)
const noMore = ref(false)
let page = 1
async function loadMore() {
if (loading.value || noMore.value) return
loading.value = true
try {
const res = await uni.request({
url: `/api/products?page=${page}&size=20`
})
const data = res.data as { list: Product[]; hasMore: boolean }
products.value.push(...data.list)
noMore.value = !data.hasMore
page++
} finally {
loading.value = false
}
}
onLoad(() => { loadMore() })
</script>
3.7 事件系统详解
uni-app 的事件系统与 Vue 3 基本一致,但有一些跨端注意事项:
@tap(原生触摸事件,响应更快)。@click 在 H5 端对应 click 事件,在其他端会被转换为 tap。实际开发中两者均可使用,框架会自动处理兼容。.stop(阻止冒泡)、.prevent(H5 端阻止默认行为)、.once(只触发一次)。注意 .native 修饰符在 Vue 3 中已废弃。$event 代表事件对象。对于表单组件(slider、switch),数据在 $event.detail.value 中。<template>
<!-- 阻止事件冒泡 -->
<view @tap="parentTap">
<view @tap.stop="childTap">
<text>子元素(不冒泡到父元素)</text>
</view>
</view>
<!-- 内联传参 -->
<view
v-for="(item, index) in list"
:key="item.id"
@tap="deleteItem(index, $event)"
>
<text>{{ item.name }}</text>
</view>
</template>
3.8 条件渲染与 v-show
v-if 和 v-show 在 uni-app 中的行为与 Vue 一致,但有性能上的跨端差异需要注意:
在微信小程序端,v-show 通过设置 CSS display: none 来隐藏元素,但某些小程序原生组件(如 video、map、camera)无法通过 CSS 隐藏,必须使用 v-if 彻底销毁节点。这是一个常见的跨端坑点。
3.9 小结
- uni-app 使用自有内置组件(view/text/image 等)代替 HTML 标签,确保跨端编译
scroll-view是局部滚动的核心,需要设置固定高度并注意大数据量时的性能image组件的mode属性非常重要,aspectFill最常用于头像/封面- easycom 极大简化了组件使用,遵循
components/名称/名称.vue路径规范即可零配置使用 - 列表渲染使用稳定的
:key(业务 ID),避免使用数组索引 - 事件处理推荐
@tap,可以用.stop修饰符阻止冒泡