学黄轶老师vue3音乐APP(2-4章)
学习黄老师的音乐app课程有一段时间了,学到了很多之前不知道的花式骚操作,怕自己忘了,记录一下自己学习的收获
# 第二章 项目初始化和推荐页面开发
# 如何使用脚手架创建vue3项目
- 运行 vue creat vue-music-next,其中vue-music-next是新建的项目的名字,可以随便写
- 然后选择Manually select features (手动的去选择) 然后最主要的是vue版本选3,其他的随意
# 项目中数据mock方案
黄老师的项目中数据使用的是真实接口的数据,但是真实接口数据会有跨域的问题,服务端是不存在跨域的,所以使用webpack
的devServer
来解决一下:
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' });
});
}
}
}
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'}
})
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>
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 补丁
算法中,如果 VNode
的 ref
键对应于渲染上下文中的 ref
,则 VNode
的相应元素或组件实例将被分配给该 ref
的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
作为模板使用的 ref
的行为与任何其他 ref
一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
# 滚动组件封装
首先写好基本的模板
<template>
<div>
<slot></slot>
</div>
</template>
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()
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这里面暴露一个函数useScroll
,传入一个ref
对象。
首先定义一个scroll
的ref
对象,表示这个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>
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
}
}
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
})
})
//...
2
3
4
5
6
7
8
9
10
然后在setup
函数中拿到props
,然后在使用useScroll
时,传递props
:
//...
setup(props) {
//...
userScroll(rootRef, props)
//...
}
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
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实例,我们是在其他地方也可以利用createApp
api去创建一个新的实例,然而这个实例挂载的地方是一个动态创建的div,这个div并没有实质上的DOM层的挂载,为什么不要挂载到DOM上呢,因为它挂载的目的很明确,它要挂载到el上。这个loading还有一些定位的问题,不过这个很简单,只是以后写的时候需要注意。
# composition API
setup
选项在组件创建之前执行,一旦props
被解析,就将作为组合式API的入口。
setip
选项是一个接受props
和context
的函数。此外,我们将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
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
},
//...
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)`
}
}
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)`
}
}
2
3
4
5
6
7
8
9
10
11
# 歌手详情页刷新
因为歌手的数据是从上一个页面带过来的,所以一刷新数据就会丢失。 所以做如下操作:
- 歌手的id通过url带过来,刷新之后url中的id不会丢失
- 歌手的数据在跳转到详情也的同时存在 session中
- 刷新的时候先对比session中存的歌手id和url的id是不是一致,如果不一致就跳转回一级路由。如果一致就把session中存的数据拿出来用
# 关于硬件加速
之前的代码里出现了很多次transform:translateZ(0)
这种样式。这里是起硬件加速的作用。
如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):
perspective: 1000px;
backface-visibility: hidden;
transform: translateZ(0);
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>
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>
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)
}
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()] : []
})
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
})
2
3
4
在严格模式下,无论何时发生了状态变更且不是由 mutation
函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
注意
不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。
# vuex的内置Logger插件
Vuex 自带一个日志插件用于一般的调试:
import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
plugins: [createLogger()]
})
2
3
4
5
日志插件还可以直接通过 <script>
标签引入,它会提供全局方法 createVuexLogger
。
要注意,logger 插件会生成状态快照,所以仅在开发环境使用。