乐于分享
好东西不私藏

UniApp元素获取与操作的那些高端骚操作实战

UniApp元素获取与操作的那些高端骚操作实战

书接上篇,我们今天来聊聊UniApp开发中一个非常实用但又容易踩坑的话题:如何获取页面元素并进行操作。支持跨平台(H5/小程序/App),附带实现思路和所有源码示例,以及常见避坑指南和开发实践。

此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。

你可能遇到过这些需求:

  • 点击按钮后,让某个元素平滑滚动到指定位置。

  • 实现一个跟随手指移动的悬浮球。

  • 获取某个组件的位置,然后在其旁边弹出一个菜单。

  • 在父组件中调用子组件的方法。

在纯Vue3中,这些操作通常用refgetBoundingClientRect就能搞定。但在UniApp中,因为要兼容小程序和App,事情就变得复杂了。今天我就带你彻底搞懂在不同平台下如何正确获取和操作元素,并给出一个完整的可拖拽悬浮球组件示例。


一、核心概念:获取元素的两种方式

在UniApp中,获取元素主要有两种途径:

1.1 Vue3的ref(组件/元素引用)

ref是Vue3的标准用法,用于获取组件实例或DOM元素(在H5中)。但在小程序环境中,ref不能直接获取到DOM元素,只能获取到组件实例。

<template>  <viewref="myView">Hello</view></template><scriptsetup>import { ref, onMounted } from 'vue'const myView = ref(null)onMounted(() => {  // H5中:myView.value是DOM元素  // 小程序中:myView.value是一个代理对象,不能直接操作DOM  console.log(myView.value)})</script>

适用场景

  • 获取子组件实例,调用其方法(需配合defineExpose)。

  • H5环境中操作DOM(如设置样式、添加事件)。

跨平台限制:在小程序和App中,ref获取的不是真正的DOM元素,不能直接调用styleclassList等方法。

1.2 UniApp的uni.createSelectorQuery()(获取元素位置信息)

这是UniApp提供的跨平台API,用于查询节点信息(位置、宽高等)。无论H5、小程序还是App,都能返回一致的数据结构。

const query = uni.createSelectorQuery().in(this// 重要:.in(this)指定当前组件上下文query.select('#my-id').boundingClientRect(rect => {  console.log(rect) // { left, top, width, height, ... }}).exec()

适用场景

  • 获取元素相对于视口的位置(如实现动画、弹窗定位)。

  • 判断元素是否在可视区域内(懒加载)。

  • 获取滚动容器的滚动位置。

注意

  • 必须在onReady(页面级)或onMounted(组件级)之后调用,否则可能获取不到。

  • 如果获取的是组件内的元素,一定要加.in(this),否则找不到。

  • 查询是异步的,结果在回调中返回。


二、操作元素的几种方式

获取到元素后,我们想操作它,比如修改样式、添加动画。不同平台的操作方式截然不同。

2.1 H5环境:直接操作DOM

在H5中,你可以用标准的DOM API:

// 获取元素const el = document.querySelector('.my-class')// 修改样式el.style.transform = 'translateX(100px)'// 添加类el.classList.add('active')

2.2 小程序环境:通过数据驱动或动画API

小程序不允许直接操作DOM,必须通过数据绑定或动画API。

方式一:修改数据,驱动视图更新

<template>  <view:style="{ transform: `translateX(${offsetX}px)` }"></view></template><scriptsetup>import { ref } from 'vue'const offsetX = ref(0)// 需要移动时,修改offsetXoffsetX.value = 100</script>

方式二:使用uni.createAnimation创建动画

const animation = uni.createAnimation({  duration300,  timingFunction'ease'})animation.translateX(100).step()this.animationData = animation.export()

然后在模板中绑定animationData到元素的animation属性(小程序特有)。

2.3 App环境(uni-app)

App端(非H5)同样不支持直接DOM操作,但可以通过数据驱动或plus扩展来实现更高级的功能(如原生动画)。通常数据驱动就足够了。


三、组件间方法调用

3.1 父组件调用子组件方法

使用ref获取子组件实例,并通过defineExpose暴露方法。

子组件

<scriptsetup>const doSomething = () => {  console.log('子组件方法执行')}defineExpose({ doSomething })</script>

父组件

<template>  <Childref="childRef" />  <button @click="callChild">调用子组件方法</button></template><scriptsetup>import { ref } from 'vue'const childRef = ref()const callChild = () => {  childRef.value?.doSomething()}</script>

跨平台注意:小程序中同样有效。

3.2 兄弟组件或跨级组件方法调用

可以通过事件总线(uni.$emit/$on)或状态管理(Pinia)实现。前面讲过,不再赘述。


四、难点与注意点(血泪经验)

4.1 .in(this)不能忘

这是最常见的问题!在组件内使用uni.createSelectorQuery时,必须指定当前组件实例,否则查询不到。

// 错误uni.createSelectorQuery().select('#id').exec()// 正确uni.createSelectorQuery().in(this).select('#id').exec()

4.2 查询时机

onLoad中查询元素位置,很可能得到0。因为此时DOM还没渲染完成。正确时机:

  • 页面级:onReady

  • 组件级:onMounted,必要时配合nextTick

    onMounted(() => {  nextTick(() => {    // 确保DOM已更新    const query = uni.createSelectorQuery().in(this)    query.select('.box').boundingClientRect().exec()  })})

4.3 动态生成元素的查询

如果元素是v-if控制的,需要确保它已渲染。可以用nextTickwatch监听数据变化。

4.4 小程序中动画的兼容性

小程序中,uni.createAnimation创建的动画作用于animation属性,但该属性只能用于少数组件(如viewimage)。且动画队列有限,复杂动画建议用数据驱动。

4.5 获取滚动位置

  • 页面滚动:onPageScroll生命周期

  • scroll-view滚动:监听@scroll事件,并通过event.detail.scrollTop获取

    <scroll-view @scroll="onScroll">  ...</scroll-view><script>const onScroll = (e) => {  console.log(e.detail.scrollTop)}</script>

五、完整示例:可拖拽悬浮球组件

下面我们实现一个跨平台的可拖拽悬浮球。点击悬浮球展开菜单,菜单需根据悬浮球位置动态计算弹出方向。这个示例综合了元素位置获取、组件方法调用、动画等知识点。

5.1 组件设计

  • 功能:悬浮球可拖拽,点击展开/收起菜单。

  • 技术点

    • 获取悬浮球位置(用于菜单定位)。

    • 触摸事件处理(拖拽)。

    • 组件内部状态管理。

    • 暴露方法给父组件(如手动收起菜单)。

5.2 组件代码:DraggableFloating.vue

<template>  <viewclass="floating-container">    <!-- 悬浮球 -->    <view      class="floating-ball"      :style="{        left: positionX + 'px',        top: positionY + 'px',        transform: isDragging ? 'none' : 'scale(1)',        transition: isDragging ? 'none' : 'left 0.2s, top 0.2s'      }"      @touchstart="onTouchStart"      @touchmove="onTouchMove"      @touchend="onTouchEnd"      @click="onClick"      ref="ballRef"    >      <textclass="ball-text">{{ isMenuOpen ? '×' : '≡' }}</text>    </view>    <!-- 菜单(根据悬浮球位置动态定位) -->    <view      v-if="isMenuOpen"      class="floating-menu"      :style="{        left: menuLeft + 'px',        top: menuTop + 'px'      }"    >      <viewclass="menu-item" @click="handleMenuItem('item1')">选项1</view>      <viewclass="menu-item" @click="handleMenuItem('item2')">选项2</view>      <viewclass="menu-item" @click="handleMenuItem('item3')">选项3</view>    </view>  </view></template><scriptsetup>import { ref, onMounted, nextTick } from 'vue'const props = defineProps({  // 初始位置  initialX: { typeNumberdefault100 },  initialY: { typeNumberdefault100 },  // 边缘吸附距离  edgeGap: { typeNumberdefault20 }})const emit = defineEmits(['menuSelect'])// 状态const positionX = ref(props.initialX)const positionY = ref(props.initialY)const isDragging = ref(false)const isMenuOpen = ref(false)const ballRef = ref(null)// 菜单位置const menuLeft = ref(0)const menuTop = ref(0)// 触摸起始位置let startX = 0, startY = 0let startPosX = 0, startPosY = 0const onTouchStart = (e) => {  isDragging.value = true  const touch = e.touches[0]  startX = touch.clientX  startY = touch.clientY  startPosX = positionX.value  startPosY = positionY.value}const onTouchMove = (e) => {  if (!isDragging.valuereturn  e.preventDefault() // 防止页面滚动  const touch = e.touches[0]  const deltaX = touch.clientX - startX  const deltaY = touch.clientY - startY  // 更新位置  positionX.value = startPosX + deltaX  positionY.value = startPosY + deltaY}const onTouchEnd = () => {  isDragging.value = false  // 边缘吸附  const screenWidth = uni.getSystemInfoSync().windowWidth  const screenHeight = uni.getSystemInfoSync().windowHeight  let newX = positionX.value  let newY = positionY.value  // 水平边缘吸附  if (newX < props.edgeGap) {    newX = props.edgeGap  } else if (newX + 60 > screenWidth - props.edgeGap) {    newX = screenWidth - 60 - props.edgeGap  }  // 垂直边缘吸附(避免超出屏幕)  newY = Math.max(props.edgeGapMath.min(newY, screenHeight - 60 - props.edgeGap))  positionX.value = newX  positionY.value = newY}// 点击悬浮球(不是拖拽)const onClick = (e) => {  if (!isDragging.value) {    toggleMenu()  }}const toggleMenu = async () => {  if (isMenuOpen.value) {    isMenuOpen.value = false  } else {    // 先获取悬浮球位置,计算菜单位置    await updateMenuPosition()    isMenuOpen.value = true  }}const updateMenuPosition = async () => {  // 获取悬浮球的位置  await nextTick()  const query = uni.createSelectorQuery().in(ballRef.value.$el ? getCurrentInstance().proxy : ballRef.value)  // 注意:ballRef可能是组件实例或DOM,需要正确获取查询上下文  // 更可靠的方式:用统一的ref通过in(this)查询  const inst = getCurrentInstance()  const q = uni.createSelectorQuery().in(inst.proxy)  q.select('.floating-ball').boundingClientRect(rect => {    if (rect) {      const screenWidth = uni.getSystemInfoSync().windowWidth      // 根据悬浮球位置决定菜单向左还是向右展开      if (rect.left < screenWidth / 2) {        menuLeft.value = rect.right      } else {        menuLeft.value = rect.left - 200 // 假设菜单宽200px      }      menuTop.value = rect.top    }  }).exec()}const handleMenuItem = (item) => {  emit('menuSelect', item)  isMenuOpen.value = false}// 暴露方法给父组件(如外部控制收起菜单)defineExpose({  closeMenu() => { isMenuOpen.value = false },  getPosition() => ({ x: positionX.valuey: positionY.value })})</script><stylescoped>.floating-ball {  position: fixed;  width60px;  height60px;  background-color#ff5500;  border-radius50%;  display: flex;  align-items: center;  justify-content: center;  color: white;  font-size30px;  box-shadow0 4px 10px rgba(0,0,0,0.3);  z-index999;  user-select: none;}.ball-text {  line-height1;}.floating-menu {  position: fixed;  background-color: white;  border-radius8px;  box-shadow0 4px 20px rgba(0,0,0,0.2);  padding10px 0;  min-width150px;  z-index1000;}.menu-item {  padding12px 20px;  border-bottom1px solid #f0f0f0;  font-size16px;}.menu-item:last-child {  border-bottom: none;}.menu-item:active {  background-color#f5f5f5;}</style>

5.3 在页面中使用

<template>  <view>    <DraggableFloating      ref="floatingRef"      :initialX="50"      :initialY="100"      @menuSelect="onMenuSelect"    />    <button @click="closeFloatingMenu">外部收起菜单</button>  </view></template><scriptsetup>import { ref } from 'vue'import DraggableFloating from '@/components/DraggableFloating.vue'const floatingRef = ref()const onMenuSelect = (item) => {  console.log('选中菜单项:', item)  uni.showToast({ title`选中${item}`icon'none' })}const closeFloatingMenu = () => {  floatingRef.value?.closeMenu()}</script>

5.4 跨平台测试要点

  • H5:一切正常,触摸事件和位置计算都OK。

  • 微信小程序

    • 需要在manifest.json中开启usingComponents,确保自定义组件正常工作。

    • 注意position: fixed在小程序中表现良好。

    • 拖拽时需加@touchmove.stop阻止页面滚动,我们已经加了e.preventDefault(),但小程序中需配合catchtouchmove?在Vue模板中可以用@touchmove.stop修饰符。

  • App

    • 同样适用,但需测试真机拖拽是否流畅。


六、常见错误及解决方案

❌ 错误1:获取元素位置为0

原因:查询时机过早(如onLoad),或元素未渲染。解决方案:在onReadyonMounted + nextTick中查询。

❌ 错误2:小程序中ref获取不到DOM

原因:小程序没有DOM概念,ref返回的是组件实例,不是元素。解决方案:用uni.createSelectorQuery获取元素信息,不要尝试直接操作。

❌ 错误3:拖拽时页面跟着滚动

原因:触摸事件未阻止默认行为。解决方案:在@touchmove上加@touchmove.stop,或调用e.preventDefault()。注意小程序中需添加catchtouchmove,但在Vue中可以用@touchmove.stop修饰符。

❌ 错误4:动态计算菜单位置时,菜单闪现

原因:先显示了菜单再查询位置,导致菜单先出现在左上角再跳过去。解决方案:先查询位置,再显示菜单(如代码中先await updateMenuPosition(),再设置isMenuOpen = true)。

❌ 错误5:在组件内使用uni.createSelectorQuery找不到元素

原因:没有加.in(this)解决方案:务必加上.in(getCurrentInstance().proxy)

❌ 错误6:H5中ref获取元素后直接操作样式,小程序报错

原因:代码没有条件编译,小程序中尝试了不存在的属性。解决方案:用条件编译区分平台,或统一使用数据驱动。


七、总结与对比

操作类型 Vue3标准做法 UniApp跨平台做法 适用场景
获取组件实例 ref + defineExpose 相同 调用子组件方法
获取DOM元素 ref H5用ref,小程序用selectorQuery 获取位置、宽高
修改样式 el.style.xxx 数据绑定(:style 动态样式
添加动画 CSS动画/requestAnimationFrame CSS动画/uni.createAnimation 简单/复杂动画
获取滚动位置 window.scrollY onPageScroll/@scroll 监听滚动

最后送你一句话:跨平台开发的核心不是找万能的API,而是理解不同平台的限制,用条件编译和通用API写出优雅的代码。希望今天的内容能帮你少走弯路,写出更健壮的UniApp应用。

如果在实际开发中遇到奇怪的问题,欢迎带着你的代码来找我。

—— 一个在元素获取上踩过无数坑的老前辈 !

加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » UniApp元素获取与操作的那些高端骚操作实战

猜你喜欢

  • 暂无文章