主题
uni-app 生命周期与适配(二)
本页对应 uni-app 生命周期与适配,共 6 段视频,约 46 分钟。
学习目标
- 能够按 uni-app 自定义组件规范创建
custom-tabs组件,完成组件命名、目录组织、模板与样式抽离,并在页面中通过 easycom 规则直接使用。 - 能够为
custom-tabs定义props接收外部传入的数组数据,使用v-for动态渲染标签项,并通过页面传参在调试工具中验证数据已正确传入。 - 能够为标签项绑定点击事件并传递索引,使用
ref维护当前选中索引,通过动态class实现“点谁谁高亮”的交互效果。 - 能够使用
uni.createSelectorQuery结合getCurrentInstance()查询每个标签节点的left与width信息,并通过控制台验证查询结果。 - 能够将节点位置结果提取为组件内可复用的数据,按
left和width处理每个标签的位置数据,为下划线游标居中及移动计算提供依据。
知识点
先明确组件的边界与命名
为什么需要:custom-tabs 后面项目还会继续用,如果每个人随意命名,自动导入和协作都会乱。转写稿里强调统一叫 custom-tabs,并放在 components 目录下。
怎么想:公共、全局组件放 components,页面私有小组件也可以正常导入;但既然本节要练 easycom 规范,就直接按规范建组件。
怎么做:在 components 下新建 custom-tabs/custom-tabs.vue,组件名与目录名保持规范一致。页面中使用时不需要手动注册、导入,直接写 <custom-tabs /> 即可获得提示。
易错点:组件名不要随便叫“张三李四”;忘记加 <script setup> 会导致后续逻辑写法不对;组件目录和文件名不规范时,自动导入可能失效。
从页面布局抽离模板与样式
为什么需要:先把结构写出来,能直观看到 tab 长什么样;再抽成组件,页面会更干净,组件也便于复用。转写稿的做法是:先在首页 index 的配置区域下面放盒子,不要把内容粘到最外层,再把相关样式追加进去。
怎么想:组件本质仍是标准 Vue 组件:模板、样式、逻辑。写组件时没有任何“特殊语法”,只是把页面里的一段结构搬进来。
怎么做:把 tab 区域模板放进组件 <template>,把从相关注释往下的样式放进 <style>;页面里只保留组件标签。组件模板可写成:
vue
<template>
<view class="custom-tabs">
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
:class="{ active: tabIndex === index }"
@click="onTab(index)"
>
{{ item.text }}
</view>
<view class="tab-line" :style="{ left: lineLeft + 'px' }"></view>
</view>
</template>易错点:粘样式时不要整页乱粘,避免污染;模板不要多套一层无关盒子;抽离后检查页面是否仍能正常显示。
用 props 接收外部数据
为什么需要:tab 文字不能写死,“关注、推荐、护肤”等应由外部传入。转写稿明确:组件内部不负责查接口,外部给什么就展示什么。
怎么想:外部数据传到组件内部,最直接的方式是组件属性,也就是 props。组合式 API 中用 defineProps 定义。
怎么做:组件内定义 list 为数组;页面用 ref 定义数据并传进去:
js
// custom-tabs.vue
const props = defineProps({
list: Array
})
// 页面 index.vue
import { ref } from 'vue'
const feedTabs = ref([
{ text: '关注' },
{ text: '推荐' },
{ text: '护肤' }
])vue
<custom-tabs :list="feedTabs" />调试时可在浏览器控制台选中组件,查看 props 中是否真的有三个数据;有数据说明传递成功。
易错点:忘记导入 ref;defineProps 是固定语法;外部传数组时不要漏掉绑定冒号;ref 使用时要记得 .value。
点击高亮与索引判断
为什么需要:点谁谁高亮,必须知道点击的是第几个 tab。转写稿用索引值区分。
怎么想:先监听点击事件,把 index 传出来;再用一个变量 tabIndex 记录当前选中项;类名 active 根据 tabIndex === index 决定是否添加。
怎么做:
js
import { ref } from 'vue'
const tabIndex = ref(0)
function onTab(index) {
tabIndex.value = index
}模板中写 :class="{ active: tabIndex === index }"。默认 0 表示第一个高亮。
易错点:组合式 API 中事件回调就是普通函数;改变量要写 tabIndex.value = index,不要写成 this.tabIndex;类名判断不要永远写死 true。
查询节点位置并整理数据
为什么需要:下划线要跟着 tab 移动,必须先拿到每个 tab 距左边的位置;还要拿宽度,才能让下划线在 tab 内居中。转写稿的推导是:大宽度减小宽度再除以 2,得到居中偏移。
怎么想:用 uni.createSelectorQuery() 查询 .tab 节点,获取 boundingClientRect 信息。自定义组件中要加 .in(getCurrentInstance()),因为组合式 API 里没有选项式里的 this。
怎么做:
js
import { ref, onMounted, getCurrentInstance } from 'vue'
const tabBarRect = ref([])
onMounted(() => {
const query = uni.createSelectorQuery().in(getCurrentInstance())
query.selectAll('.tab').boundingClientRect(res => {
tabBarRect.value = res.map(({ width, left }) => ({ width, left }))
}).exec()
})调试时在控制台查看 tabBarRect,应看到每项只保留 width 和 left。转写稿提醒:onMounted 里的局部结果外部用不到,所以要定义外层 ref 再赋值。
易错点:忘记 .exec();忘记 .in(getCurrentInstance());组合式 API 不要写 this;map 返回对象时注意 { width, left } 是简写;ref 赋值不要漏 .value。
先把组件放对位置:公共组件与页面私有组件
为什么需要:开发时如果所有组件都随意放,后面项目一大就难找、难复用。转写稿里强调:公共的、全局的组件按规范放到组件目录;某个页面自己的小组件,也可以正常导入使用。
怎么想:先判断组件复用范围。custom-tabs 后面项目还会用,所以统一命名并放到公共组件目录,避免“你叫张三、他叫李四”导致对不上。
怎么做:在组件目录下新建 custom-tabs,组件名统一。公共组件遵循 uni-app 组件规范后,页面里通常不需要手动注册、导入,直接写标签即可;页面私有组件则按普通导入方式使用。
易错点:不要因为能自动导入就把所有东西都塞进公共目录;页面私有组件强行放公共目录也能跑,但不利于维护。
把页面结构抽成组件:模板、样式、setup
为什么需要:先在页面里写布局能快速看效果,但最终要复用,就要把模板和样式抽到组件内部。
怎么想:转写稿的思路是“先把布局结构放到页面,再抽成组件也可以;既然组件规范讲完了,就直接在组件里写逻辑”。组件仍然是标准 Vue 组件:有模板、有样式、有逻辑。
怎么做:把页面中 tabs 相关模板移到 custom-tabs.vue 的 <template>,把对应样式移到 <style>,并补上 <script setup>。父页面中改为使用 <custom-tabs />。
易错点:最容易忘的是 <script setup>;转写稿反复强调“通过昨天练习发现大家很容易把它忘掉”。另外抽样式时不要连带把无关页面样式粘进去。
用 props 接收外部数据:列表不能写死
为什么需要:tabs 的文字“关注、推荐、护肤”不能固定死在组件里,否则每换一处就要改组件。查询接口的逻辑应交给组件外部,组件只负责“你给我什么数据,我展示什么数据”。
怎么想:外部传内部,最直接的方式是组件属性。组合式 API 中用 defineProps 定义属性;这里需要数组,所以定义 list。
怎么做:
vue
<script setup>
const props = defineProps({
list: Array
})
</script>父页面定义数据并传递:
vue
<script setup>
import { ref } from 'vue'
const feed = ref([
{ text: '关注' },
{ text: '推荐' },
{ text: '护肤' }
])
</script>
<custom-tabs :list="feed" />组件模板中遍历:
vue
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
>
{{ item.text }}
</view>易错点:ref 用完要导入;父组件传值写 :list="feed",不要漏冒号;v-for 的 key 要唯一,这里用 item.text。
点击高亮:先监听点击,再用索引判断
为什么需要:需求是“点谁谁高亮”,所以必须知道用户点的是第几个 tab。
怎么想:遍历时把索引 index 传给点击函数;组件内用一个变量记录当前选中项,例如 tabIndex。当 tabIndex === index 时加 active 类。
怎么做:
vue
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
:class="{ active: tabIndex === index }"
@click="onTab(index)"
>
{{ item.text }}
</view>js
import { ref } from 'vue'
const tabIndex = ref(0)
function onTab(index) {
tabIndex.value = index
}易错点:组合式 API 中事件回调就是普通函数;修改 ref 必须写 tabIndex.value = index,不要写成 this.tabIndex。转写稿调试时发现“点了没变”,原因就是 tabIndex 一直是 0,后来在点击时改值才生效。
查询每个 tab 的位置:为下划线移动做准备
为什么需要:下划线要跟着 tab 移动,必须先知道每个 tab 距离左边的位置和宽度。
怎么想:用 uni.createSelectorQuery() 查询所有 tab 节点,拿 boundingClientRect 的位置信息。注意:在自定义组件内使用节点查询,要加 .in(...) 指定组件实例。
怎么做:
js
import { onMounted, getCurrentInstance, ref } from 'vue'
const instance = getCurrentInstance()
const tabRect = ref([])
onMounted(() => {
const query = uni.createSelectorQuery().in(instance)
query.selectAll('.tab').boundingClientRect(res => {
tabRect.value = res.map(({ left, width }) => ({ left, width }))
}).exec()
})易错点:组合式 API 里没有选项式 API 的 this,要用 getCurrentInstance() 获取实例;.in(instance) 不要漏,否则自定义组件内可能查不到节点。转写稿中先 console.log 结果,确认拿到数组、每项有 left,再继续。
把位置数据提到外层并处理:只保留 left 和 width
为什么需要:节点查询结果在 onMounted 回调里是局部数据,点击时还要用,所以必须存到组件外层可访问的变量中。
怎么想:定义 tabRect,在查询回调里赋值;原始结果字段很多,但下划线定位主要用 left 和 width,所以用 map 提取。
怎么做:如上段代码。后续计算下划线居中位置时,可用“tab 宽度减下划线宽度再除以 2”的思路得到偏移量。转写稿只讲到数据准备和居中计算思路,具体下划线移动逻辑需按后续步骤继续完成。
易错点:赋值时不要漏 .value;map 解构时字段名要和查询结果一致;先调试确认 tabRect 里每项只有 left、width,再写下划线计算,避免把未验证的数据直接用于定位。
先明确 custom-tabs 要解决的四件事
这一节不是先写代码,而是先把需求拆开。转写稿里明确提到:文字内容不能写死;点谁谁高亮;底部小横线要跟着移动;组件内部还要把“当前点的是第几个”告诉外部。为什么需要先拆?因为后面每个写法都对应一个需求,否则很容易变成“照着敲但不知道在干嘛”。怎么想?先把数据流分成“外部传进来”和“内部状态”两类:列表数据由外部传入,当前选中索引由组件内部维护。怎么做?先定义组件,再接收数据,再渲染,再加点击,最后处理下划线位置。易错点是:不要一上来就写节点查询,先把数据和点击跑通。
组件定义与使用:先按规范抽组件
转写稿里建议公共、全局组件放在规范目录下,页面私有小组件正常导入即可。这里统一新建 custom-tabs 组件,避免后面项目里名字对不上。组件模板和样式先搬进去,script setup 不要忘。为什么需要?因为 script setup 是组合式 API 的简写入口,忘了它后面的 defineProps、ref 都不能按预期工作。怎么做?组件内先保留模板和样式,外部页面按组件规范直接使用,不需要额外注册。易错点:转写稿反复强调“通过昨天练习发现大家很容易把 setup 忘掉”,这是第一个高频错误。
数据从父到子:用 defineProps 接收 list
为什么需要?文字不能固定死,而“查接口”这件事应该交给组件外部,组件只负责“你给我什么数据,我展示什么数据”。怎么想?外部到内部最直接的方式是组件属性,也就是 props。怎么做?组合式 API 里用 defineProps:
js
const props = defineProps({
list: Array
})父组件定义数据并传进去:
js
const feed = ref([
{ text: '关注' },
{ text: '推荐' },
{ text: '护肤' }
])html
<custom-tabs :list="feed" />转写稿里还现场调了一次 bug:用了 ref 却忘了导入。易错点:ref 要导入;父组件传数组时前面要有冒号;调试时可以在控制台选中 custom-tabs 组件,查看 props 里是否真的有三个数据。
用 v-for 渲染文字:不要把数据写死
为什么需要?list 已经是数组,模板里不能再写死“关注、推荐、护肤”。怎么想?数组展示就遍历。怎么做?在组件模板里写:
html
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
>
{{ item.text }}
</view>转写稿里提到“有的时候没有提示就复制一下”,是为了避免把 list 写错。易错点::key 要唯一,这里用 item.text;文字要改成 {{ item.text }},不要保留原来的静态文字。
点击高亮:索引 + active 类
为什么需要?需求是“点谁谁亮”。怎么想?先监听点击,再知道点的是第几个,最后用变量控制类名。怎么做?遍历时绑定点击并传索引:
html
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
:class="{ active: tabIndex === index }"
@click="onTab(index)"
>
{{ item.text }}
</view>js
const tabIndex = ref(0)
function onTab(index) {
tabIndex.value = index
}转写稿明确说:Vue 3 组合式 API 里事件回调就是普通函数;改 ref 要用 .value。易错点:tabIndex 默认给 0,所以第一个默认高亮;点击后忘记 tabIndex.value = index,就会“怎么点都不变”。
获取每个 tab 的左侧位置:组件内查询节点
为什么需要?下划线要移动,必须先知道每个 tab 距离左边的位置。怎么想?用节点查询:创建查询器、选节点、拿位置、执行。怎么做?在 onMounted 里:
js
onMounted(() => {
const query = uni.createSelectorQuery().in(getCurrentInstance())
query.selectAll('.tab').boundingClientRect((res) => {
console.log(res)
}).exec()
})转写稿特别补充:在自定义组件里要调 .in(...);组合式 API 没有选项式里的 this,要用 getCurrentInstance() 获取实例。易错点:忘记 .in(getCurrentInstance())、类名漏点、把 selectAll 写成 select、忘记 exec()。调试时看控制台,left 大约是 15、60、105,说明位置拿到了。
把查询结果保存到组件级数据并裁剪
为什么需要?onMounted 回调里的 res 是局部数据,点击时用不到。怎么想?定义组件级 ref,在回调里赋值。怎么做?
js
const tabBar = ref([])
onMounted(() => {
const query = uni.createSelectorQuery().in(getCurrentInstance())
query.selectAll('.tab').boundingClientRect((res) => {
tabBar.value = res.map(({ width, left }) => ({ width, left }))
}).exec()
})转写稿解释:只保留 width 和 left,因为后面下划线居中要用“tab 宽度减小横线宽度再除以 2”。易错点:赋值时漏掉 .value;map 解构写错;调试时在 Vue 面板查看 tabBar,应看到每项只含 width、left。这一节先做到这里,下一节再根据点击索引计算下划线位置。
代码示例
1. 组件定义 + props 接收数据 + 点击高亮(custom-tabs.vue)
vue
<!-- components/custom-tabs/custom-tabs.vue -->
<template>
<view class="custom-tabs">
<view
v-for="(item, index) in list"
:key="item.text"
class="tab"
:class="{ active: tabIndex === index }"
@click="onTab(index)"
>
{{ item.text }}
</view>
<view class="line" :style="{ left: lineLeft + 'px' }"></view>
</view>
</template>
<script setup>
import { ref } from 'vue'
// 组件属性:接收组件外部传入的数据
defineProps({
list: Array
})
// 当前选中的索引,默认第一个高亮
const tabIndex = ref(0)
// 下划线距离左边的位置
const lineLeft = ref(0)
// 组合式 API 中事件回调就是普通函数
const onTab = (index) => {
tabIndex.value = index
}
</script>
<style lang="scss">
.custom-tabs {
position: relative;
display: flex;
height: 80rpx;
background: #fff;
.tab {
padding: 0 20rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #666;
}
.tab.active {
color: #000;
font-weight: 700;
}
.line {
position: absolute;
bottom: 0;
left: 0;
width: 40rpx;
height: 4rpx;
background: #f00;
transition: left 0.3s;
}
}
</style>要点说明
- 组件放在
components/custom-tabs/custom-tabs.vue,目录名与组件名同名,符合 easycom 规范,页面里不需要 import 和注册,直接写<custom-tabs />即可。 - 必须写
<script setup>(转写稿反复强调“很容易把它忘掉”)。 - 外部数据进组件:最直接的方式就是组件属性,组合式 API 里用
defineProps({ list: Array }),写法是固定语法。 - 点击谁谁亮:先监听
@click="onTab(index)",把v-for的索引传进去,再让tabIndex与索引比较,true时挂上active类名。 - 易错点:
tabIndex.value = index别漏.value;:key用item.text,保证唯一不重复。
2. 页面中使用组件并传入数据(pages/index/index.vue)
vue
<!-- pages/index/index.vue -->
<template>
<view class="index">
<custom-tabs :list="feedList" />
</view>
</template>
<script setup>
import { ref } from 'vue'
// 组件外部定义数据,具体怎么查由外部决定,组件只负责展示
const feedList = ref([
{ text: '关注' },
{ text: '推荐' },
{ text: '护肤' }
])
</script>要点说明
- 数据用对象数组,字段名
text是自定义约定,好处是扩展性强(以后可加id、count等)。 - 传递方式:属性名
list对应组件里的list,写:list="feedList",一定要给数组。 - 易错点:用
ref时忘了import { ref } from 'vue',运行时会直接报错(转写稿中就是这里出的 bug)。 - 易错点:真实项目里数据应来自接口,本例写死只为演示;查询逻辑应放在组件外部,组件“给什么展示什么”。
3. 节点查询获取每个 tab 的位置并计算下划线居中位置
vue
<!-- components/custom-tabs/custom-tabs.vue 的 script setup 部分 -->
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
defineProps({
list: Array
})
const tabIndex = ref(0)
// 保存每个 tab 的宽度和左边距:[{ width, left }, ...]
const tabBar = ref([])
const lineLeft = ref(0)
// 下划线宽度,与样式里 .line 的 width 保持一致
const LINE_WIDTH = 20
// 计算下划线居中位置:tab左边距 + (tab宽度 - 下划线宽度) / 2
const setLine = (index) => {
const item = tabBar.value[index]
if (!item) return
lineLeft.value = item.left + (item.width - LINE_WIDTH) / 2
}
const onTab = (index) => {
tabIndex.value = index
setLine(index)
}
onMounted(() => {
// 1. 创建查询器(自定义组件内必须 .in(...))
const query = uni.createSelectorQuery().in(getCurrentInstance())
// 2. 查找元素 + 3. 执行查找
query
.selectAll('.tab')
.boundingClientRect((res) => {
// 只保留 width 和 left,交给 tabBar 使用
tabBar.value = res.map(({ width, left }) => ({ width, left }))
setLine(0)
})
.exec()
})
</script>要点说明
- 三步走:
uni.createSelectorQuery()创建查询器 →selectAll('.tab')查找元素 →boundingClientRect(...)拿位置尺寸 →.exec()执行。 - 自定义组件里必须调
.in(...),页面里可以不用;组合式 API 中没有this,要用getCurrentInstance()获取组件实例,这是替代选项式this的写法。 - 下划线居中的算法:
(tab宽度 - 下划线宽度) / 2 + tab左边距,即“大减小再除以二”再加上偏移。 - 易错点:
onMounted里拿到的res是局部数据,必须在外面用ref定义tabBar存起来,否则点击时拿不到。 - 易错点:
res.map(...)后要赋值给tabBar.value,漏掉.value是高频错误。 - 单位注意:
boundingClientRect返回的是 px,而样式里常用rpx,left与下划线宽度混用时需保证口径一致,否则居中会偏。
常见错误
现象:模板里写
<custom-tabs>没有提示、不渲染。
原因: 组件目录/文件名不符合 easycom 规范,或页面私有组件未显式导入。
修法: 公共组件按components/custom-tabs/custom-tabs.vue放置;页面私有组件正常import后使用。现象:把布局粘到首页后样式错乱、位置不对。
原因: 代码粘到了最外层容器,或只粘了模板漏了对应样式。
修法: 放回index配置对应容器内,模板和样式成对迁移,组件内保留完整模板与样式。现象:组件逻辑不执行、页面空白或报
ref is not defined。
原因: 忘记写<script setup>,旧模板代码未清理,或ref/onMounted/getCurrentInstance未导入。
修法: 确认使用<script setup>,删除旧模板残留,按需导入组合式 API。现象:
list拿到的是字符串,v-for遍历异常。
原因: 传数组时漏写冒号,写成list="feedTabs"。
修法: 改为:list="feedTabs",并确认props类型为Array、外部数据为ref([])。现象:点击后高亮始终停在第 0 项。
原因:tabIndex没有在点击回调中更新,或 class 判断写错。
修法: 在onTab(index)中执行tabIndex.value = index,模板用tabIndex === index控制 active 类。现象:组件内
uni.createSelectorQuery()查不到 tab 节点,结果为null。
原因: 自定义组件内查询节点未绑定当前实例,或组合式 API 里误用this。
修法: 使用query.in(getCurrentInstance()).selectAll('.tab')...exec(),并确认类名、选择器正确。现象:
onMounted里能打印位置,但点击时拿不到位置数据。
原因: 查询结果只存在回调局部变量里,外部作用域无法使用。
修法: 在外层定义tabRect = ref([]),查询完成后赋值,点击逻辑统一读取该 ref。现象:下划线位置偏移,没有对准 tab 居中。
原因: 只用了 tab 的left,没有结合 tab 宽度和下划线宽度计算居中。
修法: 先保留每项的left/width,再按居中规则计算下划线位置,并用点击索引联动。
练习
- 按 easycom 规范创建
custom-tabs组件,并在首页配置区域接入,确认无需手动注册即可显示。 - 在首页定义
feedTabs数组,通过:list传入组件;组件内用v-for渲染文字,并保证key唯一。 - 实现点击高亮:点击任意 tab 后仅当前项添加 active 类,并在控制台验证传入的索引值正确。
- 在组件
onMounted中查询所有 tab 的left/width,整理成[{ left, width }]存入外层ref,用调试工具确认数据完整。 - 基于上一步数据计算下划线居中位置,点击 tab 时下划线跟随移动;补充默认选中第 0 项的处理。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义组件-custom-tabs布局模板 | 04:24 |
| 2 | 自定义组件-custom-tabs定义 | 03:35 |
| 3 | 自定义组件-custom-tabs组件数据 | 10:21 |
| 4 | 自定义组件-custom-tabs高亮显示 | 10:01 |
| 5 | 自定义组件-custom-tabs节点位置 | 09:44 |
| 6 | 自定义组件-custom-tabs位置数据处理 | 08:19 |
小结
- 先把布局和样式在页面中跑通,再抽成组件,排查成本更低。
- 公共全局组件走 easycom 规范;页面私有组件可正常导入,不必强行统一。
custom-tabs只负责展示与交互,tab 数据应由外部传入,查询数据的职责留在外部。- 高亮状态用“当前索引”驱动,点击时更新索引,模板根据索引比较结果决定 active 类。
- 自定义组件内查节点必须绑定当前组件实例,组合式 API 下不要依赖
this。 - 节点位置数据要提升到组件外层作用域,否则
onMounted的局部结果无法参与后续点击逻辑。 - 下划线联动的前提是先拿到每个 tab 的
left/width,再处理居中计算,最后与点击索引绑定。 - 调试时优先检查 props、ref、节点查询结果和当前索引,能快速定位大部分联动问题。