(23)详情页开发——⑤ AJAX 动态获取详情页数据 | Vue.js 项目实战: 移动端“旅游网站”开发

97 阅读3分钟
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 需求

使用 AJAX 动态获取详情页数据,点击不同景点时,获取不同 id 的数据: 01.gif

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 ,成功获取到了数据。

02.gif

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">&#xe64a;</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 的景点详情:

03.gif

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>

保存后,返回页面查看。控制台无报错,当点击不同景点后,能够获取到不同参数的景点数据。但,当在详情页滚动时,渐隐渐现的导航栏“消失了”:

04.gif

❓为什么给 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">&#xe658;</span>
    </router-link>
    <div
      class="header-fixed"
      v-show="!showAbs"
      :style="opacityStyle"
    >
      <router-link to="/">
        <span class="iconfont header-fixed-back">&#xe658;</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>

保存后,返回页面查看:

05.gif

以上,我们完成了详情页中数据的动态渲染。

祝好,qdywxs ♥ you!