• 【音乐App】—— Vue-music 项目学习笔记:推荐页面开发


    前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。

    上一篇总结了项目概述、项目准备、页面骨架搭建。这一篇重点梳理推荐页面开发。项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。


    一、页面简介+轮播图数据分析
    • 数据:从QQ音乐抓取的真实数据
    轮播图 热门歌单推荐
    二、JSONP原理介绍
    • 一句话解释JSONP原理:动态生成一个JavaScript标签,其src由接口url、请求参数、callback函数名拼接而成;利用js标签没有跨域限制的特性实现跨域请求
    • 有几点需要注意:
    1. callback函数要绑定在window对象上
    2. 服务端返回数据有特定格式要求:callback函数名+’(‘+JSON.stringify(返回数据) +’)’
    3. 不支持post,因为js标签本身就是一个get请求
    • 什么是Promise:
    1. 简单说就是一个容器,里面保存着某个未来才会结束的事件 (通常是一个异步操作)的结果。
    2. 从语法上说,Promise是一个对象,从它可以获取异步操作的消息
    • Promise基本用法:
    1. ES6规定,Promise对象是一个构造函数,用来生成Promise实例
      var promise = new Promise(function(resolve,reject){
          // ... some code
          if(/* 异步操作成功 */){
             resolve(value);
          }else{
             reject(error);
          }
      });
    2. Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不是自己部署。
    3. Promise实例生成以后,可以用then方法分别制定Resolved状态和Rejected状态的回调函数:
      promise.then(function(value){
               // sucess
      },function(error){
               // failure
      });
    三、JSONP
    • 安装JSONP依赖:
      npm install jsonp --save
    四、封装JSONP、Primise
    • common->js目录下: 创建 jsonp.js
      import originJSONP from 'jsonp'
      
      export default function jsonp(url, data, option) {
                url += (url.indecOf('?') < 0 ? '?' : '&') + param(data);
      
                return new Promise((resolve, reject) => {
                     originJSONP(url, option, (err, data) => {
                          if(!err){
                               resolve(data)
                          }else{
                               reject(err)
                          }
                    })
               })
      }
      
      function param(data) {
              let url = ""
              for(var k in data){
                   let value = data[k] !== undefined ? data[k] : ''
                   url += `&${k}=${encodeURIComponent(value)}`
              }
              return url ? url.substring(1) : ''
      }
    五、JSONP的应用+轮播图数据抓取
    • api目录下创建 config.js:配置与接口统一的参数
      /**
      * 为了和QQ音乐接口一致,配置一些公用的参数、options和err_num码
      */
      export const commonParams = {
                g_tk: 5381,  //会变,以实时数据为准
                inCharset: 'utf-8',
                outCharset: 'utf-8',
                notice: 0,
                format: 'jsonp'
      }
      
      export const options = {
                param: 'jsonpCallback'
      }
      
      export const ERR_OK = 0
    • api目录下创建 recommend.js
      import jsonp from '@/common/js/jsonp'
      import {commonParames, options} from './config'
      
      export function getRecommend() {
             const url = 'https://c.y.qq.com/musichall/fcgi-bin/fcg_yqqhomepagerecommend.fcg'
              
             const data = Object.assign({}, commonParames, {
                   platfrom: 'h5',
                   uin: 0,
                   needNewCode: 1
             })
             return jsonp(url, data, options)
      }
    • recommend.vue中调用并获取数据
      import {getRecommend} from '@/api/recommend'
      import {ERR_OK} from '@/api/config'
      
      export default {
          created() {
                 this._getRecommend();
          },
          methods: {
                 _getRecommend() {
                         getRecommend().then((res) => {
                               if(res.code === ERR_OK) {
                                    console.log(res.data.slider)
                               }
                         })
                   }
           }
      }
    六、 轮播图组件实现
    • base目录下: 创建slider.vue组件
    • 插槽<slot></slot>:外部引用slider.vue时,<slider></slider>里面包裹的DOM,会被插入到插槽的部分
      <div class="slider-group">
           <slot></slot>
      </div>
    • recommend.vue 中编写插槽中的DOM:
      <slider>
            <div v-for="(item, index) in recommends" :key="index">
                   <a :href="item.linkUrl">
                          <img :src="item.picUrl">
                   </a>
             </div>
      </slider> 
    • slider.vue 中指定需要从父组件接收的属性:loop是否循环、autoPlay是否自动播放、interval间隔时间
      props: {
           loop: {
                type: Boolean,
                default: true
          },
          autoPlay: {
                type: Boolean,
                default: true
          },
          interval: {
                type: Number,
                default: 4000
          }
      }
    • 横向滚动:使用better-scroll

    better-scroll中文文档:https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/options-advanced.html#snap

    better-scroll中的相关选项:

    • snap:专门为slide组件使用的
    • SnapSpeed : 轮播图切换的动画时间
    1. 安装better-scroll依赖:
      npm install better-scroll --save
    2. slider.vue 中引用:
      import BScroll from 'better-scroll'
    3. ref引用外层容器和内层元素:

      <div class="slider" ref="slider">
      <div class="slider-group" ref="sliderGroup">
    4. common->js目录下创建 dom.js:封装一些DOM操作相关的代码

      //为元素添加Class、判断元素是否有指定class
      export function addClass(el, className){
               if(hasClass(el, className)){
                        return
               }
               let newClass = el.className.split(' ')
               newClass.push(className)
               el.className = newClass.join(' ')
      }
      
      export function hasClass(el, className){
              let reg = new RegExp('(^|\s)' + className + '(\s|$)')
              return reg.test(el.className)
      }
    5. slider.vue 中引用:
      import {addClass, hasClass} from '@/common/js/dom'
    6. 在methods中定义两个方法:设置slider宽度、初始化slider

      methods: {
           _setSliderWidth() {
                  this.children = this.$refs.sliderGroup.children
      
                  let width = 0
                  let sliderWidth = this.$refs.slider.clientWidth
                  for(let i=0; i < this.children.length; i++) {
                       let child = this.children[i]
                       addClass(child, 'slider-item')//为循环生成的slider子元素,动态添加slider-item class
                       child.style.width = sliderWidth + 'px'//不要忘记加单位!
                       width += sliderWidth
                  }
      
                  if(this.loop){ //如果loop为true,BScroll的snap属性会左右克隆两个DOM,保证循环切换
                      width += 2 * sliderWidth
                  }
      
                  this.$refs.sliderGroup.style.width = width + 'px'//不要忘记加单位!
           },
           _initSilder() {
                  this.slider = new BScroll(this.$refs.slider,{
                        scrollX: true, //横向滚动
                        scrollY: false, //禁止纵向滚动
                        momentum: false,//禁止惯性运动
                        snap: {
                             loop: this.loop,
                             threshold: 0.3,
                             speed: 400
                       }
                 })
          }
      }
    7. 初始化BScroll的时机:必须保证组件已经渲染好了,DOM高度已经被撑开
      //在mouted生命钩子中通过setTimeout调用:
      mouted() {
           setTimeout(() => {
                  this._setSliderWidth()
                  this._initSlider()
            }, 20)
      }
    8. 坑:recommend.vue中直接引用了<slider>,recommends的引用时机是在created()中调用了_getRecommend(),_getRecommend()的这个时间是一个异步过程,可能会有延迟,因为它取的是真实数据;因此,当recommends还没有get到时,即还没有填入任何数据时,slider.vue中的mouted()实际上已经执行了。
    9. 解决:recommend.vue中为slider-wrapper添加v-if="recommends.length",确保recommends数组中有内容时,才渲染<slider>
      <div v-if="recommends.length" class="slide-wrapper">
    • 添加dots区块,实现自动轮播
    1. data中维护一个数据dots,默认是一个空数组
      dots: []
    2. methods中初始化Dots:
      _initDots() {
          this.dots = new Array(this.children.length)
      }
    3. 渲染dots:
      <span class="dot" v-for="(item, index) in dots" :key="index"></span>
    4. 选中高亮:
      /**  data中维护一个数据currentPageIndex:0,表示当前默认是第一页
       *   v-bind动态绑定 :class="{active: currentPageIndex === index}">
       *   在_initSlider()方法中给slider添加事件:
      */
      
      this.slider.on('scrollEnd', () => { //当一个页面滚动完毕后,会派发一个scrollEnd事件
               let pageIndex = this.slider.getCurrentPage().pageX //获得slider的pageIndex
               if(this.loop) { //如果是循环,snap会默认给子元素前面增加一个拷贝
                  pageIndex -= 1 //要得到实际的pageIndex,pageInde需要-1
               }
               this.currentPageIndex = pageIndex
      })
    5. 自动播放:
      //mounted()->setTimeout中判断autoplay属性,调用_play(): 
      if(this.autoplay) {
         this._play()
      }
      
      //methods中定义_play():
      _play() {
         let pageIndex = this.currentPageIndex + 1;//this.currentPageIndex从0开始的
         if(this.loop) {
            pageIndex += 1//loop为true时,最开始有一个复制的副本,实际的pageIndex需要+1
         }
         this.timer = setTimeout(() => { //页面的切换,利用BScroll的接口goToPage
              this.slider.goToPage(pageIndex, 0, 400) //参数:X方向、Y方向、时间间隔
         },this.interval)
      }
    6. 坑:使用setTimeout,只会执行一次,从第一张自动滚动到第二张就停止了。
    7. 解决:scrollEnd事件中添加:
      if(this.autoPlay) {
         this._play()
      }
    8. 坑:自动滚动后不到400ms时,手动滑动后又执行了自动滚动,体验效果会很奇怪
    9. 解决:slider 添加 beforeScrollStart事件
      this.slider.on('beforeScrollStart', () => {
            if (this.autoPlay) {
                clearTimeout(this.timer)
            }
      })
    10. 坑:在滚动中,改变视口大小,图片会同时显示两张,因为之前设置好的width都没变
    11. 解决:mounted中监听window的resize事件 —— 窗口改变事件,当窗口改变时,重新调用_setSlideWidth()
    12. 坑:如果窗口变和不变时都调用_setSlideWidth(),就会执行两次width += 2 * sliderWidth,这一定是不对的
    13. 解决:调用_setSlideWidth(),需要同时传入一个参数,用来判断窗口是否改变了
      window,addEventListener('resize',(() => {
            if(!this.slider) {
                return
            }
            this._setSliderWidth(true)
            this.slider.refresh()
      }))
      
      _setSliderWidth(isResize) {
           //其它代码
           if(this.loop && !isResize){ 
               width += 2 * sliderWidth
           }
      }
    14. App.vue 中优化:缓存DOM到内存中,不用重新发送请求,这样slider就不会有闪动的现象

      <keep-alive>
            <router-view></router-view>
      </keep-alive> 
    15. slider中优化:当组件中有定时器,一定要记得在组件销毁时清理掉这些定时器,使用生命周期destroyed()
      destroyed() {
          clearTimeout(this.timer)
      }
    七、歌单数据接口分析

    问题: QQ音乐歌单数据的请求头中有域名Host、来源Referer,所以请求的接口应该是有加上该域名和来源,直接请求就会报HTTP-500错误。

    原因: 前端不能直接修改request header,所以要通过后端代理的方式解决

    解决: 采用 axios 在node.js中发送http请求

    •  安装axios: 
      npm install axios --save
    • build->webpack.dev.conf.js
    1. 定义路由,通过axios发送一个Http请求,同时修改header中的和QQ相关的Host、Referer,
    2. 将浏览器传递过来的参数全部传给服务端,然后通json响应的内容输出到浏览器端。
    3. 在 const portfinder = require('portfinder') 后添加:
      const express = require('express')
      const axios = require('axios')
      const app = express()
      var apiRoutes = express.Router()
      app.use('/api', apiRoutes)
    4. devServer 中添加:
      before(app) {
         //定义getDiscList接口,回调传入两个参数,前端请求这个接口
         app.get('/api/getDiscList', function(req, res){
               var url = "https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg"
               axios.get(url, {
                    headers: { //通过node请求QQ接口,发送http请求时,修改referer和host
                    referer: 'https://y.qq.com/',
                    host: 'c.y.qq.com'
              },
              params: req.query //把前端传过来的params,全部给QQ的url
         }).then((response) => { //成功与失败的回调
              res.json(response.data)
         }).catch((e) => {
              console.log(e)
         })
      })
    • recommend.js中:
      import axios from 'axios';
      
      export function getDiscList() {
              // const url = 'https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg'
              const url = '/api/getDiscList' //调用自定义的接口
      
              const data = Object.assign({}, commonParams, {
                      platform: 'yqq',
                      hostUin: 0,
                      sin: 0,
                      ein: 29,
                      sortId: 5,
                      needNewCode: 0,
                      categoryId: 10000000,
                      rnd: Math.random(),
                      format: 'json' //使用的时axios,所以format使用的是json,不是jsonp
             })
      
             // return jsonp(url, data, options)
             return axios.get(url, {
                     params: data
             }).then((res) => {
                    return Promise.resolve(res.data) //es6新语法,返回一个以给定值解析后的Promise对象
             })
      }
    • Promise.resolve(value)方法返回一个以给定值解析后的Promise对象;但如果这个值是个thenable(即带有then方法),返回的promise会“跟随” 
    • 这个thenable的对象,采用它的最终状态(指resolved/rejected/pending/settled);
    • 如果传入的value本身就是promise对象,则该对象作为Promise.resolve方法的返回值返回;否则以该值为成功状态返回promise对象。
    • recommend.vue中:定义和调用获取数据的方法
      //created()中:
       this._getDiscList(); 
      
      //methods中:
       _getDiscList() {
          getDiscList().then((res) => {
               if(res.code === ERR_OK) {
                  console.log(res.data)
               }
         })
      }
    八、歌单列表组件开发和数据的应用
    • data中定义数据:  
      discList: []
    •  _getDiscList()中将返回的数据list赋给discList:
      this.discList = res.data.list
    • 使用 v-html="item.creator.name" 给html字符做转义
      <div class="recommend-list">
           <h1 class="list-title">热门歌单推荐</h1>
           <ul>
               <li v-for="(item, index) in discList" :key="index" class="item">
                   <div class="icon">
                         <img :src="item.imgurl" width="60" height="60">
                   </div>
                   <div class="text">
                         <h2 class="name" v-html="item.creator.name"></h2>
                         <p class="desc" v-html="item.dissname"></p>
                   </div>
               </li> 
           </ul>
      </div>
    • CSS样式:经典flex布局
    1. 左边固定宽高,右边根据手机视口宽度自适应
    2. 右侧:
      .item
          display: flex 
          align-items:center //水平方向居中
    3. 右侧文字内容:
      .text
          display: flex
          flex-direction: column //纵向排列
          justify-content: center //垂直居中
    4. 一个元素,既可以是flex布局的item,同时也可做flex布局
    九、scroll组件的抽象和应用
    •   better-scroll滚动布局:只会滚动父元素下的第一个子元素 —— 想要slider和recommend-list同时可以滚动,需要在外层再嵌套一个<div>,将两个元素包裹起来
    •  抽象出scorll组件 -- 基础组件
    1. base->scroll目录下: 创建 scroll.vue
    2. 布局DOM:一个wrapper加一个插槽
      <template>
         <div ref="wrapper">
             <slot></slot>
         </div>
      </template>
    3. 引入BScroll:

      import BScroll from 'better-scroll'
    4. 需要传入props参数:
      props: {
         //probeType: 1 滚动的时候会派发scroll事件,会截流。2 滚动的时候实时派发scroll事件,不会截流 。3 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件
         probeType: { 
                  type: Number, 
                  default: 1
         },
        // click: true 是否派发click事件,通常判断浏览器派发的click还是betterscroll派发的click,可以用event._constructed,若是bs派发的则为true
         click: { 
                  type: Boolean,
                  default: true
         },
         data: {
                  type: Array,
                  default: null
         }
      }
    5. 确保DOM已经渲染,再执行_initScroll:
      mouted() {
          setTimeout(() => { //确保DOM已经渲染
               this. _initScroll()
          }, 20)
      }
    6. methods中定义初始化scroll的方法,并代理几个必需的方法:
      methods: {
            _initScroll() {
                  if(!this.$refs.wrapper){
                           return
                  }
                  this.scroll = new BScroll(this.$refs.wrapper, {
                        probeType : this.probeType,
                        click: this.click
                  })
            },
            enable() {
                  // 启用 better-scroll,默认开启
                  this.scroll && this.scroll.enable()
            },
            disable() {
                 // 禁用better-scroll, 如果不加,scroll的高度会高于内容的高度
                 this.scroll && this.scroll.disable()
            },
            refresh() {
                 // 强制 scroll 重新计算,当 better-scroll 中的元素发生变化的时候调用此方法
                 this.scroll && this.scroll.refresh()
            }
      }
    7. watch监听data数据:
      watch: {
           data() { //监测data的变化
                setTimeout(() => {
                     this.refresh()
                }, 20)
           }
      }
    8. 后面在项目的开发中,可以根据需要再随时添加props参数和methods代理方法
    • recommend.vue 中使用:
    1. 引用scroll组件:
      import Scroll from '@/base/scroll/scroll'
    2. 把class="recommend-content"的<div>改成<scroll>
    3. 坑:此时scroll已经初始化了,但还不能滚动
    4. 原因:scroll初始化的时机,是在scroll组件的mounted();但<scroll>包含的DOM是由获取到的data数据填充撑开高度才可以滚动,此时还没撑开,就滚动不了;当数据改变后,scroll应该改变
    5. 解决:<scroll>传入一个数据 :data="discList";当数据discList接收到时,scroll组件中的watch监听到这个变化,就会强制scroll重新计算
    6. 坑:因为整个页面会有两个部分都是请求数据,当_getRecommend()的请求时间大于this._getDiscList()的时候,页面的高度就不够
    7. 如果:如下 ↓ 滚动的高度就会差一个slider的高度,滚不到底部。
      因为refresh()之前,slider的数据还没有渲染出来,scroll会认为,需要滚动的高度,只是列表的高度
      created() { 
          setTimeout(() => {
               this._getRecommend();
          }, 1000) 
          this._getDiscList(); 
      }
    8. 实际中,并不能知道两个部分,哪一个会先出现,需要注意还有一个坑:不能用计算属性计算两个部分的数据
    9. 原因:与图片的加载,视口的大小(实时图片的宽高)有关。
    10. 解决:给<img>添加onload事件
      <img :src="item.picUrl" @load="loadImage">
      loadImage() {
           if(!this.checkloaded){ //添加一个标志位,如果load一次了,就不再执行onload事件了
               this.checkloaded = true
               this.$refs.scroll.refresh()
           }
      }
    十、 lazyload懒加载插件介绍和应用
    • 歌单优化:歌单是由很多张图片组成的,使用vue-lazyload插件 解决图片懒加载 的问题
    • vue-lazyload github地址: https://github.com/hilongjw/vue-lazyload
    • 安装插件: 
      npm install vue-lazyload --save
    • 引用注册: main.js 中
      import VueLazyload from 'vue-lazyload'
      
      Vue.use(VueLazyload, {
         loading: require('@/common/image/default.png') //loading时默认显示的图片
      })
    • 使用插件:recommend.vue 中把歌单列表<img>中原来的 :src替换为v-lazy
      <img v-lazy="item.imgurl" width="60" height="60">
    • 这样,只有用户滚动过的地方,图片才会加载,没有看的地方,就不会进行加载
    • 问题:fastclick和better-scroll的click会有冲突.
    • 解决:slider中的<img>添加一个class="needsclick",这是fastclick中的一个属性
      <img class="needsclick" :src="item.picUrl" @load="loadImage">
    十一、 loading基础组件的开发和应用
    • 优化体验:在歌单列表没有渲染好之前,展示一个转圈loading
    • 布局DOM:
      <div class="loading">
             <img width="24" height="24" src="./loading.gif">
             <p class="desc">{{title}}</p>
      </div>
    • props参数:
      props: {
           title: {
               type: String,
               default: '正在载入...'
           }
      }
    • CSS样式:
      View Code
    • recommend.vue 中引用注册,在<scroll>中使用:
      <div class="loading-container" v-show="!disList.length">
             <loading></loading>
      </div>

     


    注:项目来自慕课网

  • 相关阅读:
    JavaScript数字和字符串转换示例
    Angular CLI 使用教程指南参考
    angular2 post以“application/x-www-form-urlencoded”形式传参的解决办法
    Arison [JS]window.location获取url各项参数详解
    $.ajaxComplete()
    angular2 编写公用组件
    获取本周、本季度、本月、上月的开端日期、停止日期
    Angular2
    轮播图插件 SuperSlide2.1滑动门jQuery插件
    书写Css文件要点
  • 原文地址:https://www.cnblogs.com/ljq66/p/10159589.html
Copyright © 2020-2023  润新知