vue3搭建实战项目笔记四
5.1.动态绑定ref,销毁页面时拿到值为空
```javascript
// 找到对应元素的根元素
const getSectionRef = (value) => {
console.log('value===', value);
// 在页面卸载时,元素是空的为null,所以判断下是否为空,为空直接返回出去
if(!value) return
// 1.在滚动时,会引起dom的刷新,导致sectionEls会被重新执行,会有多个sectionEls
// 2.使用v-memo,可以解决这个问题,缓存一个模板的子树,当数据变化时才会刷新
// 3.value拿到的是组件实例对象,想要拿到组件对象的根元素,怎么拿到组件对象的根元素.$el
const name = value.$el.getAttribute('name')
names.push(name)
sectionEls[name] = value.$el
}
```
5.2. 页面滚动,显示正确的tabControl的标题
-
需求: 页面滚动,滚动到一定位置,显示正确的tabControl的索引(标题)
监听滚动的位置 scrollTop
* scrollTop: 600利用scrollTop去匹配正确的位置
offsetTop
[描述300, 设施500, 房东800, 评论1200, 须知1400, 周边1600]
const values = [300, 500, 800, 1200, 1400, 1600] let index = values.length - 1 for(let i = 0; i < values.length; i++) { const value = values[i] if (value > scrollTop) { index = i - 1 break; } } console.log(index)
- 关键代码如下:
<template> <div class="detail top-page" ref="detailRef"> <van-nav-bar title="房屋详情" left-text="返回" left-arrow @click-left="onClickLeft" /> <tab-control ref="tabControlRef" class="tabs" v-if="showTabControl" :titles="names" @tabItemClick="tabClick" /> <!-- 内容部分 --> <div class="main" v-if="mainPart" v-memo="[mainPart]"> <!-- 轮播组件 --> <detail-swipe :swipe-data="mainPart.topModule.housePicture.housePics" /> <!-- 动态绑定Ref:在处理复杂组件结构和动态数据时通过动态绑定Ref,我们可以更灵活地访问和操作DOM元素或组件实例,实现更高效的交互和状态管理 --> <detail-infos name="描述" :ref="getSectionRef" :topInfos="mainPart.topModule"/> <detail-facility name="设施" :ref="getSectionRef" :house-facility="mainPart.dynamicModule.facilityModule.houseFacility"/> <!-- :landload="mainPart.dynamicModule.landloadModule.houseLandload" --> <detail-landlord name="房东" :ref="getSectionRef" :landlord="mainPart.dynamicModule.landlordModule"/> <detail-comment name="评论" :ref="getSectionRef" :comment="mainPart.dynamicModule.commentModule"/> <detail-notice name="须知" :ref="getSectionRef" :order-rules="mainPart.dynamicModule.rulesModule.orderRules"/> <detail-map name="周边" :ref="getSectionRef" :position="mainPart.dynamicModule.positionModule" /> <detail-intro :priceIntro="mainPart.introductionModule"/> </div> <div class="footer"> <img src="@/assets/img/detail/icon_ensure.png" alt=""> <div class="text">弘源旅途, 永无止境!</div> </div> </div> </template> <script></script> const detailRef = ref(null) const { scrollTop } = useScroll(detailRef) const sectionEls = ref({}) const names = [] // 找到对应元素的根元素 const getSectionRef = (value) => { // 在页面卸载时,元素是空的为null,所以判断下是否为空,为空直接返回出去 if(!value) return // 1.在滚动时,会引起dom的刷新,导致sectionEls会被重新执行,会有多个sectionEls // 2.使用v-memo,可以解决这个问题,缓存一个模板的子树,当数据变化时才会刷新 // 3.value拿到的是组件实例对象,想要拿到组件对象的根元素,怎么拿到组件对象的根元素.$el const name = value.$el.getAttribute('name') names.push(name) sectionEls.value[name] = value.$el } watch(scrollTop, (newVal) => { // 1.获取所有滚动区域的offsetTop const els = Object.values(sectionEls.value) const values = els.map(el => el.offsetTop) console.log('values===2',values); // 2.根据newValue去匹配想要 的索引 // index默认值为最后一个元素的索引 let index = values.length -1 for(let i = 0; i < values.length; i++) { if (values[i] > newVal + 44) { index = i - 1; break; } } if(index === -1) { index = 0 } if(tabControlRef.value?.setCurrentIndex === index) { return } tabControlRef.value?.setCurrentIndex(index) })
- 点击时tabControl的标题是会挨个移动端最终点击的标题上
- 4.1. 使用个变量记录当前的是否在点击
- 4.2. 记录当前点击后滚动的距离,当监听scrollTop时,他的值等于点击后的滚动距离重置是否点击
```javascript // 当点击时,不需要去监听滚动挨个匹配位置 // 当通过点击时, let isClick = false // 记录当前滚动的位置 let currentDistance = -1 // 当点击时滚动到对应位置 const tabClick = (index) => { const key = Object.keys(sectionEls.value)[index] const el = sectionEls.value[key] let distance = el.offsetTop if(index !== 0) { // 滚动距离减去 44,因为tabControl组件的高度为44,会遮挡模块的标题 distance = distance - 44 } // 当点击时将isClick设置为true,将当前的滚动位置保存的currentDistance中 isClick = true currentDistance = distance // scrollTo 使界面滚动到给定元素的指定位置 detailRef.value.scrollTo({ top: distance, behavior: 'smooth' // 滚动行为 smooth: 平滑滚动 }) } const tabControlRef = ref(null) // 页面滚动,滚动时匹配对应的tabControl的index watch(scrollTop, (newVal) => { // 当滚动到指定位置时,将isClick设置为false if(newVal === currentDistance) { isClick = false } // 当点击时,不需要去监听滚动挨个匹配位置 if(isClick) return // 1.获取所有滚动区域的offsetTop const els = Object.values(sectionEls.value) const values = els.map(el => el.offsetTop) console.log('values===2',values); // 2.根据newValue去匹配想要 的索引 // index默认值为最后一个元素的索引 let index = values.length -1 for(let i = 0; i < values.length; i++) { if (values[i] > newVal + 44) { index = i - 1; break; } } if(index === -1) { index = 0 } if(tabControlRef.value?.setCurrentIndex === index) { return } tabControlRef.value?.setCurrentIndex(index) }) ```
5.3. 首页缓存,并记录滚动位置
-
- 在App页面使用keep-alive缓存首页
<template> <div class="app"> <!-- 在首页页面的时候希望缓存首页 --> <router-view v-slot="props"> <!-- 通过name属性匹配缓存页面,组件的name属性 --> <keep-alive include="home"> <component :is="props.Component"></component> </keep-alive> </router-view> </div> </template>
-
- 找到首页页面在script标签上添加name属性
<script setup name="home"> </script> ```
-
- 首页的window滚动方式改为页面滚动
- 3.1. 将home页面中的根元素改为可以滚动
.home { height: 100vh; overflow-y: auto; box-sizing: border-box; }
- 3.2. 设置ref, 并将ref元素传到useScroll中更改为页面滚动
<template> <div class="home" ref="homeRef"> ... </div> </template> <script setup name="home"> import { ref, onMounted, watch } from 'vue' const homeRef = ref(null) // 拿到isReachBottom,然后监听isReachBottom为真的时候滚动到底部 const { isReachBottom, scrollTop } = useScroll(homeRef) watch(isReachBottom, (newValue) => { if(newValue) { homeStore.fetchHouseListData().then(() => { // 拿到数据后重置isReachBottom为false isReachBottom.value = false }) } }) </script>
-
- 记录滚动位置
// 跳转回原来的位置时,保留原来的位置 onActivated(() => { homeRef.value?.scrollTo({ top: scrollTop.value }) })
-
- 完整代码如下:
<template> <div class="home" ref="homeRef"> <home-nav-bar/> <div class="banner"> <img src="@/assets/img/home/banner.webp" alt=""> </div> <home-search-box></home-search-box> <home-categories></home-categories> <div v-if="isShowSearchBar" class="search-bar"> <searchBar :start-date="'03-27'" :end-date="'03-28'"/> </div> <home-content></home-content> </div> </template> <script setup name="home"> import HomeNavBar from './cpns/home-nav-bar.vue' import homeSearchBox from './cpns/home-search-box.vue'; // import hyRequest from '@/service/request/index.js' import useHomeStore from '@/stores/modules/home'; import homeCategories from './cpns/home-categories.vue'; import homeContent from './cpns/home-content.vue'; import searchBar from '@/components/search-bar/search-bar.vue'; import useScroll from '@/hooks/useScroll'; import { watch, ref, computed, onActivated } from 'vue'; // 发送网络请求 const homeStore = useHomeStore() homeStore.fetchHotSuggestsData() homeStore.fetchCategoriesData() homeStore.fetchHouseListData() const homeRef = ref(null) // 拿到isReachBottom,然后监听isReachBottom为真的时候滚动到底部 const { isReachBottom, scrollTop } = useScroll(homeRef) // 当监听到变化的时候去执行另外一个js代码,不用计算属性,而是用watch监听,在回调函数里面写自己的逻辑 watch(isReachBottom, (newValue) => { if(newValue) { homeStore.fetchHouseListData().then(() => { // 拿到数据后重置isReachBottom为false isReachBottom.value = false }) } }) const isShowSearchBar = computed(() => { return scrollTop.value >= 360 }) // 跳转回原来的位置时,保留原来的位置 onActivated(() => { homeRef.value?.scrollTo({ top: scrollTop.value }) }) </script> <style lang="less" scoped> .home { height: 100vh; overflow-y: auto; box-sizing: border-box; padding-bottom: 60px; .banner{ img { width: 100%; } } .search-bar { position: fixed; z-index: 9; top: 0; left: 0; right: 0; height: 45px; padding: 16px 16px 10px; background-color: #fff; } } </style>
5.4. 移动端禁止缩放,vite中将项目中所有px单位转为vw单位
-
- 移动端禁止缩放,在index.html中操作meta标签的content属性
<!DOCTYPE html> <html lang=""> <head> <meta charset="UTF-8"> <link rel="icon" href="/favicon.ico"> <!-- 禁止缩放页面 --> <!-- width=device-width 设置页面的宽度与设备的屏幕宽度相同,确保网页在移动设备上能正确显示。 initial-scale=1.0 设置网页在移动设备上的初始缩放比例 maximum-scale=1.0 设置网页在移动设备上的最大缩放比例 minimum-scale=1.0 设置网页在移动设备上的最小缩放比例 user-scalable=no 禁用用户缩放功能 --> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"> <title>弘源旅途</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script> <script type="text/javascript" src="https://api.map.baidu.com/getscript?v=3.0&&type=webgl&ak=kMX8CLJhpvH1KA8q0cN5eidPalGwvjzi"> </script> </body> </html>
-
- 自动将px单位转换成vw单位
Vite/Webpack -> postcss工具 -> plugins -> postcss-px-to-viewport
Vant 默认使用 px 作为样式单位,如果需要使用 viewport 单位 (vw, vh, vmin, vmax),推荐使用
postcss-px-to-viewport
进行转换。postcss-px-to-viewport
是一款 PostCSS 插件,用于将 px 单位转化为 vw/vh 单位。
-
- 安装
postcss-px-to-viewport
npm install postcss-px-to-viewport -D
- 安装
-
- 根目录下建立
postcss.config.cjs
文件进行配置
// postcss.config.cjs module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 375, // 设计稿的宽度 selectorBlackList: ['favor'] // 设置黑名单,不转换的页面组件 }, }, };
- 根目录下建立
5.5. 项目的部署发布和访问的流程
-
- 首先打包项目:
npm run build
- 首先打包项目:
-
- 打包后的文件会生成在
dist
文件夹中,将dist
文件夹需要放到服务器里面,意味着服务有dist
静态资源(index.html,css/,js/,img/
)
- 打包后的文件会生成在
-
- 用户在浏览器输入域名(
xxx.baidu.com
)访问项目,然后经过域名解析,解析出来IP
地址,通过IP
地址找到服务器,服务器的话请求对应的资源
- 用户在浏览器输入域名(
-
- 最先请求下来的是
index.html
, 把index.html
下载下来,然后浏览器解析会index.html
, 解析过程当中遇到谁加载谁(例如:遇到js
加载js
文件,遇到img
加载img
,遇到css
加载css
文件),然后把静态资源下载下来
- 最先请求下来的是
-
- 最终渲染页面,用户就可以看到页面了(不管是PC端还是移动端)
5.6. 项目的两个问题:一个是keep-alive使用include, 一个是tab-bar组件销毁后导致activeIndex为0错误
-
- keep-alive使用include属性
<router-view v-slot="props"> <!-- 通过name属性匹配缓存页面,组件的name属性 --> <keep-alive include="home"> <component :is="props.Component"></component> </keep-alive> </router-view>
-
- tab-bar组件销毁后导致activeIndex为0错误
2.1. 产生原因:
当点击message页面的时候,隐藏tabBar使用的是v-if会销毁掉dom,组件不存在了, 后面又把组件添加进来,相当于创建了一个新的tabBar,跟添加的时机有关系, 因为是新创建出来的,所以到时候watch这个监听还没来的及监听页面就已经跳过去了(watch监听可能不执行) 最后导致currentIndex值不对
2.2. 解决方法:
使用v-show解决
html <tab-bar v-show="!route.meta.hideTabBar"/>
2.3. 完整代码:
~ <template> <div class="app"> <!-- 官方不推荐我们这样写,推荐插槽的方式来写 --> <!-- <keep-alive includes="home"> <router-view></router-view> </keep-alive> --> <!-- 插槽的方式来写 --> <!-- 在首页页面的时候希望缓存首页 --> <router-view v-slot="props"> <!-- 通过name属性匹配缓存页面,组件的name属性 --> <keep-alive include="home"> <component :is="props.Component"></component> </keep-alive> </router-view> <!-- 当点击message页面的时候,隐藏tabBar使用的是v-if会销毁掉dom,组件不存在了, 后面又把组件添加进来,相当于创建了一个新的tabBar,跟添加的时机有关系, 因为是新创建出来的,所以到时候watch这个监听还没来的及监听页面就已经跳过去了(watch监听可能不执行) 最后导致currentIndex值不对 使用v-show解决 --> <!-- 根据路由元信息是否显示 tabBar --> <tab-bar v-show="!route.meta.hideTabBar"/> <loading /> </div> </template> <script setup> import tabBar from '@/components/tab-bar/tab-bar.vue'; import { useRoute } from 'vue-router'; import Loading from '@/components/loading/loading.vue'; // 当前活跃的路由对象 const route = useRoute() </script> <style scoped> </style>