webapp music

知识 疑问 未完 注意

1 介绍

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

异步加载用到了loading组件,

懒加载,

滚动是用的scroll组件,

截流。。

2项目初始化和推荐页面开发

使用脚手架创建项目

新建项目

https://cli.vuejs.org/zh/guide/

vue create xxx

手动选择features

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

因为选择了router 询问是否用history模式,否 使用哈希模式

在这里插入图片描述

css预处理器选择哪种

在这里插入图片描述

eslint配置 标准配置

在这里插入图片描述

保存代码的时候执行lint

在这里插入图片描述

bebel,eslint这些配置文件放在哪里

放在相应的配置文件中,还是放在package.json中

在这里插入图片描述

要不要保存成一个预设

在这里插入图片描述

代码介绍

package.json项目的描述文件

npm run serve的时候 就会执行script里面描述的vue-cli-service serve这样一个命令 也就是说通过vue-cli内置的服务去开启本地的服务

当执行npm run build,serve,lint的时候其实就是执行script里面的脚本

dependencies是项目运行时的依赖(会被打包到项目里),就是说这里的依赖的代码最终会被打包 打包到vendor.js中,浏览器运行项目的时候,他会请求到这些js

devDependencies开发时的依赖,

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

package-lock对npm依赖版本的锁,第一次安装之后会生成 package-lock文件,之后如果不手动修改依赖版本号,他就不会安装最新版本的,会使用lock版本,直接从缓存里取就行了,就是说除非手动去升级版本号,他对比到lock文件发现版本不一致 才会下载新的文件,

在这里插入图片描述

那这个js是怎么来的呢 main.js:js入口文件 ,就是说在编译阶段就会动态的找到main.js然后做一些编译,编译好之后插入到刚刚那个div里。
js运行的时候实际上就是运行main.js了

那么main.js是如何跑的呢
import createApp这个函数,import App组件
通过createApp这个方法生成一个App实例,
.use:vue3使用插件的方式, store和router实际上都是在两个文件里,
暴露方法 store是暴露的createStore方法,
router是暴露createRouter方法
内部其实有一个install方法,让我们去安装这个插件,
就是说执行.use(store)和.use(router)会执行内部的初始化工作,
createApp(App).use(store).use(router)可以看出来这个语句是支持链式调用的,就是说.use以后也会返回App对象
最终.mount(’#app’)会挂载到id为app的节点上,把它渲染出来。

在这里插入图片描述

来看看App组件

有个id为nav的导航
里面有两个router-link 这个东西怎么看呢,去路由里面看

在这里插入图片描述

每个路径他会渲染不同的组件, 根路径:Home组件,/about路径:异步加载view下面的About组件

在这里插入图片描述

点router-link它会生成一些a标签,router-view会渲染组件的模块,当请求/about就会渲染About组件

Home组件,里面有个img, 图片是一个vue logo 存储在assets(静态资源相关目录) 还有个HelloWorld组件,他是在components组件里面去维护
就是说路由对应的视图是用的views目录去维护的,路由里面用到的组件会放到components里面维护

在这里插入图片描述

项目基础代码

public/index.html

修改name为viewport这样一个meta标签,设置缩放比例为1,并且禁止用户双击缩放。这是开发移动端的一个技巧。

知识

 <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0,user-scalable=no">

css

对assets目录做些修改

增加fonts,scss目录

在这里插入图片描述

fonts:把svg导出为图标字体文件,然后去页面引用它,

scss:
variable 这样就不用使用具体的颜色 而是可以直接使用变量 字体规范也可以用这种方法

在这里插入图片描述

mixin:可以理解为定义css函数,使用mixin实际上就是把它内部的代码插入到使用mixin的地方,这是一种从css层面进行的封装,

知识

在这里插入图片描述

在这里插入图片描述

index.scss:

在这里插入图片描述

reset.scss:

对基础的标签做样式的重置,从网上拷过来就ok

icon.scss:

在这里插入图片描述

以icon-开头的都应用这个样式

在这里插入图片描述

之后直接使用icon-ok这些名字就ok

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

base.scss:基础全局的样式

在这里插入图片描述

js

先从入口文件看起,main.js

引入全局样式文件,这样我们在assets文件夹里面写的东西才能生效

知识

在这里插入图片描述

这个index.scss定义了它依赖的文件,在这里插入图片描述

这样就不用挨个引入了

App.vue 引入header组件

在这里插入图片描述

在这里插入图片描述

icon就对应的图标,text对应的文案

css:

这个是icon和text分别垂直居中的 icon垂直居中是算出来的,然后margin top 。text是用的line-height

用flex布局更方便

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

图片来源,

在这里插入图片描述

两种图片,大小不一样

为什么呢 用在不同的dpi浏览器上,dpi为2的话用2x就够了,3x就浪费了。

dpi :dots per inch , 直接来说就是一英寸多少个像素点。常见取值 120,160,240。我一般称作像素密度,简称密度

所以这里使用mixin 就是为了解决这个问题。

dpi为3时,就用3x的图片,否则就用2x的图片

还有一个吧views里面的文件都删了,

然后把router里面的路由删了

在这里插入图片描述

有个问题,为什么在scss/index.scss里面没有引入 mixin和variable呢 因为mixin和变量都不是实体的配置(这是给sass提供的一些东西,css是不认的),所以在scss/index.scss里引入是没有意义的

因为mixin可以理解为一个模块化,局部使用的时候import引入就可以,但是这里没有引入为什么也可以使用呢

因为使用的时候每个组件都引入一下就太麻烦了,那能不能全局引入呢,实际上是可以的,

vue-cli脚手架提供了对webpack做额外配置的入口,就是说他对webpack已经做了一层封装了,内部已经有一个webpack config.js了但是也给我们一个机会去修改webpack配置,怎么去修改呢,可以通过配置vue.config.js 在里面配置一些规则其实就是对webpack规则做一些修改。

sass loader

对于.scss文件在整个编译过程中,会使用sass-loader做编译,sass-loader支持一些配置,里面有个additionalData属性,这个属性允许全局引用sass文件,可以添加额外的sass文件,

知识

https://cli.vuejs.org/zh/guide/css.html#%E5%90%91%E9%A2%84%E5%A4%84%E7%90%86%E5%99%A8-loader-%E4%BC%A0%E9%80%92%E9%80%89%E9%A1%B9

在这里插入图片描述

记得把node-sass和sass-loader升级到最新版。最新版才支持这个配置。

这两个scss文件其实是给编译的时候用的,编译的时候会全局引入这些变量,然后在sass-loader编译组件的时候就知道我们全局定义了这些变量,就知道了变量值是什么了,

eslint

在这里插入图片描述

缩进和函数左边的空格的检查去掉

Tab组件

components/tab/tab.vue

知识

路由被激活的时候自动添加一个 class :.router-link-active 高亮效果用得到

<template>
  <div class="tab">
    <router-link
      class="tab-item"
      v-for="tab in tabs"
      :key="tab.path"
      :to="tab.path"
    >
      <span class="tab-link">
        {
   
   {tab.name}}
      </span>
    </router-link>
  </div>
</template>

<script>
  export default {
      
      
    name: 'tab',
    data() {
      
      
      return {
      
      
        tabs: [
          {
      
      
            name: '推荐',
            path: '/recommend'
          },
          {
      
      
            name: '歌手',
            path: '/singer'
          },
          {
      
      
            name: '排行',
            path: '/top-list'
          },
          {
      
      
            name: '搜索',
            path: '/search'
          }
        ]
      }
    }
  }
</script>

<style lang="scss" scoped>
  .tab {
      
      
    display: flex;
    height: 44px;
    line-height: 44px;
    font-size: $font-size-medium;

    .tab-item {
      
      
      flex: 1;
      text-align: center;

      .tab-link {
      
      
        padding-bottom: 5px;
        color: $color-text-l;
      }

      &.router-link-active {
      
      
        .tab-link {
      
      
          color: $color-theme;
          border-bottom: 2px solid $color-theme;
        }
      }
    }
  }
</style>

现在高亮效果是没有的,因为还没有编写路由部分

路由其实就是路径和视图的一个映射。

视图实际上就是一个组件

在这里插入图片描述

在这里插入图片描述

现在就有这种点击加下划线的效果了

在这里插入图片描述

但是发现并没有显示路由组件,

因为虽然配置了路由,但是没有把组件使用上,

知识 微信小程序用的include

App.vue 写一个router-view 路由视图需要router-view做一个承载,而且router-view是支持嵌套的,

当有多层嵌套路由的话 router-view可以再嵌套router-view

知识 history与hash模式的区别

https://www.cnblogs.com/keyng/p/13156468.html

哈希模式有井号

在这里插入图片描述

还有个体验不好的地方,刚进来的时候是没有任何一个选择的,我们需要让他渲染第一个

重定向

在这里插入图片描述

获取轮播图接口数据

首先我们要获取轮播图相关数据,数据是第三方接口提供的,但是前端是不能直接请求这个第三方服务接口的,因为会有浏览器跨域限制,并且这是第三方服务接口,是不会给我们开启跨域的,怎么办呢,我们可以想办法去绕过它

在这里插入图片描述

思路:通过接口代理的方式:自己搭建一个node server,前端页面发送请求到自己的node server,这样就不会有跨域问题了,自己的node server接收到前端请求之后,把这个请求转发到第三方服务接口,这个第三方服务接口接收到请求之后再把数据返回给node server,因为后端之间的通讯是不存在跨域问题的,跨域是浏览器本身的限制,node server接收到数据之后,对数据座一层处理,把处理后的数据再传给前端,这样就可以绕过跨域限制了。

具体代码

对本地开发而言,如何启动node server呢,实际上他是webpack的配置,

知识

devServer配置 这个配置可以利用express来起一个node server

https://cli.vuejs.org/zh/config/#%E5%85%A8%E5%B1%80-cli-%E9%85%8D%E7%BD%AE

提供before一个函数,app就是express的一个实例,就可以调用app来搭建后端路由等等

before:https://www.webpackjs.com/configuration/dev-server/#devserver-before

这里封装了一下,封装成了一个registerRouter函数

在这里插入图片描述

所以我们对第三方服务接口返回的数据所有的处理逻辑全都是放在后端的,在真实的企业开发规范就是要把数据放在后端做处理,处理好了之后前端可以拿来直接用,前端只负责数据渲染和交互,这才是一个比较好的开发方式,

来看一下registerRouter都干了哪些事情

在这里插入图片描述

轮播图组件的开发

BetterScroll

BetterScroll 2.0:支持特性的插件化

better scroll支持核心的滚动库,在核心的滚动库上可以去安装插件

https://github.com.cnpmjs.org/ustbhuangyi/better-scroll

https://github.com/ustbhuangyi/better-scroll/blob/master/README_zh-CN.md

https://better-scroll.github.io/docs/zh-CN/

https://better-scroll.github.io/docs/zh-CN/plugins/slide.html#%E5%85%B3%E4%BA%8E-slide-%E7%9A%84%E6%9C%AF%E8%AF%AD

把基础组件都维护在components/base目录下 components其他目录都是业务组件

其实在真实的开发项目中,基础组件是安装到npm私服 (疑问)然后通过npm 私服去import这些组件然后使用的

这里因为我下载better-scroll/slide依赖的时候老是报错,所以就直接下

better-scroll了

在这里插入图片描述

html+css

<template>
  <div class="slider" ref="rootRef">
    <div class="slider-group">
      <div
        class="slider-page"
        v-for="item in sliders"
        :key="item.pic"
      >
        <!-- <a :href="item.url"> -->
          <img :src="item.pic"/>
        <!-- </a> -->
      </div>
    </div>
    <!-- 有几个配置就显示几个点 -->
    <div class="dots-wrapper">
      <span
        class="dot"
        v-for="(item, index) in sliders"
        :key="item.id"
        :class="{'active': currentPageIndex === index}">
      </span>
    </div>
  </div>
</template>

<script>
  // import { ref } from 'vue'
  // import useSlider from './use-slider'

  export default {
    name: 'slider',
    props: {
      // 轮播图数据
      sliders: {
        type: Array,
        default() {
          return []
        }
      }
    },
    setup(props) {
      
    }
  }
</script>

<style lang="scss" scoped>
  .slider {
    min-height: 1px;
    font-size: 0;
    touch-action: pan-y;
    .slider-group {
      position: relative;
      overflow: hidden;
      white-space: nowrap;
      .slider-page {
        display: inline-block;
        transform: translate3d(0, 0, 0);
        backface-visibility: hidden;
        a {
          display: block;
          width: 100%;
        }
        img {
          display: block;
          width: 100%;
        }
      }
    }
    .dots-wrapper {
      position: absolute;
      left: 50%;
      bottom: 12px;
      line-height: 12px;
      transform: translateX(-50%);
      .dot {
        display: inline-block;
        margin: 0 4px;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: blueviolet;
        &.active {
          width: 20px;
          border-radius: 5px;
          // backgroud: $color-text-ll;
          background: blue;
        }
      }
    }
  }
</style>

下面使用composition Api的方式让组件和整个better-scroll配合起来,来实现轮播图的效果

composition api:

https://v3.cn.vuejs.org/guide/composition-api-introduction.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%BB%84%E5%90%88%E5%BC%8F-api

我们看文档知道better -scroll初始化的时候 都要传入一个wrapper (一个容器) 这个容器可以是个class 也可以是个dom结点,

那么在vue里面如何获取结点呢?–可以给容器加一个ref属性

<div class="slider" ref="rootRef">

熟悉options api的知道,想要获取这个dom结构,可以用this.$ref.rootRef来获取,但是在composition api里怎么拿到这个dom结构呢?

知识点

可以在setup函数中使用ref api ;ref api是vue暴露的

在这里插入图片描述

这样的话rootRef.value就是最终的dom

在用composition api的时候是可以定义一些钩子函数的

在slider初始化的时候可以使用useSlider这样一个钩子函数

新建文件 use-slider.js 暴露一个钩子函数

import BScroll from '@better-scroll/core'
import Slide from '@better-scroll/slide'
// 注册插件
BScroll.use(Slide)
// 传入一个ref对象
export default function useSlider(wrapperRef){
    
    

}

之后就可以new BScroll了

但是不能直接在里面写,为什么呢,因为容器dom渲染是在mounted钩子函数之后的,mounted钩子函数内部是可以初始化的,在函数里直接初始化是不行的,因为外部会直接useslider,在setup里面,是不能拿到这个dom对象的,所以说要在onmounted函数里面去写代码

看官方举的例子 https://v3.cn.vuejs.org/guide/composition-api-introduction.html#setup-%E7%BB%84%E4%BB%B6%E9%80%89%E9%A1%B9

用composition和options api之间最大的区别就是它暴露了很多api,我们需要知道这些api是什么,我们要手动import,options api做一些配置就好了

请添加图片描述

请添加图片描述

知识 挂载的时候初始化了,在卸载的时候需要把它给销毁,所以需要再引入一个onUnmounted

import BScroll from '@better-scroll/core'
import Slide from '@better-scroll/slide'
import {
    
    onMounted,onUnmounted,ref} from 'vue'
// 注册插件
BScroll.use(Slide)
// 传入一个ref对象
export default function useSlider(wrapperRef){
    
    
  // 为什么要定义在外部呢 因为最后要把slider return出去
  const slider=ref(null)
  onMounted(() => {
    
    
    // wrapperRef.value:对应wrapperRef容器的dom对象
    slider.value=new BScroll(wrapperRef.value, {
    
    
      click: true,
      scrollX: true,
      scrollY: false,
      momentum: false,
      bounce: false,
      probeType: 2,
      slide: true
    })
  }),
  onUnmounted(()=>{
    
    
    // 在外部定义slider的好处,如果在onMounted的时候定义,在这里就拿不到
    slider.value.destory()
  })
  return {
    
    
    slider
  }
}

在这里插入图片描述

虽然slider有返回值,但是实际上我们还没有使用它

还有个currentPageIndex这样一个逻辑

怎么去获取currentPageIndex呢

https://better-scroll.github.io/docs/zh-CN/plugins/slide.html#%E4%BA%8B%E4%BB%B6

在将要改变轮播图配置的时候,他会触发这样一个事件,在这个事件里面可以拿到page的值

在这里插入图片描述

在这里插入图片描述

有个问题,为什么useSlider不传入rootRef.value呢

因为直接传入dom(rootRef.value)的时候在onMounted的钩子里面拿到的dom会是undefined,因为他不是一个响应式的,但是rootRef是响应式的,就是说直接在setup里面拿rootRef.value应该是undefined,拿不到对应的dom,拿不到dom传到参数里面,然后在js文件里面 onMounted里面还是undefined,所以是不能用的,但是如果传入的是ref,它是一个响应式的数据,传入rootRef,然后在onMounted里面取value,这时候是可以取到dom的,因为是相应式的,这是一个要注意的点。

轮播图组件的使用

在这里插入图片描述

在这里插入图片描述

因为数据传入是异步的,而且slider是要求初始化的时候最少有一条数据,

所以使用一个v-if这样一个指令 就是说当sliders一开始没有获取数据的时候,slider组件不会渲染,渲染的时机是当sliders数据有了之后他才渲染,这样它渲染的时候肯定是有数据的,有数据了之后,初始化就能正常工作了。

现在的效果

在这里插入图片描述

这样轮播图组件就开发完了,有几个注意点,首先开发轮播图组件的时候,pc切换移动端的时候滚不动,因为pc,移动端监听的事件不一样,刷新一下就ok

还有在recommend下面使用了options api,有些人会误解就是说vue3提供了composition api,就无脑的用,其实是不推荐的,因为composition api有它的优势,也有它的不足, options api的优势在哪儿呢,在于整个结构是非常清晰的, 数据是数据,逻辑是逻辑,对逻辑比较小的组件,使用options api看起来是非常清晰的,因为他是一种描述性的结构,去描述组件,而compositon api是用场景通常是逻辑非常复杂的,或者逻辑可重用的。

那么slider为什么也用了composition api呢,是想把和第三方交互的逻辑都剥离出来,所以用了composition api 然后用了钩子函数,把它和better-scroll之间的逻辑从组件中剥离走,这也是是用composition api的场景,所以应该看使用的场景,适合用什么用什么

歌单列表实现&滚动组件的封装

再写个接口,拿到歌单列表

这里赋值给了albums

用浏览器的原生滚动:

overflow: scroll;

但是用原生滚动效果没有那么顺滑,还有滚动时不支持回弹的,其实回弹可以用css实现,但是兼容性并不好,一般来说在移动端,遇到滚动的场景推荐使用better-scroll ,better-scroll回弹这些功能都是有的,那么如何和列表滚动配合呢

可以封装成一个scroll组件,通过scroll组件来实现滚动效果,

base/scroll/scroll.vue

scroll支持插槽的方式,就是说滚动内容可以放到slot里面 然后外层可以和better-scroll做一些初始化联动,让内容部分可以实现滚动

也是通过compositon api和钩子函数实现的,

新建use-scroll.js

use-scroll.js:

import BScroll from '@better-scroll/core'
import {
    
     onMounted, ref, onUnmounted } from '[email protected]@vue'
export default function useScroll(wrapperRef) {
    
    
  const scroll = ref(null)

  onMounted(() => {
    
    
    scroll.value = new BScroll(wrapperRef.value)
  })

  onUnmounted(() => {
    
    
    scroll.value.destory()
  })
}

scroll.vue

<template>
  <div ref="rootRef">
    <slot></slot>
  </div>
</template>

<script>
  import useScroll from './use-scroll'
  import { ref } from 'vue'
  export default {
    name: 'Scroll',
    setup() {
      const rootRef = ref(null)
      useScroll()
      return {
        rootRef
      }
    }
  }
</script>

<style lang="scss" scoped>

</style>

增加一些配置项

https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html

可以看到,默认click为false ,我们想让他为true,怎么做呢?

在这里插入图片描述

然后怎么把props传递给这里呢

在这里插入图片描述

增加一个options参数

在这里插入图片描述

options里面都是一些扩展的属性

在这里插入图片描述

scroll组件对better scroll 而言,他是针对第一个子元素生效的,我们是需要上下两部分合在一起的,

在这里插入图片描述

来试一下能不能用

发现并没有生效

f12看一下

在这里插入图片描述

就是这个

在这里插入图片描述

这是scroll下面包裹的那一层

在这里插入图片描述

我们发现内容的高度和容器的高度是一样的,这样是不满足滚动条件的

滚动原理

我们可以通过修改样式去限制容器高度

在这里插入图片描述

发现仍然不能滚动

为什么呢 better-scroll判断它能不能滚动是在他new BScroll的时候,就是初始化的时候(见图),

在这里插入图片描述

这时候会计算容器高度,内容高度

在里面打个debugger试一下

在这里插入图片描述

可以看到这个时候其实容器高度,内容是没有的

在这里插入图片描述

那怎么办呢

实际上better-scroll提供了很好用的一个特性叫做observe-dom

https://better-scroll.github.io/docs/zh-CN/plugins/observe-dom.html#%E5%AE%89%E8%A3%85

在这里插入图片描述

现在滚动操作就很顺滑

图片懒加载

图片懒加载就是一种按需加载,就是说当图片出现在页面视图的时候再去加载,也就是当首屏的时候,不在视图中的图片都不去加载它,这样就可以节约带宽,是一种性能优化的手段,那图片懒加载在vue中如何使用呢

在vue2.0的时候使用的是vue-lazyload这样一个插件,来帮助我们实现图片懒加载的效果,到3.0的时候,因为内部指令的实现发生了变化,所以这个版本是不能直接用在3.0项目的,可以用这个

https://github.com/ustbhuangyi/vue3-lazy

它提供的api和vue-lazyload是一样的,对外暴露的接口是一样的。但是实现的功能还是比较少的,只实现了默认加载图片和加载失败图片这两种配置

assets目录放个全局加载的图片

安装

cnpm i vue3-lazy -S

安装完之后全局注册插件

这里配置项可以使用require语法,webpack可以识别(这里的require是webpack提供的语法),他可以通过相应的loader进行处理,最终处理有两种效果,一种是变成base64格式的,一种是变成外链,通过外链去加载。

main.js

import {
    
     createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import lazyPlugin from 'vue3-lazy' //new

import '@/assets/scss/index.scss'

createApp(App).use(store).use(router).use(lazyPlugin, {
    
    
  loading: require('@/assets/images/loading.png')  //new
}).mount('#app')

现在来修改模板

之前的recommend.vue

之前对于图片是修改他的src

现在是用的v-lazy

在这里插入图片描述

就是说这个插件它注册完之后,实际上是全局注册了一个v-lazy这样一个指令

这样真实的图片没加载出来的话就会显示require的那个图片

v-loading自定义指令的开发

当刷新页面的时候,如果异步数据请求的比较慢,会有一个白屏的效果,我们现在以往用户加载页面的时候可以有个等待预期,可以实现一个loading转圈图,

为了方便测试,响应时间设长一点

在这里插入图片描述

接下来就开发一个loading组件,loading组件本身是很简单的 一个gif文件配合一个文案

loading是可以作用到容器中间的,注意布局是怎么写的

<template>
  <div class="loading">
    <div class="loading-content">
      <img width="24" height="24" src="./loading.gif">
      <p class="desc">{
   
   {title}}</p>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'loading',
    data() {
      return {
        title: '正在载入...'
      }
    },
    methods: {
      setTitle(title) {
        this.title = title
      }
    }
  }
</script>

<style lang="scss" scoped>
  .loading {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
    .loading-content {
      text-align: center;
      .desc {
        line-height: 20px;
        font-size: 18px;
        color: white;
      }
    }
  }
</style>

可以用组件的方式进行使用,用vif控制,但是我们希望有一个更简单的方式来使用loading的效果,就不用每次用的时候都手动引入组件了

–可以用自定义指令 这是vue提供给我们的一个操作dom api的一个比较好的地方,在这里面dom比较好操作,因为它暴露了dom对象给我们,指令通常都会做一些dom操作的,

就像这样使用

在这里插入图片描述

知识 指令如何开发呢

官方文档:https://v3.cn.vuejs.org/guide/custom-directive.html#%E7%AE%80%E4%BB%8B

判断loading值,为true的话动态插入到作用的节点上,如何创建组件对应的dom呢 可以新建一个vue实例,创建一个新的app对象,然后动态挂载产生一个实例,在实例里面就可以拿到dom了

有一个要说明的

vue开发实际上是支持多实例的,并不是说只能在入口里里面创建一个唯一的app实例,在其他地方也可以创建一个新实例

在这里插入图片描述

现在这个实例挂载的地方实际上是动态创建的一个div,

在这里插入图片描述

但是这个div并没有挂载到body上,所以实质上还没有完成dom层的挂载,为什么不要挂载到dom上呢,因为它挂载的目的很明确,他要挂载到el上,组件对应的dom对象他要挂载到el上,接下来要判断binding.value 把挂载操作提取出来(函数append)

因为loading组件对应的dom对象在instance上,instance可以先把它保留起来,因为在mounted钩子函数中只创建一次,如果再其他钩子函数中也要访问它的话,可以用一个技巧–把它保留到el对象上,这样在append函数里面用el.instance.$element拿到对应的dom对象

el:指向作用的dom上
el.instance:loading组件的实例
el.instance.$el:loading组件对应的dom对象

也就是可以把dom对象挂载到el 也就是loading组件作用的dom上

把loading组件创建出来的对象挂载到loading指令作用的el上
el.appendChild(el.instance.$el)

v-loading=“loading” 这个loading变量也不是一成不变的啊,他可能会更新

当触发updated钩子函数,也就是说组件更新之后要做一个判断,如果loading的前后值不一样的话,要进行一些添加或者移除的操作了,比如说一开始loading为true ,然后loading变成false,那肯定要把它移除,如果一开始loading为false,后来变成true的话需要把它添加上

// 指令对象 把loading组件生成的dom动态
// 插入到指令作用的dom上

// 判断loading值,为true的话动态插入到作用的节点上,
// 如何创建组件对应的dom呢  可以新建一个vue实例,
// 创建一个新的app对象,然后动态挂载然后产生一个实例,
// 在实例里面就可以拿到dom了,
import {
    
     createApp } from 'vue'
import Loading from './loading'
const loadingDirective = {
    
    
  // 挂载的时候执行
  mounted(el, binding) {
    
    
    // app对象的根组件就是loading组件
    const app =createApp(Loading)
    // 通过app.mount方法拿到他的实例
    // 可以动态在里面创建一个div对象
    // vue开发实际上是支持多实例的,并不是说只能在入口里里面创建一个实例,
    const instance = app.mount(document.createElement('div'))
    el.instance = instance
    if(binding.value) {
    
    
      append(el)
    }

  },
  // 组件更新时执行
  updated(el, binding) {
    
    
    if(binding.value !== binding.oldValue){
    
    
      binding.value ? append(el) : remove(el)
    }
  }
}
function append(el) {
    
    
  // 执行挂载
  el.appendChild(el.instance.$el)
}
function remove(el){
    
    
  // 移除
  el.removeChild(el.instance.$el)
}

这样这个指令就简单的实现了,把它导出去

export default loadingDirective

实现之后就可以在外面引入这个指令了

在哪里引入呢,因为我们要全局使用,所以在main.js里引入它,做全局的注册

在这里插入图片描述

现在我们就在这个App下全局注册了,然后在这个App下的所有组件就可以使用v-loading了,

在这里插入图片描述

在这里插入图片描述

把热门歌单推荐那几个字去掉

在这里插入图片描述

优化

到现在看似loading指令是开发完了,但是还是存在一些问题,首先,是loading的定位问题,

在这里插入图片描述

最外层是绝对定位的,用top,left,transform来实现垂直水平居中,这是一个常见的技巧,但是它是要求外层容器非static的布局,也就是说,position要么是fixed啊,relative啊,absolute都可以,这边它恰好recommend这个class是fixed,所以是没有问题的,为了能够通用是可以做一些优化的。

就是说当我们去给loading这个组件append到作用到的元素上的时候,可以给el添加一个相对定位的样式,然后在remove的时候把它移除

dom api:getComputedStyle

https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle

position: https://www.w3school.com.cn/cssref/pr_class_position.asp

base.scss:

.g-relative {
    
    
  position: relative;
}
function append(el) {
    
    
  const style = getComputedStyle(el)
  if(['absolute', 'fixed', 'relative'].indexOf(style.position) == -1){
    
    
     // 添加relative样式
    addClass(el, relativeCls)
  }
  // 执行挂载
  el.appendChild(el.instance.$el)
}

src/assets/js/dom.js

在这里插入图片描述

在这里插入图片描述

来看一下效果,怎么看效果呢,recommend的position为fix 逻辑是没有进来的,可以debugger看一下

想看他有没有生效:可以把recommend的fixed注释掉

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个优化的作用就是希望v-loading的作用场景是不依赖于作用元素本身的样式的,

还有我们的自定义指令支持传入title ,那如何传入title呢

官方示例

在这里插入图片描述

在这里插入图片描述

使用

在这里插入图片描述

3歌手列表

歌手列表数据获取

https://neteasecloudmusicapi.vercel.app/#/?id=%e6%ad%8c%e6%89%8b%e5%88%86%e7%b1%bb%e5%88%97%e8%a1%a8

http://47.103.29.206:3000/artist/list?type=-1&area=7&initial=x

在这里插入图片描述

这里的判断歌手首字母是在A–Z哪个字母 用到了一个拼音库 pinyin 需要把依赖保存到devDependencies 因为它是后端的相关的逻辑,他不会运行在前端,所以不要放在dependence里面,

cnpm i pinyin --save-dev

拼音库返回的数据

在这里插入图片描述

function registerSingerList(app){
    
    
  app.get('/api/singer', (req,res) =>{
    
    
    const HOT_NAME = '热'
    const url=`${
      
      baseUrl}/top/artists`
    const params=req.query
    params.limit&&(params.limit=Number(params.limit))
    params.offset&&Number(params.offset)
    console.log(params);
    axios.get(url, {
    
    params}).then((result)=>{
    
    
      const artists=result.data.artists
      const artistList = artists.map((item)=>{
    
    
        return {
    
    
          id:item.id,
          name:item.name,
          picUrl:item.picUrl
        }
      })
      const singerMap={
    
    
        hot:{
    
    
          title:HOT_NAME,
          list:artistList.slice(0,6)
        }
      }
      artistList.forEach((item)=>{
    
    
        // 把歌手名转换成拼音  找出拼音的首字母就知道歌手属于哪个字母了
        const p=pinyin(item.name)
        if(!p||!p.length){
    
    
          return 
        }
        // console.log(p);
        const key=p[0][0].slice(0,1).toUpperCase()
        if(key){
    
    
          // 没有当前字母
          if(!singerMap[key]){
    
    
            singerMap[key]={
    
    
              title:key,
              list:[]
            }
          }
        }
        // push相应歌手到对应key的list下面
        singerMap[key].list.push(item)
      })
      // console.log(result.data);
      // console.log(artistList)
      console.log(singerMap);
      res.json({
    
    
        result:artists,
        code:ERR_OK
      })
    }).catch((err)=>{
    
    
      console.log(err);
    })
  })
}

现在 singerMap:

在这里插入图片描述

需要让他有序

      const hot=[]
      const letter=[]
      for(const key in singerMap){
    
    
        const item= singerMap[key]
        if(item.title.match(/[a-zA-Z]/)){
    
    
          letter.push(item)
        }else if(item.title==HOT_NAME){
    
    
          hot.push(item)
        }
      }
      // 按字母顺序排序
      letter.sort((a,b)=>{
    
    
        return a.title.charCodeAt(0)-b.title.charCodeAt(0)
      })
      console.log(letter);
      res.json({
    
    
        result:hot.concat(letter),
        code:ERR_OK
      })

这样后端路由就定义好了

在这里插入图片描述

现在来编写前端请求的相关逻辑

新建 service/singer

import {
    
    get} from './base'
export function getSingerList(){
    
    
  return get('/api/singer')
}

到singer.vue里面

created钩子函数里面去获取数据

生命周期:

请添加图片描述

在这里插入图片描述

result.data.result数据:

在这里插入图片描述

IndexList组件基础滚动功能实现

在这里插入图片描述

还有点击导航字母会自动跳转到对应的组

这个有点像手机的通讯录的效果

这里把滚动的效果包括右侧导航,和fix title固定标题的效果封装到IndexList基础组件,

因为是个基础组件,所以把它放到base目录下,

base/index-list/index-list.vue

<template>
  <Scroll class="index-list">
    <div>
      <div class="group-list" v-for="group in list" :key="group.name">
        <div class="title">{
   
   {group.title}}</div>
        <div class="listItem" v-for="item in group.list" :key="item.name" >
          <img v-lazy="item.picUrl" alt="" srcset="">
          <div class="name">{
   
   {item.name}}</div>
        </div>
      </div>
    </div>
  </Scroll>
</template>

<script>
import Scroll from "@/components/base/scroll/scroll";
export default {
  components: { Scroll },
  props: {
    list: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  mounted() {
    console.log("index-list", this.list);
  }
};
</script>

<style lang="scss" scoped>
.index-list{
  width: 100%;
  height: 100%;
  overflow: hidden;
  .group-list{
    .title{
      height: 30px;
      line-height: 30px;
      font-size: 12px;
      background-color: rgba(145, 137, 137, 0.938);
      color:rgb(192, 189, 189);
      padding-left: 7px;
    }
    .listItem{
      height: 80px;
      display: flex;
      align-items: center;
      // justify-content: center;
      .name{
        color:rgb(192, 189, 189);
      }
      img{
        width: 50px;
        height: 50px;
        border-radius: 50%;
        margin: 0 15px;
      }
    }
  }
}
</style>

歌手列表固定标题实现 上

思路:固定显示的话需要一个层固定在那个位置,需要求解的是层内渲染的内容,就是说应该渲染的标记应该是什么(热,A,B…)

我们需要求解每个组的区间,区间的高度是可以获取到的,另外我们滚动的时候是可以知道y值的,那现在就是要知道y值落在哪个区间,那就表示当前的组正在展示

这个层是应该要绝对定位的,

在这里插入图片描述在这里插入图片描述

这是老师写的样式,

有个疑问,为什么不在fixed里写高度,而在内层写高度呢

扩展 注意是写的absolute,而不是fixed, 如果用fixed,top 0 会在页面的顶部 还有absolute不能在滚动的内容区,他会随着手往上滑而往上滑,fixed就不会

现在就要求fixedTitle 可以把逻辑都封装到一个函数里面,

新建use-fixed.js

写一些大致框架

在这里插入图片描述

在这里插入图片描述

拿到groupRef以后就可以拿到他的子元素,group-list

这里老师用的setup 我听不太懂,屡不过来,就不用compositon api了还是用vue2的知识研究研究怎么写吧


需要拿到子元素,然后得到每个子元素的高度

知识 element的属性

https://developer.mozilla.org/zh-CN/docs/Web/API/Element/children

https://developer.mozilla.org/zh-CN/docs/Web/API/Element/clientHeight

在这里插入图片描述

在这里插入图片描述

可以看到height里面并没有数据

但是加个定时器就出来了

在这里插入图片描述

看vue的官方文档

在这里插入图片描述

注意需要观测数据变化 需要用watch

还有一个数据变化了以后dom是没有被渲染出来的,

dom发生变化是在nextTick发生以后,

没有加nextTick:

  watch: {
    
    
    list: function () {
    
    
      const children = this.$refs.groupRef.children;
      console.log(children);
      const heightArr = [];
      let height = 0;
      for (let i = 0; i < children.length; i++) {
    
    
        height += children[i].clientHeight;
        // console.log(height);
        heightArr.push(height);
      }
      console.log(heightArr);
    },
  },

在这里插入图片描述

加nextTick以后

  watch: {
    
    
    list: async function () {
    
    
      await this.$nextTick();
      const children = this.$refs.groupRef.children;
      console.log(children);
      const heightArr = [];
      let height = 0;
      for (let i = 0; i < children.length; i++) {
    
    
        height += children[i].clientHeight;
        // console.log(height);
        heightArr.push(height);
      }
      console.log(heightArr);
    },
  },

在这里插入图片描述

或者

item高度

watch: {
    
    
    list: function () {
    
    
      this.$nextTick(() => {
    
    
        const children = this.$refs.groupRef.children;
        console.log(children);
        const heightArr = [];
        let height = 0;
        for (let i = 0; i < children.length; i++) {
    
    
          height += children[i].clientHeight;
          // console.log(height);
          heightArr.push(height);
        }
        console.log(heightArr);
      });
    },
  },

滚动高度

借助scroll组件,scroll组件是基于better-scroll实现的,better-scroll内部是怎么拿到滚动值的呢

https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-options.html#probetype

就先推荐页,我们不需要知道他的位置,probeType配成0,这样性能就好一点,但是到歌手页面,我们希望滚动的时候拿到那个值,所以配probeType为3,可以拿到scroll事件派发的位置,然后再去计算就ok了

所以这里需要对scroll组件进行修改

在这里插入图片描述

https://better-scroll.github.io/docs/zh-CN/guide/base-scroll-api.html#%E6%96%B9%E6%B3%95

在这里插入图片描述

use-scroll.js

在这里插入图片描述

知识 问题来了 外部如何拿到pos呢 —事件派发

https://v3.cn.vuejs.org/guide/migration/emits-option.html#_2-x-%E7%9A%84%E8%A1%8C%E4%B8%BA

vue3用法

在这里插入图片描述

之后在use-scroll.js里:

在这里插入图片描述

接下来配置probeType>0

在这里插入图片描述

滑动一下

在这里插入图片描述

在这里插入图片描述

我本来是想能不能监听一个全局的scrollY 如果把scrollY放到data里面是不是性能不太好

知识 计算属性和监听器

https://cn.vuejs.org/v2/guide/computed.html#%E4%BE%A6%E5%90%AC%E5%99%A8

后来发现不能监听全局的变量,所以还是放到data里面

vue3 watch的使用https://v3.cn.vuejs.org/api/computed-watch-api.html#watch

这里还是用的vue2

在这里插入图片描述

在这里插入图片描述

这样在index-list里面就拿到它了

歌手列表固定标题实现 中

有个问题,在高度的数组里,应该给第一位置为0

因为求heightTop 是拿的heightArr[i]

求heightBottom是拿的heightArr[i+1]

很明显如果数组是这样的话

在这里插入图片描述

第一个元素的top高度heightArr[0]就是510 这是错误的, bottom才是510 所以要给第一位设为0

在这里插入图片描述

这样第一位就是第一个元素的top 第二位就是第一个元素的bottom&第二位元素的bottom

heightArr,currentIndex设为全局变量。

    // 监听滚动
    scrollY(val) {
    
    
      console.log(val);
      // 判断滑动的y值哪个区间内
      for (let i = 0; i < heightArr.length - 1; i++) {
    
    
        const heightTop = heightArr[i];
        const heightBottom = heightArr[i + 1];
        // 落在这个区间内
        if (val >= heightTop && val <= heightBottom) {
    
    
          // 当前展示组的索引
          currentIndex = i;
        }
      }
    },
  },

根据索引来判断fixedTitle应该显示什么

知识 计算属性 扩展 计算属性与watch区别

疑问 但是老师为什么要用到计算属性呢

在这里插入图片描述

试了一下这样写也可以

在这里插入图片描述

我写的计算属性报错了

在这里插入图片描述

应该是一开始list里面还没有内容吧

修改:

在这里插入图片描述

问题优化

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

歌手列表固定标题实现 下

还有一个bug

在这里插入图片描述

上面的那个应该随着下面的往上移

思路:拿到当前底区间的底部, 减去当前滚动的y值

就是这段距离的值 用distance存下来

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

歌手快速导航入口1

特点:数据来源是当前组的title

滚动过程中始终固定在那个位置。

滚动过程中,高亮显示。

暂缓

4 歌手详情页

歌手详情页

https://neteasecloudmusicapi.vercel.app/#/?id=%e6%ad%8c%e6%89%8b%e7%83%ad%e9%97%a850%e9%a6%96%e6%ad%8c%e6%9b%b2

https://neteasecloudmusicapi.vercel.app/#/?id=%e8%8e%b7%e5%8f%96%e9%9f%b3%e4%b9%90-url

在这里插入图片描述

来分析一下返回的数据

因为有的音乐是要vip的 播放时间30秒

找一首vip歌曲 戴佩妮的怎样

在这里插入图片描述

找一首不用vip可以听的

在这里插入图片描述

猜测fee=1的为会员歌曲,fee=0的不用会员 然后freeTrialInfo为试听的段落

不对 fee不是这个意思

又找了一个vip的歌曲,发现它的size也是481115

然后网易云有付费歌曲吗?我好像没见过

周杰伦:6452 薛之谦:5781 孙燕姿:9272

歌曲的数据结构:

id(歌曲id),name(歌曲名),singer,url(暂无),pic,album(专辑名)

歌手可能是有很多人合唱的,所以可以新增一个函数处理一下

function mergeSinger(singer) {
    
    
  if (singer.length == 0 || !singer) {
    
    
    return
  }
  const singerMerge = []
  singer.forEach((item) => {
    
    
    singerMerge.push(item.name)
  })
  return singerMerge.join('/')
}
function registerSingerDetail(app) {
    
    
  app.get('/api/getSingerDetail', (req, res) => {
    
    
    const params = req.query
    const url = `${
      
      baseUrl}/artist/top/song`
    axios.get(url, {
    
     params }).then((result) => {
    
    
      const  data = result.data.songs
      const songList=[]
      data.forEach((item) => {
    
    
        const singer = mergeSinger(item.ar)
        const id = item.id
        const name = item.name
        const url = ''
        const pic = item.al.picUrl
        const album = item.al.name
        const alId = item.al.id
        const songItem = {
    
    
          singer,id,name,url,pic,album,alId
        }
        songList.push(songItem)
      })
      res.json({
    
    
        result: songList
      })
    })
  })
}

service/singer.js

export function getSinderDetail(singer) {
    
    
  return get('/api/getSingerDetail', {
    
     id: singer.id })
}

在哪里发请求呢

新建views/singer-detail.vue组件

为什么要在views下面创建呢,因为它是个二级路由,

歌手详情是接收一个singer的,传入不同的singer就能渲染不同歌手的数据了

疑问,为什么要把整个singer传进去呢 只传一个id不行吗 我知道了 进入的这个歌手详情是要有歌手名和歌手图片的,需要singer里面的图片和歌手名

因为是个二级路由,所以首先要去配置这个路由,

router/index.js

  {
    
    
    path: '/singer',
    component: Singer,
    children:[
      {
    
    
        path:':/id',
        components:SingerDetail
      }
    ]
  },

singer.vue里面写个routerview来承载二级路由,用来渲染singer-detail

这个组件要传入参数,传入singer

怎么做呢

在这里插入图片描述

知识 子组件->父组件 应该在这个组件里做点击事件,然后传到父组件(就是这个singer.vue)

在这里插入图片描述

    onItemClick(item) {
    
    
      console.log(item);
        // 派发一个自定义事件select
      this.$emit('select', item)
    },

监听select事件

在这里插入图片描述

  methods: {
    
    
    selectSinger(item) {
    
    
      console.log(item);
    },
  },

在这里插入图片描述

这样就实现了数据传递 类似小程序的triggerEvent?

当然,进行到这一步还是不行的,因为要进行路由跳转,

在这里插入图片描述

记得把详情页设为全屏

  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
  background: rgb(68, 66, 66);

在这里插入图片描述

歌手详情页获取url

批量获取url

https://neteasecloudmusicapi.vercel.app/#/?id=%e8%8e%b7%e5%8f%96%e9%9f%b3%e4%b9%90-url

批量传入歌曲的id即可

知识点 forEach和map

这里写接口要用post了,因为get请求携带不了那么多参数

一个坑

就到这一步!!我尝试用post发请求的时候,死活拿不到请求的数据

因为app是express的一个实例,在vue.config.js里加入

app.use(bodyParser.urlencoded({extended:true}));
app.use(bodyParser.json());

function registerSongUrl(app) {
    
    
  app.post('/api/getSongUrl', async (req, res) => {
    
    
    // console.log(req.body);
    let mid = req.body.mid
    // console.log("mid", mid);
    let result = await process(mid)
    // console.log("result", result);
    res.json({
    
    
      result: result,
      code:CODE_OK
    })

  })
  // 处理返回的id
  function process(item) {
    
    

    const url = `${
      
      baseUrl}/song/url`

    const urlMap = {
    
    }
    let id = []
    item.forEach((item) => {
    
    
      // console.log("item", item)
      id.push(item.id)
    })
    id = id.join(',')
    console.log("id", id);
    return axios.post(url, {
    
     id }).then((res) => {
    
    
      // console.log("res", res.data.data);
      const data = res.data.data
      data.forEach((info) => {
    
    
        //  以歌曲的id为key,存储歌曲URL
        // 这个urlMap是给前端用的,因为知道了每个id对应什么歌曲,那就可以遍历
        // 之前的歌曲列表,然后拿到每个歌曲的id,然后就能映射它的url是什么,
        // 就可以给他补充url信息了
        urlMap[info.id]=(!info.url||info.url=='')?`https://music.163.com/song/media/outer/url?${
      
      info.id}=${
      
      info.id}.mp3`:info.url
      })
      // console.log(urlMap);
      return urlMap
    })
  }
}

但是发现总有几首歌是拿不到的

在这里插入图片描述

解决方法:

https://music.163.com/song/media/outer/url?id=id.mp3

service/song.js

import {
    
     post } from './base'
export function getSongUrl(songs) {
    
    
  return post('/api/getSongUrl', songs).then((res) => {
    
    
    // console.log("test", res);
    const map = res
    return songs.mid.map((song) => {
    
    
      const id = JSON.stringify(song.id)
      song.url = map[id]
      // console.log(song.url);
      return song
    })

  })
}

vue调用

  async created() {
    
    
    let result = await getSingerDetail(this.singer);
    result = result.data.result;

    const urlData = await getSongUrl({
    
     mid: result, aa: "test" })
    console.log(urlData);
  },
};

数据

在这里插入图片描述

哭死,我才发现不能用post,因为第二次发起请求传入不同的id 请求回来的数据还是和第一个一样

MusicList组件

写完大概是这样的,

在这里插入图片描述

写之前思考一下,

其实这个布局方式在这个项目里面很多是类似的

比如说热门歌单推荐

在这里插入图片描述

还有排行榜详情页

在这里插入图片描述

所以就可以统一封装成一个组件—music-list.vue

这个下面的音乐的列表也可以抽象成一个组件 song-list

接收的参数:音乐列表,标题,背景图

有个疑问 把传参传入的singer中的title和pic放到data里面,直接这样写不可以吗

在这里插入图片描述

为什么要这样写呢

在这里插入图片描述

知识背景图

在这里插入图片描述

注意background-size的使用

在这里插入图片描述

图片被遮住了

背景去掉看一下

在这里插入图片描述

因为没有设scroll的top值,所以就成这样了,为什么现在不设它的top值呢

因为图片的高度没有去固定它,

知识点

背景图的宽高比css设置

  .bg-image {
    
    
    width: 100%;
    // height: 250px;
    background-size: cover;
    height: 0;
    padding-top: 50%;
  }

那如何去计算图片的高度,然后给list设top值呢

优化交互-动态设置列表top值

那我们要先动态获得图片的高度,

在这里插入图片描述

在这里插入图片描述

有了top值了以后就可以滚动了

知识 回退方法:this.$router.back()

现在想在music-list.vue里面加入加载功能,但是呢,数据不是在这个组件里面请求的,需要去它的父组件判断loading是true还是false,然后子组件接收这个参数

singer-detail.vue

在这里插入图片描述

music-item:

在这里插入图片描述

在这里插入图片描述

这个交互效果和想要的还不一样

我们想让列表有个向上推的效果并且推到最上面的话可以让标题有个背景色, 往下拉的时候有个图片变大的效果 还有个拉下去回弹的效果

交互优化2

来分析一下,往上推的时候,overflow:hidden是要去掉的,

在这里插入图片描述

这里标题也要修改层级

先解决往上推的问题

在这里插入图片描述

这样就ok

在这里可以改变一下层级

在这里插入图片描述

那么如何判断有没有滚动到那个位置呢

在这里插入图片描述

可以用scroll组件实时派发scroll事件,来拿到滚动位置 probe-type

在这里插入图片描述

在这里插入图片描述

可以定义一个常量,

在这里插入图片描述

在定义一个它可以滚动的最大的高度

在这里插入图片描述

这个最大滚动高度就是图片的高度-RESERVED_HRIGHT

在这里插入图片描述

这样就可以动态修改style了,

知识 动态修改style

在这里插入图片描述

但是

请添加图片描述

我们知道他是有10:7的宽高比的,

这时候我们把他的高改一下不就行了吗

所以这个高是要动态改的

把这俩拿出来

在这里插入图片描述

在这里插入图片描述

但是这里有问题

在这里插入图片描述

在这里插入图片描述

知识 display:none 与visibility:hidden

https://www.cnblogs.com/zenan/p/8257094.html

扩展qrcode

知识 有个问题 iphone手机z-index不生效,

在这里插入图片描述

接下来实现向下拉图片放大效果

缩放是用的scale

知识 原来transform可以这样写

在这里插入图片描述

往上推模糊效果

未完

backdrop-filter blur

在这里插入图片描述

疑问 为什么要设个最大值呢

这里有个bug

在这里插入图片描述

为什么到标题固定的时候这个blur虽然有值,但是却没有啥效果,和blur=0一样,我检查了一下,没有层级的问题啊

在这里插入图片描述

有个值得注意的,写计算属性的时候,有时候会获取一个临时变量,然后后面用的是临时变量,这是一个好的习惯,当获取响应数据次数大于1次的时候,一定要用一个临时变量缓存,这也是一个vue性能优化的一个常用的技巧,实际上,每次执行this.xxx的时候,都有一个依赖收集的过程,

知识点:依赖收集?

当计算属性频繁执行的时候,比如说如果频繁调用scrollY,那这个依赖收集过程就会发生多次,显然没有必要,只要发生一次就可以了

支持详情页刷新

扩展

https://img2018.cnblogs.com/blog/857591/201903/857591-20190329180958410-1892531373.png

先来分析一下为什么之前刷新页面会报错,因为页面渲染的数据是依赖于传递过来的props singer这样一个数据

渲染二级路由的时候,把对象传递过来,所以说页面可以正常渲染,但是刷新了之后,内存之中的数据都丢失了,拿不到歌手对象,拿不到歌手对象,自然就不能正常渲染了。那刷新的时候如何能依然能获取这个对象呢,可以借助浏览器的本地存储能力,本地存储api有两个,一个sessionStorage,一个localStorage

知识

区别 :localStorage是持久存储,关掉页面仍然是在的,下次打开页面它还是会有的,

这里用localStorage,所以在点击某一个歌手之后,跳到新路由的同时,把singer对象存储到本地缓存sessionStorage中,刷新页面的时候就判断,如果是通过props传入的singer那就用这个singer,如果没有传的话就从本地缓存中拿,比如说存储的singer 那就拿到singer的id,和当前的路由参数(id)进行对比,如果能匹配上,那就就代表是在这个页面进行刷新的,这样就能拿到对应的歌手了,拿到对应的歌手之后就可以渲染页面了,这就是我们的思路,

老师用的他自己的第三方库,我觉得我还是得自己用原生的写一下

https://developer.mozilla.org/zh-CN/docs/Web/API/Storage

good-storage它是可以操作sessionStorage和localStorage的,为什么要封装呢,因为存储到本地的是字符串,但是通过库的话可以存储对象的形式,

代码不多,以后要去学习一下

https://www.npmjs.com/package/good-storage

https://github.com/ustbhuangyi/storage/


存储的话是有个key的,可以用个常量来表示,

src/assets/js/constant.js

constant.js里面存储一些项目里面共享的常量

为什么要在constant里面去定义呢,因为一个地方要去设置值,一个地方去获取值,是在不同的组件中,但是常量定义可以放在一个公共的地方

在这里插入图片描述

在这里插入图片描述

我们要把它变成字符串

JSON.stringify

在这里插入图片描述

singer-detail.vue

在这里插入图片描述

要注意 类型的比较

        console.log(typeof cachedSinger.id); // number
        console.log(typeof this.$route.params.id); // string

在这里插入图片描述

这样就实现刷新的功能了

但是如果随便写一个参数

在这里插入图片描述

在这里插入图片描述

来到getSingerDetail

在这里插入图片描述

这里参数传递为null是不行的,

要避免这个情况,需要加个保护

   if(this.computedSinger) {
    
    
      return 
    }

那也得让页面正常啊 那怎么办呢 知识 可以让它退回到一级路由

    if (!this.computedSinger) {
    
    
      const path = this.$route.matched[0].path;
      console.log("path", path);
      this.$router.push({
    
    
        path,
      });
    }

歌手详情页路由过渡效果实现

知识 vuejs内部提供一个transition组件,

https://cn.vuejs.org/v2/api/#transition

https://v3.cn.vuejs.org/guide/transitions-enterleave.html#%E8%BF%87%E6%B8%A1class

https://next.router.vuejs.org/zh/guide/advanced/transitions.html

可以很好的帮助我们实现这个效果

它实现原理其实就是使用transition组件止之后,他会在合适时机给dom元素添加一些class,在class样式名内部,可以借助比如css3的transition,transform啊来实现动画效果,其实它实现动画效果还是要自己写的,来决定最后以什么样的方式做动画,现在来看看是怎么使用的,

在全局,定义一个slide这样一个过渡动画,为什么呢,因为过渡效果在多处都要使用,所以可以定义一个公共样式,transition组件可以添加或删除这些样式名,

base.scss:

在这里插入图片描述

translate3d (100%,0,0)横向坐标偏移自身的100% 这样就可以把一开始的位置挪到右侧

transtion:all 0.3s 设置时间

在这里插入图片描述

这个name其实是和class对应起来的,改成slide

router-view改造

在这里插入图片描述

在这里插入图片描述

看了一下vue下面使用的例子,我觉得很炫酷,以后自己写项目的时候可以用

歌手详情页边界情况处理

知识 边界处理

当获取歌手详情的时候,也就是获取歌曲列表的时候是可能获取不到的

发送异步请求去请求某个数据,数据有的时候是要去渲染它,数据没有的时候也要处理这个情况,给用户一个反馈

把请求参数改一下复现一下情况

在这里插入图片描述

这就是前端经常做的一些事情,就是做一些边界的处理,那怎么去做呢,编写一个no-result组件

这个组件长得跟loading组件非常像,他有个图案,下面还有个文案,“抱歉,没有结果” 最好不要赋值粘贴代码而是可以把指令directive.js抽象一下 让它更加通用

在这里插入图片描述

在这里插入图片描述

loading/directive.js

import Loading from './loading'
import createLoadingLikeDirective from '@/assets/js/create-loading-like-directive'
const loadingDirective = createLoadingLikeDirective(Loading)
export default loadingDirective

能看到是可以正确运行的

接着创建一个no-result的指令

import NoResult from './no-result'
import createLoadingLikeDirective from '@/assets/js/create-loading-like-directive.js'
const noResultDirective = createLoadingLikeDirective(NoResult)
export default noResultDirective

还要去注册他

在这里插入图片描述

使用

music-list.vue

在这里插入图片描述

在这里插入图片描述

这里我后端的代码应该是出问题了,因为没有返回东西

在这里插入图片描述

在这里插入图片描述

所以v-loading一直不会消失,v-loading不消失这个no-rseult指令就不会出来,所以应该返回一个空数组的,

这个先不改了 待做吧

这一P老师调试了一下执行的过程,以后要再看看

歌手详情页列表点击以及vuex的使用

https://next.vuex.vuejs.org/zh/guide/mutations.html

这个播放器可以展开也可以收缩,我们在任何的路由视图中都可以访问到播放器,所以说这个播放器组件是个全局组件

那么播放器里的数据也是把它放到全局来管理,vuex :vue官方全局管理的工具,用它就可以管理播放器相关的数据,比如说播放器的播放列表,播放器的状态,播放模式,播放第几首歌,以及播放器的展开收缩的状态等等,这些都可以用vuex来管理

知识,以后要跟着官方文档把它的例子过一遍

他有几个核心概念 :

state:全局的数据仓库 里面存储一些最基础的数据

它上层有个getters 可以理解为state的计算属性

那可以通过state和getters得到数据,那怎么如何修改数据呢

可以commit一个mutation 他是唯一可以改变数据的方式 所有对state里面的修改都可以提交一个mutations,为了保证数据可追踪,他就要求必须提交一个mutation

action可以理解为对mutation的一个封装,允许我们做一些额外的操作,最终改数据还是通过提交mutation ,比如说可以在action里面做一些异步请求这这些逻辑,

可以用代码的方式来了解一下用法

store/index.js

import {
    
     createStore } from 'vuex'

export default createStore({
    
    
  state: {
    
    
  },
  mutations: {
    
    
  },
  actions: {
    
    
  },
  modules: {
    
    
  }
})

它暴露出store这个实例,其实store已经注入到实例里面了

在这里插入图片描述

接下来填充数据

因为我们这个算中型的项目,所以可以拆分到不同的文件里面

state.js

import {
    
     PLAY_MODE } from "../assets/js/constant"
const state={
    
    
  // 播放列表原始数据
  sequenceList:[],
  // 因为有播放模式 这个是实际播放列表
  playlist:[],
  // 是否正在播放
  playling:false,
  // 有三种模式 放到常量里面
  playMode: PLAY_MODE.sequence,
  // 当前歌曲索引
  currentIndex: 0,
  // 全屏的还是收缩的
  fullScreen: false
}

在这里插入图片描述

当我们点击列表的时候,相当于是设置一个playlist 就是说选择了列表里的一个播放,可以定义一个actions

听不懂 我还是先去学学vuex吧

听了一遍coderwhy的

我先都写一个文件里 后面再抽离出去

插播:看一下老师这个写法

在这里插入图片描述

本来以我的认知的话这种函数是要用花括号括起来引入的

在这里插入图片描述

但是老师那种写法就不用一个一个引入

在这里插入图片描述

知识logger插件,疑问 不能用插件devtools吗

https://vuex.vuejs.org/zh/guide/plugins.html#%E5%86%85%E7%BD%AE-logger-%E6%8F%92%E4%BB%B6

strict模式

https://vuex.vuejs.org/zh/api/#strict

在这里插入图片描述

接下来来应用vuex

点击的时候提交一个action selectPlay

给列表添加点击事件

song-list.vue

在这里插入图片描述

vfor一个添加到ul上一个添加到li上 ,他俩有啥区别吗

派发事件

在这里插入图片描述

music-list.vue

在这里插入图片描述

这里就可以派发actions了

语法糖 mapActions

[https://vuex.vuejs.org/zh/guide/actions.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%AD%E5%88%86%E5%8F%91-action](https://vuex.vuejs.org/zh/guide/actions.html#在组件中分发-action

我不用语法糖了

在这里插入图片描述

在这里插入图片描述

列表随机播放

知识Knuth-Durstenfeld Shuffle** 算法 http://rosettacode.org/wiki/Knuth_shuffle

和顺序播放的区别就是它的播放列表是被随机打乱的

用一个随机算法对原有列表进行随机打乱

然后播放打乱后的列表

并且每次点击随机播放按钮都能再次打乱,

知识 洗牌算法 比math.random更随机

取随机值和数组下标依次交换


对随机播放 索引值是没有意义的 所以改成0了

这个list是需要洗牌的

在这里插入图片描述

洗牌函数:src/assets/js/util.js

老师这个真的对吗我看网站上是从最后一个遍历到第一个的

在这里插入图片描述

在这里插入图片描述

知识 slice

在这里插入图片描述

有个地方写错了 应该是i-- 浏览器差点卡死。。。哭

在这里插入图片描述

引入

在这里插入图片描述

添加事件

    randomPlay() {
    
    
      this.$store.dispatch('randomPlay', this.songs)
    }

在这里插入图片描述

5 播放器核心

5.1播放器基础样式及播放功能

思路:

因为很多组件都要用这个播放器,所以把它放到一个公共组件里面,点击歌曲列表调出来它,那怎么知道是点的哪个呢,因为播放器需要用到这些数据,用到了vuex,前面我们写了,点击列表以后,vuex里面有个getters,可以根据索引计算返回出对应歌曲信息,我们点击以后,就把state里面的fullScreen给设为true,这时候,播放器组件就会显示出来(vshow),和fullScreen一样,这个currentSong也是会被监听的,里面的数据会被渲染出来,那currentSong里面的url如何给audio去播放呢,可以用ref取到audio,然后给这个element 添加src属性,在什么时机呢,可以在currentSong变化的时候,用到了watch钩子函数,之后在调用play方法让他播放

当点击随机播放按钮 ,点击歌曲列表,点击迷你播放器都能唤起播放器,播放器以全屏状态展开

这一节我们先能让歌曲播放起来

新建@/components/player/player.vue

因为是全屏播放 fix布局

因为播放器功能比较复杂,所以用的vue3的compositon api来开发,把不同的逻辑拆到不同的文件中,这样把相同的逻辑放到一个文件中管理,这样代码看起来会比较清晰一点

设position:absolute是会脱离文档流吗

        opacity: 0.6;
        filter: blur(20px);

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

学到一个新词 subtitle

这里样式有点毛病

在这里插入图片描述

在这里插入图片描述

title的margin跑到父元素里去了

https://img-blog.csdnimg.cn/20200724092025488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW96aGF6aGF6aGF6aGE=,size_16,color_FFFFFF,t_70

样式效果

在这里插入图片描述

图片代码写错了 应该是:src

在这里插入图片描述

来填充数据,

在这里插入图片描述

在这里插入图片描述

来测一下,那这个组件应该放那儿呢

其实这是一个全局的组件,在任何的一个路由下都可以访问到这个player组件 所以可以放到app.vue

在这里插入图片描述

bug

还没点呢就报错

在这里插入图片描述

就是说currentSong是undefined

知识

为什么会报这个错,因为player组件实际上一开始就渲染了,并且是通过v show来控制的

vshow其实组件是会渲染的,只是说fullScreen为false的时候display为none

vif

在这里插入图片描述

根本就还没有渲染,所以不会报错

vshow就报错了,所以vue就阻止执行后续流程 他就无法将节点append到body下

所以就没有办法去看到player组件(chrome调试工具下)

那这个currentSong为什么是undefined呢

去看看他定义的地方

在这里插入图片描述

playList开始是个空数组,currentIndex是0 也就是取数组的第一项,这样的话就是一个undefined值

在这里插入图片描述

undefined值不能调用任何的属性 要不然就会报前面的那个错误

知道了出错原因,怎么去解决呢 其实可以给他一个默认的值,当取不到歌曲的时候,不要给他undefined,给他一个空对象

在这里插入图片描述
在这里插入图片描述

空对象.pic是不会报错的,它顶多就是不渲染

在这里插入图片描述

要注意,这只是一级,如果嵌套的比较深再来一层

还有个解决方案 套个template 用vif判断currentSong为空的话就不渲染

在这里插入图片描述

vif指令在编译阶段会生成一个三元运算符,不满足条件的话就不会去执行的,也就不会生成vnode,也就不会把vnode patch生成真实dom 整个逻辑都不执行

两种方法,如果想让模板足够简单的话就在数据层面控制,根据实际情况去使用

在这里插入图片描述

vue2改造

关于audio

https://www.runoob.com/jsref/dom-obj-audio.html

https://www.runoob.com/tags/ref-av-dom.html

在这里插入图片描述

5.2 播放与暂停逻辑

学到一个新词 toggle

思路:因为播放暂停按钮是不确定是哪种的,所以添加一个动态class,名字叫playIcon,他是根据state里面的playing的状态,如何获取:计算属性playing赋为state里面playing,然后playingIcon也可以通过计算属性 判断playing的值如果是true,就显示播放的图标,false显示暂停的图标。绑定播放事件,之后翻转播放状态,放到state里 那这样的话,playing变化了以后,computed监听到playing变化了,icon也跟着变了,接下来是音乐播放的播放暂停状态也需要跟着改变,可以监听playing,拿到元素之后,为true,audioEl.play(),为false audioEl.pause()

插播

在这里插入图片描述

声明式语法和命令式语法 声明式语法和命令式语法

注意这个audio可以放到全局

在这里插入图片描述

其实可以这样写 还可以改造一下,用三元运算符

在这里插入图片描述

音乐自己关闭的情况 不是用户交互触发它暂停,就需要同步他的状态,有一种情况,如果把笔记本合上,就处于一个待机状态,这时候会让audio暂停,但是这时候并没有修改他的数据,可能就会让数据乱掉了,(音乐已经暂停了,但是playing依然为true)怎么做呢,当audio暂停的时候,它是有一个事件的,pause事件,可以设一个pause方法,在pause方法里去修改数据 修改state里的playing为false就ok

5.3 上一曲下一曲

有个问题

在这里插入图片描述

我们的点击事件的范围大了, 这是因为布局的时候的使用的是flex :1

可以改动一下

知识 这个可以记录一下,这只是一种实现思路 用到了justify-content

在这里插入图片描述

看一下老师的

知识 flex 1

也是用的flex布局,上下垂直居中,也是每个icon都平分,中间的icon设个padding 让他占的空间大一点,在右边的两个icon 贴在盒子的左边,左边的盒子贴在盒子的右边 这样也可以 但是有个疑问,为什么不让icon在盒子里居中呢,这种效果更好吗


接下来开发上一曲下一曲

这个其实就是拿到state里面的currentIndex,点击上一曲,把currentIndex-1赋给currentIndex,下一曲同理,当到最下一曲的时候,再点下一曲应该切到第一曲,那这时候就要拿到歌曲列表的length来进行判断,当点到最下一曲的时候,应该切到第一曲,那这时候就要拿到歌曲列表的length,应该也可以在disptach的时候拿吧,老师是在vue文件里拿的

我改了一下改成了在vuex里面写逻辑
需要考虑当前暂停歌曲,state为playingState为false,当点击下一曲的时候,音乐会自动播放,但是图标却没有变化,因为watch里面我们写了一个监听currentSong的逻辑,它一旦变化,就会自动播放,但是playingState却没有变化,所以需要切换它的状态

在actions selectPlay新增

  state.commit('setPlayListLen', list)

mutations:

    // 列表长度
    setPlayListLen(state, list) {
    
    
      state.songListLen = list.length
    },

在这里插入图片描述

还有一个边界情况,如果只有一首歌或者一首歌都没有也需要做一些处理

只有一首歌的情况,因为next和pre事件都会遇到,所以写个函数,能够复用这个函数

在这里插入图片描述

还有个问题,如果当前是暂停的状态,点击上一曲下一曲时,按钮是没有变化的,还是暂停的按钮,但是歌曲已经在播了

所以还是需要修改一下他的状态

在这里插入图片描述

5.4 修改报错

在这里插入图片描述

翻译过来是play()被一个加载的请求打断了? 其实并不会影响整个的播放效果

位置

在这里插入图片描述

给src赋值的时候报错

打个断点

在这里插入图片描述

先执行play方法,其实play方法返回的是一个promise对象,紧接着会进入到这里

在这里插入图片描述

这时候会给他赋值src 然后就报错了

解决这个问题也很简单,歌曲播放其实是要缓冲数据的,audio会有一个事件,有一个叫canplay

数据是流式加载的,缓冲一段数据之后就播放这一段,然后继续缓冲下一段,当有一段缓冲数据可以播放的时候就会触发canplay事件(好像不是这样? 我试了一下只会触发一次canplay事件) 不不不 如果不动他的话是只会触发一次的,如果拖动它是会触发的—来自5.11的经验。

https://www.runoob.com/tags/av-event-canplay.html

就是说缓冲数据目前够我们播放的时候就会触发这个事件

我们要用一个标志来控制(songReady),歌曲切换的时候,不能说状态一更新就播放,而是应该等歌曲ready的时候才去播放 所以定义一个songReady,初始值为false(歌曲没有准备好),

在这里插入图片描述

有了songReady就可以去控制能否播放

监听playing状态发生变化的时候,如果songReady为false就不要去播放

在这里插入图片描述

切歌的时候把songReady置为false

在这里插入图片描述

这样的话后面执行audioEl.play()的时候加载成功又会触发ready函数,再把songReady置为true

点前进和后退的时候也要做个判断,如果还未加载完就直接返回,就是说没有ready的情况就不让他前进或后退 如果songReady是false的话 那肯定就是点太快导致的

在这里插入图片描述

在快速点击的过程里,很多点击都是无效的,无效的情况下,可以在页面上看到效果,在songReady为false的情况下,可以给按钮添加disable的样式

这里有个问题,样式是用计算属性计算songReady的值的,所以应该不应该放在全局,因为这样监听不到它的变化

我直接喵喵喵??? 为什么 为什么一直是false

在这里插入图片描述

哦不好意思 是我拼错了o((⊙﹏⊙))o

还有播放的时候发现的问题

在这里插入图片描述

在这里插入图片描述

这个链接有问题,这样的话他就不会触发canplay这个事件了,它会派发一个error事件 可以用来解决上一首下一首不能切换的问题 我们前面设置了如果点击的时候songReady为false就直接返回,什么逻辑都不做,那现在只要把songReady改成true就可以了

5.5 歌曲播放模式相关逻辑

逻辑:点击图标以后,他就会派发一个changMode的actions事件,mode=(this.playMode + 1) % 3 把这个mode传进去在里面判断mode的值,之后根据mode的值来确定要不要洗牌sequenceList, commit这个mode mode变化以后,在vue文件里icon图标可以根据mode来确定

对于歌曲列表而言,它有三种播放模式

要做到图标和播放模式关联起来,点击按钮切换模式

在这里插入图片描述

在这里插入图片描述

这个是增强功能,可以不在主逻辑去写,可以拆成一个钩子函数,利用composition api钩子函数去拆到不同的文件去维护,这样的话,逻辑比较清晰了。

我还是先用vue2写,不拆了 疑问 vue2如何拆

获取播放的mode 根据mode来确定显示的图标
第二个三元运算符写错了 应该是playMode===PLAYMODE_random

在这里插入图片描述

切换状态:拿到playMode之后在他的基础上+1
点击事件

在这里插入图片描述

这样肯定不够啊,这只是图标变了而已,我们要去实现一个action,做进一步的操作

在这里插入图片描述

但是这样不行 state.sequence拿不到数据

在这里插入图片描述

我发现state.xxx都拿不到数据,

文档https://vuex.vuejs.org/zh/guide/actions.html#action

以后还是都解构出来

在这里插入图片描述

有个问题,歌曲暂停之后,切换模式,他会自动播放,但是暂停按钮还是暂停,

先解决切换到随机播放以后播放的不是原来的歌了

这个很好理解,就是随机播放的时候,把列表切换了,currentSong他是怎么来的,他是根据playlist和currentIndex两者计算而来的,在random情况下playlist被重新洗牌了,新的列表变了,但是索引没变,那么currentSong就会发生变化,

解决:拿到当前播放歌曲的id,先缓存一下当前播放的歌曲,

这次用的是findIndex方法 上次有个寻找position的

if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
    
    
  // 给元素添加relative样式
  addClass(el, relativeCls)
}

知识 findIndex 记得return

在这里插入图片描述

在这里插入图片描述

知识 这里老师讲了setup里面一些为了让代码更清晰,维持一定的顺序,

data-vuex-hooks-computed-watch-method

watch和计算属性的区别在于一个计算属性是声明式的,是根据一个响应式数据通过某种方式计算出另一个响应式数据,而watch更像是执行命令式的代码,更像是观测某个数据的变化然后执行一些逻辑,更侧重的是去写一些逻辑

5.6 歌曲收藏功能相关逻辑1

我先自己写一个

用isLike来判断是否已经收藏过了 同时用来控制按钮是不是红色的

这个要考虑的东西很多,先来写个最基础的

在这里插入图片描述

在这里插入图片描述

加个功能:取消收藏

在这里插入图片描述

继续完善,让isLike是根据storage/store有无此歌来决定 这里根据store来决定 在什么时机呢 --在歌曲发生变化的时候

在这里插入图片描述

但是这个state.likeList每次都会被清空,所以在进来页面的时候需要从缓存取出来

  created() {
    
    
    this.$store.commit('setLikelist', JSON.parse(sessionStorage.getItem(FAVORITE_KEY)))
  },

不不不 不需要这样,直接给state里面的likelist设初始值为缓存值就ok

在这里插入图片描述

这里有个要注意的地方, 当取不到缓存的时候,应该给他一个空数组,要不然用findIndex方法的时候会报错的

待做 要看看老师这一节是怎么写的,他封装了很多函数

缓存是存在相应的域名中的

知识 vuex和storage区别:vuex本质上是一个内存级别的存储,在页面的任何地方都能访问到这个数据,但是一旦刷新页面,内存就会重置,数据就全部被清空了,再次刷新的数据取决于state初始的数据,而本地存储他可以永久的把数据存储到浏览器中,

5.8 进度条相关

https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent

中间的进度条是一个进度条组件,一个黑色的背景是进度的总长度,左侧黄色的条是当前播放的进度,中间的滑块是可以左右拖动的,可以手动改变进度条,在播放的过程中,进度条是会变长的,并且滑块是向右偏移的,可以左右拖动滑块,拖动也是改变了播放进度,并且左侧的时间是会发生变化的

来实现播放过程中,进度条也会随之播放 组件的状态靠什么决定呢 可以靠进度来决定,组件的任何状态都可以根据进度来决定,父组件传入一个数字类型的progress

btn的位置,以及progress黄条的宽度都是根据progress计算而来的,宽度可以用一个数据offset来表示(定义个data),之后要监听progess,

https://cn.vuejs.org/v2/api/#vm-el

知识 获取根 DOM 元素

      watch: {
    
    
        progress(newProgress) {
    
    
          // 进度条宽度
          const barWidth = this.$el.clientWidth - progressBtnWidth
          // 偏移量
          this.offset = barWidth * newProgress
        }
      }

知识 当然可以用computed,但是要注意用computed获取el的宽度一开始肯定是获取不到的,computed一开始上来就计算一次,在模板被渲染的时候就会访问offset,然后就会计算一次el宽度,这时候组件还没有mounted,是获取不到的;watch的话,progress变化的时候其实已经渲染了,所以clientWidth就可以拿到,另外,因为之后还要处理一些逻辑,更偏向逻辑的编写,所以应该用watch去实现

有了offset之后要去映射dom,给黄色进度条和btn设置一个动态的style,
在这里插入图片描述

他们两个的style都是根据offset计算而来的,

      computed: {
    
    
        progressStyle(){
    
    
          return  `width: ${
      
      this.offset}px`
        },
        btnStyle() {
    
    
          return `transform: translate3d(${
      
      this.offset}px,0,0)`
        }
      },

现在来根据offset来计算出它的样式是怎么样的 我们接受progress这个属性,当外部的progress变了之后,就根据progress计算出它的offset,有了偏移量,样式就能发生变化,

疑问 flex 0 0 40px 与width 两者效果是类似的,但是在某些场合下,flex布局会出现挤压或塌陷的现象,导致宽度被挤压,所以设定width可以保证我们的宽度不变化

这里是监听canplay事件

在这里插入图片描述

父组件计算属性 播放进度:已播放时间/总时间 总时间已经拿到了,播放时间可以用一个事件:timeupdate来监听

在这里插入图片描述

现在的效果

可以看出来这是秒数,需要格式化时间,定义一个工具函数

插播 函数柯里化 https://www.jianshu.com/p/2975c25e4d71 IIFE:自我执行函数 柯里化
还有位运算一些东西 https://www.jianshu.com/p/a3202bc3f7a4
在这里插入图片描述

在这里插入图片描述

一个疑问 xxx.yyy|0 为什么等于xxx 为什么这里或运算符能有取整的作用呢

知识padstart方法

formatTime函数

formatTime(interval) {
    
    
  // interval 向下取整
  interval = interval | 0
  // 不足两位的话就向前填充一个0
  const minute = ((interval / 60 | 0) + '').padstart(2, '0')
  const second = ((interval % 60 | 0) + '').padstart(2, '0')
  return `${
      
      minute}:${
      
      second}`
}

但是并不能用 它识别不了这个padstart方法

所以只能自己写了

        formatTime(interval) {
    
    
          // interval 向下取整
          interval = interval | 0
          // 不足两位的话就向前填充一个0
          let minute = ((interval / 60 | 0) + '')
          let second = ((interval % 60 | 0) + '')
          let len = minute.length
          for( ;len<2;len++){
    
    
            minute='0'+minute
          }
          len = second.length
          for( ;len<2;len++){
    
    
            second='0'+second
          }
          return `${
      
      minute}:${
      
      second}`
        }

接下来写进度条的交互逻辑

支持拖动和点击

在移动端常见的就是ontouchstart ontouchmove ontouchend
https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent

知识 prevent修饰符

给滑块添加三个事件

      methods: {
    
    
        onTouchStart(e) {
    
    
          console.log(e);
        },
        onTouchMove(e) {
    
    
          console.log(e);
        },
        onTouchEnd(e) {
    
    
          console.log(e);
        }
      },

需要获取两个信息,一个是要知道它点击的位置,也就是说要知道他的横坐标是什么。以及左侧进度条的宽度(offset)

[screenX clientX pageX概念

因为横坐标的位置在touchmove的时候也需要获取,所以可以把数据绑定到一个可以被共享的对象上,可以在created钩子函数中定义一个对象,

      created() {
    
    
        this.touch = {
    
    }
      },

给黄条一个ref 之后

        onTouchStart(e) {
    
    
          // console.log(e);
          this.touch.x1=e.changedTouches[0].clientX
          // 黄色进度条初始宽度
          this.touch.beginWidth = this.$refs.progress.clientWidth
          console.log(this.touch);
        },
        onTouchStart(e) {
    
    
          // console.log(e);
          this.touch.x1=e.changedTouches[0].clientX
          // 黄色进度条初始宽度
          this.touch.beginWidth = this.$refs.progress.clientWidth
          console.log(this.touch);
        },
        onTouchMove(e) {
    
    
          // console.log(e);
          // x偏移量
          const delta = e.changedTouches[0].clientX-this.touch.x1
          // 之前的width+这次拖动增加的偏移量=应有的黄条长度
          const tempWidth = this.touch.beginWidth + delta
          // 再拿到barWidth
          const barWidth = this.$el.clientWidth - progressBtnWidth
          // 黄条长度/barwidth = progress 现在应该有的进度
          const progress = tempWidth/barWidth
          this.offset = barWidth * progress
          // console.log("tempWidth", tempWidth);
          // console.log("barWidth", barWidth);
          // console.log("progress", progress);

        },

来整理一下,最终目的是要拿到offset,offset是由progress和barWidth共同决定的,这里progress怎么算呢需要拿到当前黄条应该的宽度除总宽度,黄条应该的宽度就是一开始的宽度+这次滑动的x距离,然后barWidth的获取是简单的,之后就可以算出来了

会不会觉得多此一举呢 直接原来的黄条宽度+这次滑动的长度不就可以了吗 为什么还要算progress呢,因为要让外部知道,歌曲的进度发生了改变,要让他们对应上才可以,最终是要修改audio的,这个是用父组件做的,现在只是实现了拖动,所以需要派发事件,这里派发两个自定义事件,一个progress-changing事件,表示手指还在拖动的过程中,还没有离开,当手指离开的时候还要派发一个progress-change 把新的progress传出去

实时修改currentTime的值

在这里插入图片描述

这是拖动的时候修改currentTIme,修改音乐的时间是在手松开的时候,

在这里插入图片描述

但是我们暂停的时候发现是可以拖动的,但是播放的时候拖动发现是有问题的,

优化:在change的时候,如果是暂停的效果就让他播放,这时候就要定义一个isplay在点击播放暂停的时候翻转

在这里插入图片描述

现在来改bug,在播放的时候,拖动进度会出问题,为什么呢,监听progressChanging,我们修改了currentTime,这个currentTime一旦发生了改变,progress会根据currentTime做一个新的计算,然后传给子组件,子组件他就会进入到这个逻辑

在这里插入图片描述

offset就会重新做一次计算,

最后这里会覆盖

在这里插入图片描述

应该在update的时候需要做一些控制,在changing的过程加一个标志位,

在这里插入图片描述

就是说在update函数中,如果changing在拖动的过程中,不要去修改currentTime,在changing的过程中,就认为是进度条改变,他修改进度条的优先级高,自身播放导致的currentTime改变优先级比较低,

这样就ok了

除了拖动,我们还希望点击它跳转到对应位置,

知识webapi --getBoundingClientRect 方法返回元素的大小及其相对于视口的位置(获取短的那一条)。

在这里插入图片描述

用pagex获取长的那一条

        clickProgress(e){
    
    
          // console.log("fds");
          console.log('getBoundingClientRect', this.$el.getBoundingClientRect());
          const rect = this.$el.getBoundingClientRect()
          // 黄条应有的宽度
          const offsetWidth = e.pageX - rect.x
          const barWidth = this.$el.clientWidth - progressBtnWidth
          const progress = offsetWidth/barWidth
          this.$emit('progress-changed', progress)
          console.log(offsetWidth)
        }

有个问题,就是那个可以拖动的圆,它拖得时候可以超出范围,需要修改一下

使用 Math.min() 裁剪值(Clipping a value)

妙 progress修改

const progress = Math.min(1, Math.max(tempWidth / barWidth, 0))

还有问题,歌曲播放完了以后,我们希望歌曲结束了以后能根据播放模式,如果是顺序播放或随机播放,它就跳到下一首歌,如果是循环播放那就回到开始

加一个end回调函数

    end(e) {
    
    
      console.log("end")
      this.currentTime = 0
      if (this.playMode === PLAY_MODE.loop) {
    
    
        this.loop()
      } else {
    
    
        this.next()
      }
    }

5.10 cd唱片相关逻辑

老师是用的js控制是否旋转的,但是可以用一个属性 animation-play-state 更方便一点

重要的几个

        @keyframes rotateImg {
    
    
          from {
    
    
            transform: rotate(0deg);
          }
          to {
    
    
            transform: rotate(360deg);
          }
        }
    animationState() {
    
    
      return {
    
    
        animationPlayState: this.playing ? 'running' : 'paused'
      }
    }

5.11 歌词相关逻辑

歌词是通过单独的接口获取的,为什么不在请求歌曲列表的时候就一并返回呢,因为对于一个歌曲,歌词里的数据实在太庞大了,可以当访问到某首歌的时候再去异步请求他的歌词,这样的话数据量就可以大大减小,

router.js新增注册歌词接口 https://neteasecloudmusicapi.vercel.app/#/?id=%e8%8e%b7%e5%8f%96%e6%ad%8c%e8%af%8d

在这里插入图片描述

什么时候去调用呢,可以在当前歌曲发生变化的时候就是说切歌的时候,

我这里就不在项目里写了 先在html文件里写个demo

在这里插入图片描述

在这里插入图片描述


现在初步歌词能跟着音乐动了

在这里插入图片描述

来看一下

在音乐加载完之后(不应该在这里实例化,后面有改)

在这里插入图片描述

handleLyric 给currentLine赋值 用来高亮

在这里插入图片描述

点击暂停和播放

在这里插入图片描述

播放音乐中

在这里插入图片描述

playLyric

在这里插入图片描述

页面

在这里插入图片描述

但是会发现有时候高亮会有延迟,这是因为lyric-parser的问题

修改方法

https://github.com/ustbhuangyi/lyric-parser/issues/11

在这里插入图片描述

在这里插入图片描述

这样就ok了

但是发现个bug

在这里插入图片描述

它为什么会跳到前面

试了一下,不拖动的话是没有问题的,但是一旦拖动或者点击再点暂停,暂停功能就会失效

为什么呢 因为如果拖动的话canplay是会触发的,所以可能创建了很多个实例,开了很多定时器(可以看lyric-parse的源码),上一个还没被销毁就又创建了一个,导致上一个还没有关掉

再来整理一下,用到playLyric和stopLyric的地方

在这里插入图片描述

在这里插入图片描述

这样高亮功能就完成了,接下来完成自动滚动

希望的效果:前5行,不动,接下来第六行:当前减去前五行保证一直在中间的位置

在这里插入图片描述

获取外层滚动的盒子el,和滚动盒子里面内容的el

获取外层滚动盒子的el是为了能够滚动,获取里面的内容是为了拿到里面的子元素,获取一行的dom,计算出它的高度

我又改了一下样式,再来整理一下这个功能,因为p标签它自动会加上margin ,上下的margin还是重叠的,获取height的时候还不会加上margin的值,很麻烦,所以吧margin去掉了,换成了padding
在这里插入图片描述

web-api scrollTo的用法

https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo

在这里插入图片描述

现在把它套到项目里

插播 知识 注意 双击在移动端失效? display none 和block在@keyframes里面失效?

https://blog.csdn.net/Cherishor/article/details/51100303

https://www.imooc.com/wenda/detail/501578

这里用的是position: absolute; 脱离文档流


不知道为什么 用scrollToElement的时候会报错,但是用scrollTo就不会报错

知道了,需要拿到scroll实例 在scroll组件里

在这里插入图片描述

在这里插入图片描述

之后

在这里插入图片描述

可以看到能拿到实例了

里面就有scrolltoelement方法了

在这里插入图片描述

使用

在这里插入图片描述

有个问题,我们是获取歌词以后就让他自动播放歌词了,但是这时候音乐可能还没请求过来,所以可能会导致歌词会比歌曲播放的快,这时候可以在歌曲准备好的时候再次play一次歌词 让歌词和歌曲能够同步起来 还有种就是先请求到歌曲再请求到歌词的情况,请求完歌曲后调用playLyric,他会判断歌词有没有准备好,防止报错

在播放下一首歌的时候,如果不做处理,他会开两个定时器,就会出现歌词播放会乱掉的情况

所以 在监听到歌曲变化的时候 调用stop方法把上一个定时器清掉

还有一种情况 歌曲是纯音乐 没有歌词

在请求api的时候

在这里插入图片描述

不不不 好像纯音乐和伴奏他们返回的数据不一样

修改

在这里插入图片描述

为了符合网易云的歌词格式的规范

比如说这首歌·

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

不了不了, 不用为了符合网易云的官方加时间了不然到时候还得截取

在这里插入图片描述

再加点样式

在这里插入图片描述

在这里插入图片描述

这段空出来的是要显示现在正在播放的歌词

data里写一个变量playingLyric

怎么实时拿到它呢 用lyric库拿

在这里插入图片描述

知识点 betterscroll里的方向锁

5.16 mini player

在这里插入图片描述

cubic-bezier()

一开始没有播放音乐的话会出现这种情况

在这里插入图片描述

控制它不要显示

根据state里面的playlist(列表歌曲)来判断 ,如果列表里面没有歌曲,他就是空的 可以根据它来判断

在这里插入图片描述

  &.mini-enter-active, &.mini-leave-active{
    
    
    transition: all 0.6s cubic-bezier(0.45, 0, 0.55,1);
  }
  &.mini-enter-from, &.mini-leave-to{
    
    
    //transition: all 0.6s cubic-bezier(0.45, 0, 0.55,1);
    opacity: 0;
    //y轴位移 有从下往上冒的效果
    transform: translate3d(0, 100% , 0);
  }

图片旋转可以参照以前的代码

现在来实现点击迷你播放器展开播放器

增加点击事件 然后让state里的fullScreen变成true就ok

这里老师用的是svg 我觉得好难啊 要不用element ui里面的环形进度条吧

再写一个组件

<template>
<!--  注意阻止冒泡-->
  <div class="progressContainer" @click.stop="handleIsplay">
    <el-progress type="circle" :percentage="percentage" color="yellow" :width=41 :strokeWidth=2
                 :showText="false" ></el-progress>
    <i v-if="!isPlay" class="iconfont icon-24gl-play playIcon"></i>
    <i v-else class="iconfont icon-24gl-pause playIcon"></i>
  </div>

</template>

<script>
export default {
  name: "progress",
  props: {
    percentage: {
      type: Number,
      default: 0
    }
  },
  computed: {
    isPlay() {
      return this.$store.state.playing
    }
  },
  methods: {
    handleIsplay() {
      this.$store.commit('setPlayingState', !this.isPlay)
    }
  }
}
</script>

<style scoped lang="scss">
.progressContainer {
  position: relative;
  display: flex;

  .playIcon {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate3d(-50%, -50%, 0);
  }

}
</style>

手指左右滑动屏幕去切歌

可以用slider插件 之前的dom结构要变化一下

在这里插入图片描述

遍历播放列表渲染出sliderPage

当前渲染的那首歌就是把sliderPage和index对应起来

注意 在mounted钩子函数里可以直接初始化slider吗 不可以 因为miniplayer是通过vshow来驱动的 mounted钩子函数执行的时候这个miniplayer它不一定是显示的

在这里插入图片描述

那不显示的话 dom都没有真实的渲染出来

我们应该在mini-player显示的时候来完成slider的初始化 一个是fullScreen的值为false(不是全屏),一个是playlist的值大于0的时候(有播放的歌曲)显示

计算属性

    sliderShow() {
    
    
      return !this.fullScreen && this.playlist.length
    }

在这里插入图片描述

报错

在这里插入图片描述

注意知识 nexttick

从false变成true的过程中,需要等一个nexttick,dom才真正的渲染出来

在这里插入图片描述

还有一种写法

在这里插入图片描述

优化一下,因为slidershow它每次切换都会new一次BScroll, 只用第一次检测到slidershow变化new BScroll就可以了,不用每次变化都new 一次

在这里插入图片描述

做这个的目的就是mini-player显示的时候 slider做一个初始化,初始化完了以后,slider的currentpage 和播放列表currentIndex对应起来,怎么办呢 可以拿到BScroll实例的方法,goToPage

https://better-scroll.github.io/docs/zh-CN/plugins/slide.html#%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95

在这里插入图片描述

在currentIndex变化的时候也要跳转到指定page

注意前面应该再加个判断

在这里插入图片描述

在这里插入图片描述

有个样式需要注意

在这里插入图片描述


但是现在只是dom层面的操作,滑动的时候播放的歌曲并没有发生变化,因为currentIndex的索引并没有发生变化

解决 在初始化slider之后,可以去监听一个事件,

slidePageChanged

https://better-scroll.github.io/docs/zh-CN/plugins/slide.html#%E4%BA%8B%E4%BB%B6

在这里插入图片描述

5.20 21 交互动画 待看

5.22 播放列表相关逻辑

在这里插入图片描述

知识 vue3新增teleport内置组件

https://v3.cn.vuejs.org/api/built-in-components.html#teleport

https://v3.cn.vuejs.org/guide/teleport.html#teleport

这是一个playlist组件 这个组件有个半透明的层,当点击这个半透明的层以后他会关闭,顶部左侧是播放模式按钮,点击按钮的时候会切换播放模式,右侧垃圾桶,点击它会弹出一个询问框询问是否要清空列表,当点击清空的时候会把整个列表清空,中间一层是可滚动的歌曲列表,左侧可以显示歌曲的名称,当前播放的歌曲左侧会有一个播放的icon ,右侧有两个icon,一个是收藏歌曲,已收藏的话会亮一个红心,点击它的时候会切换收藏和未收藏,点击叉叉会删除这首歌曲 下方有个添加歌曲的功能,会牵扯到歌曲的搜索,现在暂时不做。

在这里插入图片描述

teleport 是用来干嘛的呢 vue组件是有个组件树的,他是会按组件树的结构去生成dom的,它的嵌套关系是和组件树的关系是一致的,但是teleport可以指定渲染到哪些部分的,to body就是让组件渲染到body下,如果是有些组件不想被父元素样式所影响,最好把它挂载到body上,在全屏类和弹层类的组件上可以用这个

这个组件是通过两个变量来决定的 一个是visible 一个是playlist.length vshow

list-wrapper内容层里面有list header 和scroll 列表滚动 他还有个list-footer 点击之后隐藏,我觉得还是写在右上角比较好,所以我的list-header要改造一下 然后list-footer不要了

用scroll组件 发现并不能滚动

在这里插入图片描述

注意

页面初始化scroll的时候还没有被渲染

看我们封装的组件,它是在mounted的时候被实例化的

在这里插入图片描述

但是实例化的时候页面并没有被渲染,高度计算不对 ,那就不能判断是否能滚动 这是经常会遇到的问题

初始化的时机不对,怎么解决呢 执行他的betterscroll实例的refresh方法 重新刷新, 什么时候重新计算呢 在show的时候

在这里插入图片描述

注意 这样还是不行 为什么呢

因为数据变了但是dom还没有更新呢

icon和favorite逻辑可以复用 老师用的composition api 我待会还是试一下mixin怎么用的

不过我现在还是先不复用了 先重新再写一遍 我怕待会脑子乱掉

在这里插入图片描述

正在播放列表高亮功能

https://img-blog.csdnimg.cn/fe9d7711166142248dcf701e5f0284ea.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Lyk5b-D5bCP546L5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16

嗯? 我的收藏缓存怎么写成sessionStorage了? 要改成localStorage

收藏的心亮

不对不对 computed不能传参进去? getFavoriteIcon 应该写在method里 有个疑问

点击触发togglelike 改变state里的likelist favoriteList也就变了 但是getFavoriteIcon 改变心的颜色,他会自动触发吗?? 结果是它确实运行了 看了一下coderwhy的视频 确实动态绑定style是可以在method下的 返回一个对象就可以 他应该是可以自动变化的 在函数里面log了一下 它好像隔很短的时间间隔都会运行一次? 是因为里面的响应式数据变化了

在这里插入图片描述

还是写个demo实验一下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
  
    <div :style="mystyle()">{
   
   {aaa}}</div>
    <div @click="changeColor">点击变颜色</div>
    <!-- {
    
    {color}}
    {
    
    {mystyle()}} -->
  </div>

  <script src="../../js/vue.js"></script>
  <script>
    const app=new Vue({
      
      
      el:"#app",
      data: {
      
      
        aaa:'fdasdas',
        color:'red'
      },
      methods: {
      
      
        mystyle() {
      
      
          console.log("mystyle");
          return {
      
      
            color: this.color,
            fontSize: "18px"
          }
        },
        changeColor() {
      
      
          this.color==="red"?this.color='blue':this.color='red'
        }
      },
    })
  </script>
</body>
</html>

切换收藏

先扩展小图标的点击范围

在这里插入图片描述

togglelike切换收藏

在这里插入图片描述

在这里插入图片描述

现在

在这里插入图片描述

还应该做点击图标切换播放模式 还有点击歌曲切歌曲的功能

点击图标切换模式

在这里插入图片描述

点击playlist item切换歌曲 当前一首歌是暂停状态时点击 应该让他自动播放 所以还应该commit playingstate为true

在这里插入图片描述

老师还做了每次打开都滚动到正在播放的歌曲的位置 通过watch currentSong实现

在这里插入图片描述

删除功能

把歌曲从sequencelist和playlist中移除 每次删除完dom就会立刻变化

可以封装成一个action

在这里插入图片描述

这样修改是会报错的,需要提交mutations

知识 .slice获得副本

在这里插入图片描述

在这里插入图片描述

这里还有个bug就是getIndex这种普通的方法不能写在actions里 和method不同,他如果用this.方法来调用是调用不到的,

它的this是这个

在这里插入图片描述

所以可以这样写

在这里插入图片描述

老师的vue3

在这里插入图片描述

现在可以删了 但是没有动画,有点生硬

在这里插入图片描述

知识 内置组件, transition-group

在这里插入图片描述

以后可能要复用

在这里插入图片描述

不复用的话

在这里插入图片描述

改删除逻辑的bug

如果是删除正在播放歌曲下面的歌曲是没有问题的,但是如果删除正在播放歌曲上面的歌曲的话 因为currentIndex没有变,所以他会切换到下面一首歌

在这里插入图片描述

在这里插入图片描述

bug

让他播放最后一首歌,删掉它以后会报错

在这里插入图片描述

为什么呢 比如现在一共10首歌,最后一首currentIndex就是9,

注意

那么现在删除以后,列表长度就是9,但是这时候currentIndex还没有变化,还是9,9的下标在长度为9的数组下是溢出的,最大是8才对,

所以这里可以再加一个逻辑

在这里插入图片描述

有个问题,当歌曲暂停的时候,点击它前面的歌曲以后,歌曲又会自动播放

这是为什么呢

在这里插入图片描述

在这里插入图片描述

我们当时是想解决暂停状态下切换它能自动播放,但是现在看来,派发sliderPageChanged他的条件会有很多种,除了左右活动会触发这个事件,还有是歌曲播到最后自动currentIndex发生变化

在这里插入图片描述

会执行goToPage也会触发这个事件,

总之就是currentSong发生了变化

删除歌曲的时候也有一些条件触发这个事件,但是我们不希望修改它的playingState 它的currentSong 没有变化

所以说可以在监听currentSong发生变化的时候 我们可以让它播放, 刚刚那种情况currentSong没有发生变化 所以可以在这里入手

player.vue里 watch currentSong变化时

this.$store.commit("setPlayingState", true)

这里老师有个bug 但是我的没有 看看是哪里需要注意的,我们删除歌曲以后,

在这里插入图片描述

这里的dom如果没有发生变化肯定是会报错的 就像这样

在这里插入图片描述

所以这里的scroll需要refresh一下 我这里每次从全屏切换回来的时候或者是playlist长度变化(其实最初是判断如果length=0的话那就不显示的)都会触发refresh

在这里插入图片描述

那为什么我的没有错 但是老师的有错呢 下面是老师写的 如果有playlist 这个computed就不会重新执行 学习一下!!写法

在这里插入图片描述

但是我的是每次长度变化了都会重新计算sliderShow

那么监听到sliderShow变化了以后,如果已经有Bscroll实例了 那就执行refresh

这是瞎猫碰上死耗子了 来看一下老师的解决办法

在这里插入图片描述

一定注意nextTick

这样bug就改完了吗

删除的时候是有过渡动画的,如果点的很快的话,有可能就会出现问题,

在这里插入图片描述

因为老师这里写了个动画,打开这个playlist以后让他自动滚动到当前播放的歌曲 所以我没有这个错误

看看老师是什么问题

在这里插入图片描述

返回的index为-1,为什么为-1呢, 快速点击其实就是对同一个按钮进行了多次点击,给按钮增加了remove事件,快速点击,在动画还没有完成的时候点击了两次,那removeSong就执行了两次, actions就执行了两次,

第一次从列表中删除了,第二次又是这首歌,那么findIndex就找不到他了, 比如说当前播放的currentIndex是0,findIndex是-1 -1<0 满足 currentIndex–变成-1了,这时候求currentSong就是一个undefined的值了, 这里老师前面把他变成一个空对象了在这里插入图片描述

变成一个空对象之后再执行下面的逻辑肯定有问题,

优化:

之前:

在这里插入图片描述

之后:

在这里插入图片描述

在这里插入图片描述

知识 常用手段 删除的时候是有动画的,我们希望在这个期间如果多次点击删除的话就不让他进行二次点击,所以可以加一个变量来控制,

在这里插入图片描述

那这样的话不就一直为true了吗 在什么时机给他设成false呢 300ms 因为动画是300ms

这个好像和防抖节流有点像?

在这里插入图片描述

dom层面和ismoving关联 一个不可点击的效果

在这里插入图片描述

我们为了更严格一点,在actions也可以做一层保护

在这里插入图片描述

清空playlist列表

待做 我觉得这个功能有点鸡肋

这个有个弹出框 confirm组件

点击确定删除的时候 调用一个action

在这里插入图片描述

在这里插入图片描述

这时候currentIndex发生变化了,那currentSong也变化了,currentSong就成了一个空对象了

在这里插入图片描述

那就肯定不会播放了,因为player组件有个发现没有id或者url就return的功能

在这里插入图片描述

新增

在这里插入图片描述

slider报错

在这里插入图片描述

全删了以后长度为0 这时候执行refresh是有问题的 因为对于slider而言,里面至少有一个dom才对,

在这里插入图片描述

更改

在这里插入图片描述

但是还是有问题,

清空之后 再次返回点击一首歌曲 playlist层他会直接这样弹出来

在这里插入图片描述

它显示的条件有两个

在这里插入图片描述

关闭的时候:visible为true, playlist.length为false 为什么visible为true呢 因为点击那个框框关闭清空列表的过程中没有改变visible的值 所以仍然为true

再次点开:playlist也是true了 ,所以就直接展示出来了 。我们希望应该点击icon才能展开它

解决:

在这里插入图片描述

同理 removeSong的时候也可以加一个判断

在这里插入图片描述

经验之谈,我们看到里面有非常多的边界条件需要处理,这样一种开发方式是工作中经常遇到的一些问题和一些挑战,那么怎么才能让开发代码的坑变少呢–开发逻辑一定要合理,尽量不要写一些为了解决某个bug写一些逻辑不合理的代码,这样的话看上去这个bug修复了,但是很可能会引发另外一个bug,所以一定要想到这样一个代码它是不是合理的,逻辑很重要,逻辑好,思路清楚,bug可能性就会变少。同时写代码的时候一定要清楚,要写一些保护啊,边界条件判断啊,这些都是工作中不断积累的

5.27 滚动列表高度自适应

还有个问题

被遮住了,滚动不到底部 对列表需要做一些调整

在这里插入图片描述

其实只要把bottom设置成mini-player的高度就ok

知识

music-list组件

我们只动态计算了top的值,现在还需要动态计算bottom的值 因为如果列表都被删光的话mini-player会隐藏,所以这时候bottom可以是0 但是如果不是0的话那就是要等于mini-player的高度了

扩展知识 mapstate https://vuex.vuejs.org/zh/guide/state.html#mapstate-%E8%BE%85%E5%8A%A9%E5%87%BD%E6%95%B0

在这里插入图片描述

同样,这两个页面同样也滚动不到底部

在这里插入图片描述

可以发现他们都是一级路由

知识 可以在一级路由的入口设置

知识 mapState语法糖

在这里插入图片描述

在这里插入图片描述

bug 为什么会有这种情况,因为层的高度是变了,但是scroll没有refresh重新计算,他还是根据原先的值计算 为什么滚动一次就没有问题了呢,因为滚动过程中内部会执行一次refresh,计算就正确了,他就会重新把这个位置给算进去,

接下来解决自动刷新的问题

解决思路就是观测playlist的变化,变化执行scroll实例的refresh, 那我们是不是要在基础的scroll组件内部编写逻辑,watch playlist的变化,然后动态执行实例的refresh,这样从技术上是可行的,但是并不合理,为什么? scroll组件是个基础组件,playlist是个业务数据,这个逻辑也是偏业务的一个逻辑,把业务数据放在基础组件里面去实现是不合理的,而且并不是所有的scroll组件都需要这个逻辑,比如说歌词也是使用scroll组件的,它就不需要,所以说放在基础组件里面是不合理的

那怎么办呢,可以封装成一个业务组件,在基础scroll组件之上去封装一个高阶函数,(一个高阶的scroll组件) 保持它的使用方式和基础组件是一致的,

我去百度搜了一下,发现搜高阶组件的都是有关react的

https://blog.csdn.net/qq_33834489/article/details/79249088

开始

首先看一下业务组件的模板是怎么样的 vue的模板导出工具–https://vue-next-template-explorer.netlify.app/

其实也可以用模板实现高阶组件的,但是老师说先多教一点知识,所以这里用这种方法,用纯js来实现

那么就要用到render函数,比如说高阶组件模板现在长成这样子

<scroll
  v-bind="$props"
  @scroll="$emit('scroll,$event')"
>
<slot></slot>

</scroll>

它内部是对scroll组件的调用,scroll组件把props传入进去,并且能监听内部scroll组件派发的事件 然后派发到外层,同时接收一个slot插槽,这样就能保证和基础的scroll组件一致了,

render函数

在这里插入图片描述

接下来编写高阶scroll组件

在components层级下新建一个wrap-scroll wrap-scroll内部要有一个js文件来实现这个逻辑

因为是个组件,所以最终还是要export一个对象

因为是对基础组件的封装,所以要import一个基础的组件

接收props,可以直接使用scroll组件的props属性

因为高阶组件和基础组件的使用方式是一样的,所以接收props也是一样的,所以可以直接使用scroll.props

import scroll from "@/components/base/scroll/scroll";
export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits
}

接下来就比较关键了,就是实现它的render函数,vue3的render函数和2不一样,

引入渲染函数

在这里插入图片描述

import {
    
     h, mergeProps } from 'vue'
import scroll from "@/components/base/scroll/scroll";

export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits,
  // ctx可以理解为this 上下文实例,
  render(ctx) {
    
    
    // 可以理解为vue2的createElement和createComponent
    return h(scroll, mergeProps(ctx.$props, {
    
    
      onScroll: (e) => {
    
    
        ctx.$emit('scroll', e)
      }
    }))
  }
}

接下来就是插槽的部分了

可以看到插槽部分是第三个参数

在这里插入图片描述

_withCtx :保证上下文是正确的

import {
    
     h, mergeProps, withCtx, renderSlot } from 'vue'
import scroll from "@/components/base/scroll/scroll";

export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits,
  // ctx可以理解为this 上下文实例,
  render(ctx) {
    
    
    // 可以理解为vue2的createElement和createComponent
    return h(scroll, mergeProps(ctx.$props, {
    
    
      onScroll: (e) => {
    
    
        ctx.$emit('scroll', e)
      }
    }), {
    
    
      default: withCtx(() => {
    
    
        return [renderSlot(ctx.$slots), 'default']
      })
    })
  }
}

如何去拿到对应的实例呢,

在这里插入图片描述

import {
    
     h, mergeProps, withCtx, renderSlot , ref} from 'vue'
import scroll from "@/components/base/scroll/scroll";

export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits,
  // ctx可以理解为this 上下文实例,
  render(ctx) {
    
    
    // 可以理解为vue2的createElement和createComponent
    return h(scroll, mergeProps({
    
     ref: "scrollRef" }, ctx.$props, {
    
    
      onScroll: (e) => {
    
    
        ctx.$emit('scroll', e)
      }
    }), {
    
    
      default: withCtx(() => {
    
    
        return [renderSlot(ctx.$slots), 'default']
      })
    })
  },
  setup() {
    
    
    const scrollRef = ref(null)
    return {
    
    
      // return到模板中
      scrollRef
    }
  }
}

这样就能在模板中访问到scroll组件的实例了,

还要拿到playlist的数据

import {
    
     h, mergeProps, withCtx, renderSlot, ref, computed, watch, nextTick} from 'vue'
import scroll from "@/components/base/scroll/scroll";
import {
    
     useStore } from "vuex";

export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits,
  // ctx可以理解为this 上下文实例,
  render(ctx) {
    
    
    // 可以理解为vue2的createElement和createComponent
    return h(scroll, mergeProps({
    
     ref: "scrollRef" }, ctx.$props, {
    
    
      onScroll: (e) => {
    
    
        ctx.$emit('scroll', e)
      }
    }), {
    
    
      default: withCtx(() => {
    
    
        return [renderSlot(ctx.$slots), 'default']
      })
    })
  },
  setup() {
    
    
    const scrollRef = ref(null)
    const store = useStore()
    const playlist = computed(() =>
      store.state.playlist
    )
    watch(playlist, async () => {
    
    
      // scrollRef.value.scroll :scroll对象
      await nextTick()
      scrollRef.value.scroll.refresh()
    });
    return {
    
    
      // return到模板中
      scrollRef
    }
  }
}

我们还要把scroll对象return出去,因为要保持一致

在这里插入图片描述

这样写是有问题的,因为一开始定义的scrollRef是个null,这时候执行.value那肯定也是个null,

那就有个技巧是用计算属性 妙

在这里插入图片描述

完整版

import {
    
     h, mergeProps, withCtx, renderSlot, ref, computed, watch, nextTick} from 'vue'
import scroll from "@/components/base/scroll/scroll";
import {
    
     useStore } from "vuex";

export default {
    
    
  name: 'wrap-scroll',
  props: scroll.props,
  emits: scroll.emits,
  // ctx可以理解为this 上下文实例,
  render(ctx) {
    
    
    // 可以理解为vue2的createElement和createComponent
    return h(scroll, mergeProps({
    
     ref: "scrollRef" }, ctx.$props, {
    
    
      onScroll: (e) => {
    
    
        ctx.$emit('scroll', e)
      }
    }), {
    
    
      default: withCtx(() => {
    
    
        return [renderSlot(ctx.$slots), 'default']
      })
    })
  },
  setup() {
    
    
    const scrollRef = ref(null)
    const store = useStore()
    const playlist = computed(() =>
      store.state.playlist
    )
    watch(playlist, async () => {
    
    
      // scrollRef.value.scroll :scroll对象
      await nextTick()
      scroll.value.refresh()
    });
    return {
    
    
      // return到模板中
      scrollRef,
      scroll
    }
  }
}

来检测一下对不对

有个地方写错了

在这里插入图片描述

index-list和music-list引入这个js文件

之后就ok了

猜你喜欢

转载自blog.csdn.net/xiaozhazhazhazha/article/details/120165886