学黄轶老师vue3音乐APP(2-4章)

更新时间: 2021-07-26 15:33:58

学习黄老师的音乐app课程有一段时间了,学到了很多之前不知道的花式骚操作,怕自己忘了,记录一下自己学习的收获

# 第二章 项目初始化和推荐页面开发

# 如何使用脚手架创建vue3项目

  1. 运行 vue creat vue-music-next,其中vue-music-next是新建的项目的名字,可以随便写
  2. 然后选择Manually select features (手动的去选择) 然后最主要的是vue版本选3,其他的随意

# 项目中数据mock方案

黄老师的项目中数据使用的是真实接口的数据,但是真实接口数据会有跨域的问题,服务端是不存在跨域的,所以使用webpackdevServer来解决一下:
devServer.before提供了一个在 devServer 内部的 所有中间件执行之前的自定义执行函数,所以可以利用它来模拟接口: 例: 在vue.config.js中如下定义:

//vue.config.js
module.exports = {
    css: {
        loaderOptions:{
            sass: {
                //全局引入变量和 mixin
                prependData: `
                    @import "@/assets/scss/variable.scss";
                    @import "@/assets/scss/mixin.scss";
                `
            }
        }
    },
    devServer: {
        before(app) {
            app.get('/some/path', function (req, res) {
                res.json({ custom: 'response' });
            });
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

然后就可以在项目中使用接口:

axios.get('/some/path').then((res) => {
    this.data = res.data // {custom:'response'}
})
1
2
3

# 组合式API模板引用(ref)

在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:

<template> 
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // DOM元素将在初始渲染后分配给ref
        console.log(root.value) // <div>这是根元素</div>
      })

      return {
        root
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

这里我们在渲染上下文中暴露 root,并通过 ref="root",将其绑定到 div 作为其 ref。在虚拟 DOM 补丁算法中,如果 VNoderef 键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。

作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。

# 滚动组件封装

首先写好基本的模板

<template>
  <div>
    <slot></slot>
  </div>
</template>
1
2
3
4
5

使用插槽的形式,滚动的内容部分可用放到插槽的那一块,然后外层可用和betterScroll做一些初始化的联动。使用compositionAPI和钩子函数的方式。
所以再新建一个js文件use-scroll.js

import BScroll from '@better-scroll/core'
import ObserveDOM from '@/better-scroll/observe-dom' //自动探测DOM的高度
import { onMounted, onUnmounted, ref } from 'vue'

BScroll.use(ObserveDOM)

export default function useScroll(wrapperRef) {
  const scroll = ref(null)

  onMounted(() => {
    scroll.value = new BScroll(wrapperRef.value, {
      observeDOM: true
    })
  })

  onUnmounted(() => {
    scroll.value.destroy()
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这里面暴露一个函数useScroll,传入一个ref对象。
首先定义一个scrollref对象,表示这个scroll对象是响应式的。
onMounted钩子函数中拿到对应的DOM,实现betterScroll的初始化。
onUnMounted钩子函数中去执行scroll实例的卸载逻辑。

下面在scroll组件种去引用它:

<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(rootRef)

      return {
        rootRef
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

先引入useScroll,因为我们使用compositionAPI,所以我们在setup()函数中去调useScroll的函数,要传入一个ref对象,所以要定义一个rootRef。 使用compositionAPI后,很多东西都要手动去定义响应式。const rootRef = ref(null),定义好了之后一定要return,不然不会生效。

这个滚动组件是比较简单的,我们加props增加可以自定义的参数。

props: {
  click: {
    type: Boolean,
    default: true
  }
}
1
2
3
4
5
6

然后我们需要把props传递给初始化scroll的方法。首先给useScroll函数扩展一个options参数:

//...
export default function useScroll(wrapperRef, options) {
//...  
  onMounted(() => {
    scroll.value = new BScroll(wrapperRef.value, {
      observeDOM: true,
      ...options
    })
  })
//...  
1
2
3
4
5
6
7
8
9
10

然后在setup函数中拿到props,然后在使用useScroll时,传递props

//...
setup(props) {
  //...
  userScroll(rootRef, props)
  //...
}
1
2
3
4
5
6

# v-loading自定义指令开发

首先开发一个loading组件,loading组件很简单,但是这样直接用不优雅,所以可以实现一个v-loading的自定义指令。vue3的自定义指令和vue2略有不同。

指令作用是把loading组件动态插入到指令作用的对象内部
那我们怎么创建组件对应的DOM呢?我们也是可以新建一个vue实例,创建一个新的app对象,然后用loading组件,然后我们再动态去挂载,然后产生一个实例,在实例里面就可以拿到它的DOM对象。

import { createApp } from 'vue'
import Loading from './loading'

cosnt loadingDirective = {
  mounted(el, binding) {
    const app = createApp(Loading)
    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

实际上vue开发其实它是多实例的,并不是说只能在入口里面就创建一个唯一的App实例,我们是在其他地方也可以利用createAppapi去创建一个新的实例,然而这个实例挂载的地方是一个动态创建的div,这个div并没有实质上的DOM层的挂载,为什么不要挂载到DOM上呢,因为它挂载的目的很明确,它要挂载到el上。这个loading还有一些定位的问题,不过这个很简单,只是以后写的时候需要注意。

# composition API

setup选项在组件创建之前执行,一旦props被解析,就将作为组合式API的入口。
setip选项是一个接受propscontext的函数。此外,我们将setup返回的所有内容都暴露给组件的其余部分(计算属性、方法、生命周期钩子等等)以及组件的模板。

# 带ref的响应式变量

在Vue3.0中,我们可以通过一个新的ref函数使任何响应式变量在任何地方起作用,ref接收参数并将其包裹在一个带有value property的对象中返回,返回可以使该property访问或更改响应式变量的值。

# 在setup内注册声明周期钩子

组合式API上的声明周期钩子与选项式API的名称相同,但前缀为on

# watch响应式更改

就像在组件中使用watch选项上设置侦听器一样,我们也可以从Vue导入的watch函数执行相同的操作。它接受3个参数:

  • 一个想要侦听的响应式引用或getter函数
  • 一个回调
  • 可选的配置选项

我们只能将顶层的 data、props 或 computed property 名作为字符串传递。对于更复杂的表达式,用一个函数取代。

# 独立的computed属性

与ref和watch类似,也可以使用从Vue导入的computed函数在Vue组件外创建计算属性。

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2
1
2
3
4
5
6
7
8

这里我们给computed函数传递了第一个参数,它是一个类似getter的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的value,我们需要像ref一样使用.value property。

# 第三章 歌手页面开发

# 歌手列表和字母表如何关联

1.首先将歌手列表渲染出来 2.然后计算每个字母的列表的高度,并将它们存在一个数组里,这将是以后判断当前列表滚动到哪里的重要依据 3.滚动的时候将当前滚动高度返回出来,并且监听这个高度,计算出当前字母的index 4.在字母列表上滑动的时候,也是根据滑动的高度,去改变这个index,由此将二者联系起来

# 歌手列表的当前的字母如何写动画

1.在滚动列表的时候,计算当前字母歌手列表的底部距离顶部有多少距离 2.当小于一个标题高度的时候,改变标题的translate3d的y分量。

# 第四章 歌手详情页开发

# 歌手详情页面组件交互效果

歌手详情页的交互效果很特别,首先是列表往下拉的时候歌手的图片会放大,往上翻的时候歌手的图片会被挡住并且逐渐变模糊。

首先这些效果都和列表的位置有很大关系,所以监听scroll组件的scroll事件,拿到实时的scrollY

<scroll
        class="list"
        :probe-type="3"
        @scroll="onScroll"
    >
      <!-- -->
    </scroll>

//...

onScroll(pos) {
    this.scrollY = -pos.y
},
//...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后使用计算属性,根据scrollY动态计算歌手图片的style属性:

//...
//这个是标题栏的高度
const RESERVED_HEIGHT = 40 
this.imageHeight = this.$refs.bgImage.clientHeight
this.maxTranslateY = this.imageHeight - RESERVED_HEIGHT
//...
bgImageStyle() {
    const scrollY = this.scrollY
    let zIndex = 0
    //给图片一个padding高度撑开
    let paddingTop = '70%'
    let height = 0
    let translateZ = 0 // 为了兼容IOS

    //列表拖动的高度超出了标题位置的时候,将标题的zIndex提高,保证列表不遮标题
    if (scrollY > this.maxTranslateY) {
        zIndex = 10
        paddingTop = 0
        height = `${RESERVED_HEIGHT}px`
        translateZ = 1 // 为了兼容IOS
    }

    let scale = 1

    //列表向下拉的时候,修改scale值来放大图片
    if (scrollY < 0) {
        scale = 1 + Math.abs(scrollY / this.imageHeight)
    }

    return {
        backgroundImage: `url(${this.pic})`,
        zIndex,
        paddingTop,
        height,
        transform: `scale(${scale})translateZ(${translateZ}px)`
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

注意

当取用响应式变量大于一次的时候,一定要用临时变量存储

接着来写图片逐渐变模糊的效果:

模糊效果是一层div,再加上backdropFilter: blur(10px)这样的样式,blur中的数值越大模糊的程度越大,所以这个参数适合用scrollY来计算出来:

filterStyle() {
  let blur = 0
  const scrollY = this.scrollY
  const imageHeight = this.imageHeight
  if (scrollY >= 0) {
    blur = Math.min(this.maxTranslateY / imageHeight * 20, scrollY / imageHeight * 20)
  }
  return {
    backdropFilter: `blur(${blur}px)`
  }
}
1
2
3
4
5
6
7
8
9
10
11

# 歌手详情页刷新

因为歌手的数据是从上一个页面带过来的,所以一刷新数据就会丢失。 所以做如下操作:

  1. 歌手的id通过url带过来,刷新之后url中的id不会丢失
  2. 歌手的数据在跳转到详情也的同时存在 session中
  3. 刷新的时候先对比session中存的歌手id和url的id是不是一致,如果不一致就跳转回一级路由。如果一致就把session中存的数据拿出来用

# 关于硬件加速

之前的代码里出现了很多次transform:translateZ(0)这种样式。这里是起硬件加速的作用。 如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):

perspective: 1000px;
backface-visibility: hidden;
transform: translateZ(0);
1
2
3

# 歌手页面路由效果过渡

<router-view>暴露了一个 v-slot API,主要使用 <transition><keep-alive> 组件来包裹你的路由组件。

<Suspense>
  <template #default>
    <router-view v-slot="{ Component, route }">
      <transition :name="route.meta.transition || 'fade'" mode="out-in">
        <keep-alive>
          <component
            :is="Component"
            :key="route.meta.usePathKey ? route.path : undefined"
          />
        </keep-alive>
      </transition>
    </router-view>
  </template>
  <template #fallback> Loading... </template>
</Suspense>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • Component: 要传递给 <component> 的 VNodes 是 prop。
  • route: 解析出的标准化路由地址。

所以singer.vue页面的router-view需要改一下:

<router-view v-slot="{Component}">
    <transition appear name="slide">
        <somponent :is="Component" :singer="selectedSinger"/>
    </transition>
</router-view>
1
2
3
4
5

然后添加上对应的transition的样式,就可以轻松实现页面切换的时候的过渡

.slide-enter-active, .slide-leave-active {
    transition: all 0.3s;
}

.slide-enter-from, .slide-leave-to {
    transform: translate3d(100%,0,0)
}
1
2
3
4
5
6
7

# 如何在开发环境中调试vuex

 





 






 
 



import { createStore, createLogger } from 'vuex'
import state from './state'
import mutations from './mutations'
import * as getters from './getters'
import * as actions from './actions'

const debug = process.env.NODE_ENV !== 'production'

export default createStore({
  state,
  getters,
  mutations,
  actions,
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# vuex的严格模式

开启严格模式,仅需在创建 store 的时候传入 strict: true

const store = createStore({
  // ...
  strict: true
})
1
2
3
4

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

注意

不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。

# vuex的内置Logger插件

Vuex 自带一个日志插件用于一般的调试:

import createLogger from 'vuex/dist/logger'

const store = new Vuex.Store({
  plugins: [createLogger()]
})
1
2
3
4
5

日志插件还可以直接通过 <script> 标签引入,它会提供全局方法 createVuexLogger

要注意,logger 插件会生成状态快照,所以仅在开发环境使用。