ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Taro 滚动时导航栏变色:usePageScroll 实战(附完整代码和踩坑记录)

Taro 滚动时导航栏变色:usePageScroll 实战(附完整代码和踩坑记录) Taro一个开放式跨端跨框架解决方案用 Vue / React 写一套代码编译到微信小程序、H5、支付宝小程序等多个端。目录效果一、介绍1、为什么滚动相关的导航栏这么麻烦2、官方文档usePageScroll 与页面原生滚动二、准备工作1、项目环境2、前置依赖自定义导航栏组件三、使用步骤关键点1、滚动监听usePageScroll 判断滚动状态2、导航栏背景切换transparent → white3、搜索栏 / 头部容器同步切换样式4、固定头部高度计算5、跨端兼容微信小程序 / H5四、完整示例1、src/pages/home/index.vue首页滚动导航栏2、src/components/NavBar.vue配套导航栏组件完整代码3、NavBar 组件实现要点运行方式五、可能出现的小问题踩坑记录1、usePageScroll 只对页面级原生滚动生效2、scrollTop 判断太敏感滚动 1px 就变色3、头部高度的单位陷阱rpx vs px4、状态栏高度用 getSystemInfoSync 取5、fixed 头部必须给内容留白结语欢迎关注【前端小知识营地】效果滚动前导航栏透明沉浸展示 Hero 背景图标题、左侧城市文字、定位图标均为白色。滚动后导航栏切换为纯白背景标题、城市文字、定位图标同步切换为深色。所有列表页原生页面滚动固定头部始终悬浮在顶部内容从头部下方自然流出滚到底不遮挡、不出白条。一、介绍1、为什么滚动相关的导航栏这么麻烦自定义导航栏组件封装好之后页面真正跑起来还会遇到两类滚动场景滚动变色首页顶部导航栏一开始是透明的露出轮播图 / Hero 背景一旦用户往下滚透明背景上的白字就糊了需要平滑切成白底黑字搜索栏同步加阴影。固定头部避让导航栏 搜索栏用position: fixed悬浮在内容之上下方内容必须留出等高的空白否则第一屏内容会被头部盖住。这两个问题太常见了值得一篇完整示例讲清楚。2、官方文档usePageScroll 与页面原生滚动usePageScroll监听页面级原生滚动回调里能拿到scrollTop等滚动信息。滚动方案整页滚动采用页面原生滚动——根容器用min-height: 100vh的流式布局避免height: 100vh锁死高度、避免overflow: hidden截断滚动固定头部用position: fixed 内容padding-top避让。下面的滚动变色方案即基于这套原生滚动实现。二、准备工作1、项目环境tarojs/taro: ^4.2.0, tarojs/plugin-framework-vue3: 4.2.0, vue: ^3.0.0, typescript: ^5.4.5, sass: ^1.75.02、前置依赖自定义导航栏组件本示例依赖自定义导航栏组件NavBar.vue状态栏高度、占位符、透明背景切换都封装好了其完整代码在第四·2 节贴出复制即用。三、使用步骤关键点1、滚动监听usePageScroll 判断滚动状态Taro 提供usePageScrollHook页面原生滚动时回调。判断scrollTop是否超过阈值避免轻微滚动就触发变色import { ref } from vue import { usePageScroll } from tarojs/taro // 滚动阈值px超过才认为已下滑 const SCROLL_THRESHOLD 50 interface ScrollDetail { scrollTop: number } const isScrolled ref(false) usePageScroll((e: ScrollDetail) { const st e?.scrollTop ?? 0 isScrolled.value st SCROLL_THRESHOLD })2、导航栏背景切换transparent → white用computed把滚动状态映射成导航栏背景。NavBar组件内部自带transition: background 0.24s ease切换平滑const navBarBg computed(() isScrolled.value ? white : transparent)NavBar title首页 :backgroundnavBarBg /滚动前transparent白字露 Hero 背景→ 滚动后white黑字。3、搜索栏 / 头部容器同步切换样式导航栏下方的搜索栏、头部容器也要同步反馈滚动状态。用同一份isScrolled驱动view classhome-header-fixed :class{ home-header-fixed--scrolled: isScrolled } NavBar title首页 :backgroundnavBarBg template #left view classhome-navbar__city taphandleCityTap text classhome-navbar__city-text成都市/text image classhome-navbar__city-pin :srciconLocation modeaspectFit / /view /template /NavBar /view对应 SCSS 中通过.home-header-fixed--scrolled控制内部文字与图标颜色.home-header-fixed { position: fixed; top: 0; left: 0; right: 0; z-index: 99; background-color: transparent; transition: background-color 0.24s ease; --scrolled { background-color: #ffffff; } } .home-navbar__city-text { color: #ffffff; transition: color 0.24s ease; } .home-navbar__city-pin { filter: brightness(0) invert(1); // 透明态下把深色 SVG 变白 transition: filter 0.24s ease; } .home-header-fixed--scrolled { .home-navbar__city-text { color: #1a1a1a; } .home-navbar__city-pin { filter: none; // 白底时恢复原始深色 } }4、固定头部高度计算固定头部position: fixed不占文档流下方内容必须用padding-top留出等高的空白。头部总高度 状态栏高度 导航栏内容高度(44px) 额外偏移搜索栏等。const sysInfo Taro.getSystemInfoSync() const statusBarHeightPx sysInfo.statusBarHeight ?? 27 const navBarBaseHeightPx 44 const searchBarHeightRpx 100 // 若页面无搜索栏可设为 0 const pxToRpx (px: number) Math.round(px * 750 / sysInfo.screenWidth) const fixedHeaderRpx pxToRpx(statusBarHeightPx) pxToRpx(navBarBaseHeightPx) searchBarHeightRpx要点状态栏高度必须动态取不同机型 20~59px 差异极大写死会导致头部留白错位。用pxToRpx把 px 换算成 rpx直接写 px 会和搜索栏(rpx) 单位不一致。5、跨端兼容微信小程序 / H5用Taro.getSystemInfoSync()拿状态栏高度Taro 会自动抹平小程序与 H5 的差异再给状态栏高度兜底避免个别环境取不到值算出来 NaNconst statusBarHeightPx sysInfo.statusBarHeight ?? 27H5 端statusBarHeight恒为 0所以兜底用??而不是||||会把合法的 0 也兜底成 27px导致 H5 头部凭空多出 27px。四、完整示例下面给出可直接复制运行的两个文件均包含template/script/style三部分。第一个文件是首页演示滚动前透明白字 / 滚动后白底黑字第二个文件是配套的NavBar.vue完整代码贴在同一篇里无需再跳转 01。滚动效果的关键.home-content内层需有足够高的内容撑出页面原生滚动示例中用一个占位元素.demo-placeholder高 2000rpx即可无需填充真实内容。1、src/pages/home/index.vue首页滚动导航栏template view classhome-page !-- Hero 装饰背景 -- view classhome-hero image classhome-hero__image :srchomeHero modeaspectFill / view classhome-hero__shade / /view !-- 固定头部透明态 → 白底态 同步切换 -- view classhome-header-fixed :class{ home-header-fixed--scrolled: isScrolled } NavBar title首页 :backgroundnavBarBg template #left view classhome-navbar__city taphandleCityTap text classhome-navbar__city-text{{ currentCity }}/text image classhome-navbar__city-pin :srciconLocation modeaspectFit / /view /template template #right view classhome-navbar__right / /template /NavBar /view !-- 页面原生滚动padding-top 避让固定头部内部内容足够高即可滚动 -- view classhome-content :style{ paddingTop: fixedHeaderRpx rpx } !-- 占位元素高度超过一屏即可触发滚动变色无需真实内容 -- view classdemo-placeholder / view classbottom-spacer / /view /view /template script setup langts import { ref, computed } from vue import Taro, { usePageScroll } from tarojs/taro import NavBar from /components/NavBar.vue // 静态资源替换成你项目里的图片即可 import homeHero from /static/home/home-hero.png import iconLocation from /static/common/icon-location.svg const sysInfo Taro.getSystemInfoSync() const statusBarHeightPx sysInfo.statusBarHeight ?? 27 const navBarBaseHeightPx 44 const searchBarHeightRpx 100 // 若当前页面无搜索栏可改为 0 const pxToRpx (px: number) Math.round(px * 750 / sysInfo.screenWidth) const statusBarRpx pxToRpx(statusBarHeightPx) const navBarRpx pxToRpx(navBarBaseHeightPx) const fixedHeaderRpx statusBarRpx navBarRpx searchBarHeightRpx // 固定头部总高度rpx内容区 padding-top 用此值避让 // 当前城市 const currentCity ref(成都市) // 页面滚动状态 const isScrolled ref(false) const navBarBg computed(() (isScrolled.value ? white : transparent)) // 监听页面级原生滚动滚动即切换导航栏状态透明 ↔ 白底 usePageScroll((e: any) { const st e?.scrollTop ?? 0 isScrolled.value st 0 }) function handleCityTap() { Taro.showToast({ title: 城市选择开发中, icon: none }) } /script style langscss .home-page { min-height: 100vh; background-color: #f6f8fb; // 页面底色 display: flex; flex-direction: column; position: relative; } // ---- Hero 装饰背景 ---- .home-hero { position: absolute; top: 0; left: 0; right: 0; height: 466rpx; // 233px overflow: hidden; z-index: 0; pointer-events: none; } .home-hero__image { width: 100%; height: 100%; } // 渐变遮罩从透明渐变到页面底色 .home-hero__shade { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(245, 247, 250, 0) 0%, rgba(245, 247, 250, 0) 35%, #f6f8fb 100%); // 末尾色 #f6f8fb让 Hero 与内容自然过渡 } // ---- 固定头部整体容器透明态 → 白底态 ---- .home-header-fixed { position: fixed; top: 0; left: 0; right: 0; z-index: 99; background-color: transparent; transition: background-color 0.24s ease; // 滚动后背景变白 --scrolled { background-color: #ffffff; } } // ---- 页面原生滚动内容区用 padding-top 避让固定头部 ---- .home-content { box-sizing: border-box; } // 导航栏城市样式透明态白字 / 白底态深字 .home-navbar__city { display: flex; align-items: center; margin-right: 8rpx; min-width: 120rpx; -text { font-family: PingFang SC, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 28rpx; font-weight: 600; color: #ffffff; // 透明态白字 transition: color 0.24s ease; } -pin { width: 20rpx; height: 20rpx; margin-left: 8rpx; filter: brightness(0) invert(1); // 透明态下把深色 SVG 变白 transition: filter 0.24s ease; } } // 滚动后城市文字与定位图标切换为深色 .home-header-fixed--scrolled { .home-navbar__city-text { color: #1a1a1a; // 白底态深字 } .home-navbar__city-pin { filter: none; // 白底时恢复原始深色 } } .home-navbar__right { width: 100rpx; height: 48rpx; } // ---- 占位元素仅用于撑出滚动高度触发导航栏变色 ---- .demo-placeholder { height: 2000rpx; margin: 24rpx 30rpx; } // ---- 底部留白适配 iPhone 底部安全区 ---- .bottom-spacer { height: env(safe-area-inset-bottom); } /style2、src/components/NavBar.vue配套导航栏组件完整代码template view classnavbar :class[ bgClass, { navbar--shadow: shadow }, themeClass, { navbar--embedded: embedded } ] :style[props.style, embedded ? {} : { paddingTop: statusBarHeight px }] view classnavbar__inner view classnavbar__left slot nameleft view v-ifshowBack classnavbar__back taphandleBack image classnavbar__back-icon :srciconChevronLeft modeaspectFit / /view /slot /view view classnavbar__center text classnavbar__title text-ellipsis{{ title }}/text /view view classnavbar__right slot nameright / /view /view /view !-- 占位防止内容被导航栏遮挡embedded 模式下不需要 -- view v-if!embedded classnavbar__placeholder :style{ height: totalHeight px } / /template script setup langts import { computed } from vue import Taro from tarojs/taro import iconChevronLeft from /static/common/icon-chevron-left.svg const props withDefaults(defineProps{ title?: string showBack?: boolean background?: white | transparent shadow?: boolean theme?: light | dark embedded?: boolean style?: Recordstring, string }(), { title: , showBack: true, background: white, shadow: false, theme: light, embedded: false, style: () ({}), }) // 状态栏高度动态读取机型差异大约 20~59px // H5 端恒为 0兜底 27px。注意此处用 || H5 会把 0 误兜底成 27px const statusBarHeight computed(() { const info Taro.getSystemInfoSync() return info.statusBarHeight ?? 27 }) const totalHeight computed(() { return statusBarHeight.value 44 // 44px 导航栏内容高度 }) const bgClass computed(() { if (props.background transparent) return navbar--transparent return navbar--white }) const themeClass computed(() { return props.theme dark ? navbar--dark-theme : }) function handleBack() { if (!props.showBack) return const pages Taro.getCurrentPages() if (pages.length 1) { Taro.navigateBack() } else { Taro.switchTab({ url: /pages/home/index }) } } /script style langscss .navbar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; box-sizing: border-box; transition: background 0.24s ease; --embedded { position: relative; top: auto; left: auto; right: auto; z-index: 1; } --white { background-color: #ffffff; } --transparent { background-color: transparent; .navbar__title { color: #ffffff; // 透明态白字 } .navbar__back-icon { filter: brightness(0) invert(1); } } --dark-theme.navbar--transparent { .navbar__title { color: #1a1a1a; // 浅色页面深字 } .navbar__back-icon { filter: none; } } --shadow { box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06); } __inner { display: flex; align-items: center; justify-content: space-between; height: 88rpx; // 44px padding: 0 32rpx; position: relative; } __left, __right { width: 100rpx; display: flex; align-items: center; } __right { justify-content: flex-end; } __back { width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; -icon { width: 48rpx; height: 48rpx; } } __center { flex: 1; display: flex; align-items: center; justify-content: center; padding: 0 16rpx; overflow: hidden; } __title { font-size: 34rpx; font-weight: 600; color: #1a1a1a; line-height: 1.2; } __placeholder { width: 100%; } } /style3、NavBar 组件实现要点下方NavBar.vue为可直接运行的完整代码实现时有三处值得注意返回箭头图标示例用import iconChevronLeft from /static/common/icon-chevron-left.svg走资源管线引入。若担心小程序 iOS 对 SVG 渲染不稳定可换回内联data:image/svgxml,...的>运行方式环境要求Taro 4 Vue3 项目npm i tarojs/taro vue用到 SCSS 需再装sass。把第四·1 节的home/index.vue放进src/pages/home/index.vue把第四·2 节的NavBar.vue放进src/components/NavBar.vue无需额外文件即可运行。homeHero/iconLocation指向静态资源/static/...替换成你自己的图片即可要验证滚动变色把示例里的NavBar换成一个固定高度的view也能跑。使用自定义导航栏时记得在src/app.config.ts的window里设置navigationStyle: custom。预览小程序端taro build --type weapp --watch后用微信开发者工具打开dist/H5 端taro dev --type h5H5 状态栏高度为 0头部会比小程序端矮一截属正常现象。五、可能出现的小问题踩坑记录1、usePageScroll 只对页面级原生滚动生效usePageScroll只监听页面级原生滚动。只要页面采用原生滚动而非scroll-view整页滚动usePageScroll就能直接生效导航栏平滑变色。例外如果某个局部区域用了scroll-view做局部滚动如弹窗里的有界列表、横向滑动usePageScroll不会因它触发。需要监听局部 scroll-view 的滚动时改用它的scroll事件。2、scrollTop 判断太敏感滚动 1px 就变色st 0意味着用户轻轻一碰就触发变色视觉上会显得神经质。实战中建议加个阈值const SCROLL_THRESHOLD 50 usePageScroll((e: { scrollTop: number }) { const st e?.scrollTop ?? 0 if (st SCROLL_THRESHOLD) isScrolled.value true else if (st 0) isScrolled.value false })阈值如 50px可以让导航栏在顶部区域保持透明更久观感更自然。注意别用else直接取反——iOS 橡皮筋回弹时scrollTop会变负显式处理才能避免闪烁。3、头部高度的单位陷阱rpx vs px算固定头部高度时状态栏高度、导航栏高度是系统 px而搜索栏等在样式里写的是rpx设计稿两者单位不同。必须用pxToRpx把 px 统一换算成 rpx再作为padding-top的单位否则内容padding-top偏矮一截第一屏被固定头部压住。4、状态栏高度用 getSystemInfoSync 取Taro.getSystemInfoSync().statusBarHeight是动态值机型差异极大iOS 20~59px、Android 24~48px、H5 端为 0。务必动态取并兜底别写死成 27px——写死会导致某些机型头部留白过大或过小。H5 端该值恒为 0所以兜底用??而不是||||会把合法的 0 也兜底成 27px导致 H5 头部凭空多出 27px。5、fixed 头部必须给内容留白position: fixed不占文档流内容会直接从页面顶部开始排第一屏被头部盖住。必须给内容根容器加padding-top fixedHeaderRpx上面算好的头部总高度否则轮播图/列表第一行直接躲在导航栏底下。另外注意如果滚动区内容撑不满高度页面只是不滚而已这是正常行为别当成 bug。结语本示例实现了自定义导航栏的滚动变色滚动变色usePageScrollcomputed驱动导航栏背景平滑切换基于页面原生滚动。文字/图标同步切换通过.home-header-fixed--scrolled控制左侧城市文字和定位图标的颜色确保透明态白字清晰、白底态黑字可读。固定头部避让position: fixed头部 内容padding-top 固定头部总高度微信小程序与 H5 通用。NavBar.vue完整代码已贴出可直接复用。踩坑记录里的 5 条都是实际开发中常见问题尤其usePageScroll只对原生滚动生效和rpx/px单位换算提前了解能省不少排查时间。欢迎关注【前端小知识营地】
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进