转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 需求
使用 AJAX 动态获取详情页数据,点击不同景点时,获取不同 id 的数据:
2 AJAX 动态获取数据
🔗前置知识:《详情页开发——① 动态路由和 Banner 组件布局》
需求分析:“点击不同景点时,获取不同 id 的数据”。即,需要在发送请求时携带参数 id ,才能获取到 id 所对应的数据。
❓发送请求时如何携带对应的 id 呢?
答:在详情页开发设置“动态路由”时,我们在路径上使用了动态路径参数 :id 。而当我们使用了路径参数后,参数值会被设置到 this.$route.params 中,每个组件都可以使用。
所以,通过 this.$route.params.id 我们就可以获取到路由中的 id 这个参数。
1️⃣在 static 下的 mock 文件夹中新建一个 detail.json 文件,里边是详情页所有的数据:
{
"ret": true,
"data": {
// 1️⃣-①:data 中有一个 sightName 是景点名称;
"sightName": "故宫(AAAAA景区)",
// 1️⃣-②:bannerImg 是详情页 Banner 图片;
"bannerImg": "https://qdywxs.github.io/travel-images/detail-banner-img.jpg",
// 1️⃣-③:galleryImgs 是点击海报后画廊组件显示的图片;
"galleryImgs": ["https://qdywxs.github.io/travel-images/detail-gallary-img01.jpg", "https://qdywxs.github.io/travel-images/detail-gallary-img02.jpg", "https://qdywxs.github.io/travel-images/detail-gallary-img03.jpg"],
// 1️⃣-④:categoryList 是景点门票的类别;
"categoryList": [{
"title": "故宫当日票",
"children": [{
"title": "成人票",
"children": [{
"title": "故宫成人票+钟表馆+珍宝馆"
}]
}, {
"title": "学生票"
}]
}, {
"title": "故宫预售成人票",
"children": [{
"title": "故宫+钟表馆+珍宝馆"
}]
}],
// 1️⃣-⑤:commentList 是评论内容。
"commentList": [{
"id": "0001",
"star": "★★★★★",
"date": "q*9 2019-11-01",
"content": "我们是早上 8:30 的第一场,有珍宝馆的套票,王蕾导游讲解很到位,妙语连珠,互动性强,服务热情,路线和讲解安排合理,游玩很开心有意义,对故宫有了深入全面的了解,不虚此行,不留遗憾,深有体会,故宫博物院完美打卡。",
"imgUrl": ["https://qdywxs.github.io/travel-images/commentImg01.jpg", "https://qdywxs.github.io/travel-images/commentImg02.jpg", "https://qdywxs.github.io/travel-images/commentImg03.jpg", "https://qdywxs.github.io/travel-images/commentImg04.jpg", "https://qdywxs.github.io/travel-images/commentImg05.jpg", "https://qdywxs.github.io/travel-images/commentImg06.jpg"]
}, {
"id": "0002",
"star": "★★★★★",
"date": "z*3 2019-11-01",
"content": "非常好的体验,推荐大福晋导游。故宫太大,又是历史文化浓重的宫殿,如果没有导游,自己瞎逛浪费时间和体力,也不明白很多殿的故事,每个人一个无线耳麦,离导游三十米内都听的很清楚,导游讲解的风趣幽默,不错的一次体验。",
"imgUrl": ["https://qdywxs.github.io/travel-images/commentImg01.jpg", "https://qdywxs.github.io/travel-images/commentImg02.jpg", "https://qdywxs.github.io/travel-images/commentImg03.jpg", "https://qdywxs.github.io/travel-images/commentImg04.jpg", "https://qdywxs.github.io/travel-images/commentImg05.jpg", "https://qdywxs.github.io/travel-images/commentImg06.jpg"]
}]
}
}
2️⃣打开 detail 下的 Detail.vue :
<template>
<div>
<detail-banner></detail-banner>
<detail-header></detail-header>
<detail-list :list="list"></detail-list>
<detail-comment></detail-comment>
</div>
</template>
<script>
import DetailBanner from './components/Banner'
import DetailHeader from './components/Header'
import DetailList from './components/List'
import DetailComment from './components/Comment'
import axios from 'axios' // 2️⃣-①:引入 Axios;
export default {
name: 'Detail',
components: {
DetailBanner,
DetailHeader,
DetailList,
DetailComment
},
data () {
return {
list: [{
title: '故宫当日票',
children: [{
title: '成人票',
children: [{
title: '故宫成人票+钟表馆+珍宝馆'
}]
}, {
title: '学生票'
}]
}, {
title: '故宫预售成人票'
}]
}
},
methods: {
getDetailInfo () { /*
2️⃣-③:在 methods 中定义 getDetailInfo 方法,使用 Axios 发送请求,
当请求成功时,调用 getDetailInfoSucc 方法;
*/
axios.get('/api/detail.json?id=' + this.$route.params.id)
.then(this.getDetailInfoSucc)
},
getDetailInfoSucc (res) { // 2️⃣-④:getDetailInfoSucc 中输出请求到的数据;
console.log(res)
}
},
mounted () { // 2️⃣-②:在 mounted 生命周期函数中调用 getDetailInfo 方法获取数据;
this.getDetailInfo()
}
}
</script>
<style lang="stylus" scoped>
</style>
保存后,返回页面,打开 Network 中的 XHR 可以看到:在首页点击“景点”进入详情页后,发送了一次请求,并携带了参数 id ,成功获取到了数据。
2️⃣-⑤:返回 Detail.vue ;
<template>
<div>
<!-- 2️⃣-⑲:将 sightName、bannerImg、bannerImgs 三个数据传递给 Banner.vue; -->
<detail-banner
:sightName="sightName"
:bannerImg="bannerImg"
:bannerImgs="bannerImgs"
></detail-banner>
<detail-header></detail-header>
<!-- 2️⃣-⑳:将数据 categoryList 传递给 List.vue; -->
<detail-list :list="categoryList"></detail-list>
<!-- 2️⃣-㉑:将数据 commentList 传递给 Comment.vue。 -->
<detail-comment :commentList="commentList"></detail-comment>
</div>
</template>
<script>
import DetailBanner from './components/Banner'
import DetailHeader from './components/Header'
import DetailList from './components/List'
import DetailComment from './components/Comment'
import axios from 'axios'
export default {
name: 'Detail',
components: {
DetailBanner,
DetailHeader,
DetailList,
DetailComment
},
data () {
return { // ❗️删除 data 中写死的数据 list。
sightName: '', // 2️⃣-⑨:data 中定义一个变量 sightName 为空(即景点名称);
bannerImg: '', // 2️⃣-⑩:data 中定义一个变量 bannerImg 为空(即 Banner 图片);
bannerImgs: [], /*
2️⃣-⑪:data 中定义一个变量 bannerImgs 为空数组(即点在 Gallery 中显示
的图片);
*/
categoryList: [], // 2️⃣-⑫:data 中定义一个变量 categoryList 为空数组(即门票类别);
commentList: [] // 2️⃣-⑬:data 中定义一个变量 commentList 为空数组(即用户评论内容);
}
},
methods: {
getDetailInfo () {
// axios.get('/api/detail.json?id=' + this.$route.params.id)
axios.get('/api/detail.json', { /*
❗️发送请求时,在请求的 API 中拼接后边的参数实际很麻烦,
我们可以在前边只写接口名,再在后边写一个对象,对象中有
一个 params,然后将参数放在 params 中。
*/
params: {
id: this.$route.params.id
}
}).then(this.getDetailInfoSucc)
},
getDetailInfoSucc (res) {
res = res.data // 2️⃣-⑥:将获取到的数据中的 data 赋值给变量 res;
if (res.ret && res.data) { /*
2️⃣-⑦:如果 res.ret 为 true(即,后端正确返回了结果),
并且 res 中有对应的数据 data;
*/
const data = res.data // 2️⃣-⑧:将 res.data 赋值给变量 data;
this.sightName = data.sightName /*
2️⃣-⑭:将数据项中的 sightName 赋值
给 this.sightName;
*/
this.bannerImg = data.bannerImg /*
2️⃣-⑮:将数据项中的 bannerImg 赋值
给 this.bannerImg;
*/
this.bannerImgs = data.galleryImgs /*
2️⃣-⑯:将数据项中的 galleryImgs 赋值
给 this.bannerImgs;
*/
this.categoryList = data.categoryList /*
2️⃣-⑰:将数据项中的 categoryList 赋值
给 this.categoryList;
*/
this.commentList = data.commentList /*
2️⃣-⑱:将数据项中的 commentList 赋值
给 this.commentList;
*/
}
}
},
mounted () {
this.getDetailInfo()
}
}
</script>
<style lang="stylus" scoped>
</style>
3️⃣打开 detail 下 components 中的 Banner.vue 接收数据:
<template>
<div>
<div class="banner" @click="handleBannerClick">
<!-- 3️⃣-③:动态绑定 .banner-img 的 src 为 bannerImg;-->
<img class="banner-img" :src="bannerImg">
<div class="banner-info">
<!-- 3️⃣-②:.banner-title 的景区名渲染为 this.sightName; -->
<div class="banner-title">{{this.sightName}}</div>
<div class="banner-number">
<span class="iconfont banner-icon"></span>
<!-- 3️⃣-⑤:图标旁的数字渲染为 this.bannerImgs 的长度(即,共有几张图片); -->
{{this.bannerImgs.length}}
</div>
</div>
</div>
<!-- 3️⃣-④:传递给 Gallery.vue 的数据替换为 bannerImgs; -->
<common-gallery :imgs="bannerImgs" v-show="showGallery" @close="handleGalleryClose"></common-gallery>
</div>
</template>
<script>
import CommonGallery from 'common/gallery/Gallery'
export default {
name: 'DetailBanner',
props: { /*
3️⃣-①:Banner.vue 接收到父组件传递过来的三个参数,sightName 和 bannerImg 类型为
字符串, bannerImgs 类型为数组 Array;
*/
sightName: String,
bannerImg: String,
bannerImgs: Array
},
data () { // ❗️删除写死的数据 imgs。
return {
showGallery: false
}
},
methods: {
handleBannerClick () {
this.showGallery = true
},
handleGalleryClose () {
this.showGallery = false
}
},
components: {
CommonGallery
}
}
</script>
<style lang="stylus" scoped>
.banner
position: relative
overflow: hidden
height: 0
padding-bottom: 55%
.banner-img
width: 100%
.banner-info
position: absolute
left: 0
right: 0
bottom: 0
display: flex
line-height: .6rem
color: #fff
background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .8))
.banner-title
flex: 1
padding: 0 .2rem
font-size: .32rem
.banner-number
height: .32rem
padding: 0 .4rem
margin-top: .14rem
font-size: .24rem
color: #fff
line-height: .32rem
border-radius: .2rem
background: rgba(0, 0, 0, .8)
.banner-icon
font-size: .24rem
</style>
3️⃣-⑥:打开 detail 下 components 中的 Comment.vue 接收数据;
<template>
<div>
<div class="title">用户评论</div>
<div
class="comment border-bottom"
v-for="item of commentList"
:key="item.id"
>
<div class="stardate">
<p class="star-level">{{item.star}}</p>
<p class="comment-date">{{item.date}}</p>
</div>
<p class="comment-content">{{item.content}}</p>
<div class="imgs">
<div
class="img-wrapper"
v-for="(innerItem, index) of item.imgUrl"
:key="index"
>
<img :src="innerItem" class="comment-img">
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DetailComment',
// ❗️删除组件中写死的数据。
props: { // 3️⃣-⑦:Comment.vue 接收父组件传递的数据 commentList。
commentList: Array
}
}
</script>
<style lang="stylus" scoped>
.title
margin-top: .2rem
line-height: .8rem
background: #eee
text-indent: .2rem
.comment
padding: .1rem .2rem .2rem .2rem
.stardate
position: relative
height: .6rem
line-height: .6rem
.star-level
position: absolute
float: left
top: .15rem
left: 0
line-height: .3rem
color: #ffb436
.comment-date
position: absolute
float: right
top: .15rem
right: 0
line-height: .3rem
font-size: .24rem
.comment-content
line-height: .44rem
.imgs
overflow: hidden
width: 100%
height: 0
padding-bottom: 50%
.img-wrapper
overflow: hidden
float: left
width: 32.85%
height: 0
padding-bottom: 23%
margin: .1rem 0 0 .1rem
&:nth-child(1),
&:nth-child(4)
margin-left: -.1rem
.comment-img
width: 100%
</style>
保存后,返回页面查看。控制台无报错,详情页数据正确渲染,但再进入不同景点时,没有请求对应 id 的景点详情:
3 <keep-alive> 的 exclude 属性
🔗前置知识:《城市选择页开发——⑧ 使用 keep-alive 优化网页性能》
其实“首次进入页面发送请求,再次进入不同页面不请求”这个问题,我们在城市选择页开发使用 <keep-alive> 时已经遇到过,且借助 <keep-alive> 中的 activated 生命周期函数实现了“适时获取参数不同的数据”的需求。
但除了在 activated 中重新发送请求之外, <keep-alive> 还有一个 exclude 属性,可以使名称匹配的组件不被缓存**。那么每次进入详情页时,便都会重新发送请求了。
4️⃣打开 App.vue 在 <keep-alive> 标签上添加 exclude 属性:
<template>
<div id="app">
<!-- 4️⃣-①:exclude 属性后边为不需要被缓存的组件的名字 Detail; -->
<keep-alive exclude="Detail">
<router-view/>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
</style>
保存后,返回页面查看。控制台无报错,当点击不同景点后,能够获取到不同参数的景点数据。但,当在详情页滚动时,渐隐渐现的导航栏“消失了”:
❓为什么给 Detail.vue 添加了 exclude 属性后,详情页 Header 组件的导航栏失效了呢?
答:因为添加 exclude 属性后,详情页不会被缓存,即 <keep-alive> 不会被激活。
而 Header 组件的导航栏渐隐渐现的实现,是在 activated 生命周期函数中全局绑定了 scroll 事件,触发后执行对应方法。但 <keep-alive> 不被激活,activated 就不会被调用,也就不会再执行对应的方法。
4️⃣-②:打开 detail 下 components 中的 Header.vue ;
<template>
<div>
<router-link
tag="div"
to="/"
class="header-abs"
v-show="showAbs"
>
<span class="iconfont header-abs-back"></span>
</router-link>
<div
class="header-fixed"
v-show="!showAbs"
:style="opacityStyle"
>
<router-link to="/">
<span class="iconfont header-fixed-back"></span>
</router-link>
景点详情
</div>
</div>
</template>
<script>
export default {
name: 'DetailHeader',
data () {
return {
showAbs: true,
opacityStyle: {
opacity: 0
}
}
},
methods: {
handleScroll () {
const top = document.documentElement.scrollTop
if (top > 50) {
let opacity = top / 150
opacity = opacity > 1 ? 1 : opacity
this.opacityStyle = {opacity}
this.showAbs = false
} else {
this.showAbs = true
}
}
},
mounted () { // 4️⃣-③:将 activated 替换为 mounted 生命周期函数;
window.addEventListener('scroll', this.handleScroll)
},
beforeDestory () { // 4️⃣-④:将 deactivated 替换为 beforeDestroy 生命周期函数。
window.removeEventListener('scroll', this.handleScroll)
}
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.header-abs
position: absolute
top: .2rem
left: .2rem
width: .8rem
height: .8rem
line-height: .8rem
text-align: center
border-radius: .4rem
background: rgba(0, 0, 0, .8)
.header-abs-back
color: #fff
font-size: .56rem
.header-fixed
position: fixed
top: 0
left: 0
right: 0
z-index: 2
height: $headerHeight
line-height: $headerHeight
color: #fff
text-align: center
font-size: .32rem
background: $bgColor
.header-fixed-back
position: absolute
top: 0
left: 0
width: .64rem
text-align: center
font-size: .56rem
color: #fff
</style>
保存后,返回页面查看:
以上,我们完成了详情页中数据的动态渲染。
祝好,qdywxs ♥ you!