Chapter 03

模板语法与内置组件

掌握 uni-app 核心内置组件,理解它们与 HTML 标签的本质区别,学会 easycom 自动引入

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 模式的兼容

在 H5 编译目标下,uni-app 的内置组件会被编译为对应的 HTML 元素(viewdiv),因此即使你使用 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>
hover-class
按下时应用的样式类名,用于模拟点击反馈效果。当值为 none 时禁用。默认值为 none
hover-stay-time
手指松开后 hover 态保持的时间(毫秒)。适当延长可让用户感知到点击操作已被响应。
@tap
uni-app 的点击事件。在 App/小程序端对应原生 tap 事件,在 H5 端对应 click 事件(框架自动处理)。

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 的高度限制

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 实体,如 &amp; → & -->
  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 的平台差异

inputtype 属性在不同平台调起的键盘不同。number(纯数字键盘)、digit(带小数点的数字键盘)、idcard(身份证键盘)、safe-password(密码键盘)等类型会调用平台的原生键盘,提升用户体验。

3.5 easycom — 组件自动引入

easycom 是 uni-app 的一项核心优化机制,允许开发者在无需手动 import 和注册的情况下直接使用组件。这极大地减少了模板文件顶部的 import 代码量。

工作原理

easycom 遵循两个规则:

  1. 规则一(自动扫描):凡是放在 components/组件名/组件名.vue 路径下的组件,无需注册即可使用。例如 components/MyButton/MyButton.vue 可以直接用 <MyButton />
  2. 规则二(自定义规则):在 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 vs @click
在 App 和小程序端,推荐使用 @tap(原生触摸事件,响应更快)。@click 在 H5 端对应 click 事件,在其他端会被转换为 tap。实际开发中两者均可使用,框架会自动处理兼容。
事件修饰符
支持 .stop(阻止冒泡)、.prevent(H5 端阻止默认行为)、.once(只触发一次)。注意 .native 修饰符在 Vue 3 中已废弃。
事件对象 $event
在模板中内联处理时,$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-ifv-show 在 uni-app 中的行为与 Vue 一致,但有性能上的跨端差异需要注意:

小程序端 v-show 的限制

在微信小程序端,v-show 通过设置 CSS display: none 来隐藏元素,但某些小程序原生组件(如 video、map、camera)无法通过 CSS 隐藏,必须使用 v-if 彻底销毁节点。这是一个常见的跨端坑点。

3.9 小结