(1)项目初始化和推荐页面开发:
重点:轮播图、Scroll、下拉加载方法实现、
问题一:轮播图实现
① 获取轮播图数据:虽然找到接口了,但是由于XHR请求在浏览器端会有跨域的限制,不能直接请求QQ官网的接口地址,需要做一层Proxy代理:
获取响应数据:
获取轮播图数据,使用BetterScroll的基本配置项,监听slideWillChange事件
onMounted(() => {
const sliderVal = slider.value = new BScroll(wrapperRef.value, {
click: true,
scrollX: true,
scrollY: false,
momentum: false,
bounce: false,
probeType: 2,
slide: true
})
sliderVal.on('slideWillChange', (page) => {
currentPageIndex.value = page.pageX
})
})
问题二: 歌单列表 超过 recommend 页面时 不能滚动,如何解决?
添加 overflow: scroll;属性就可以滚动了
支持slot 插槽的方式,滚动内容放入插槽中,外层可以和better-scroll做一些初始化联动
// 构造钩子 函数
import BScroll from '@better-scroll/core'
import ObserveDOM from '@better-scroll/observe-dom'
import { onMounted, onUnmounted, ref } from 'vue'
BScroll.use(ObserveDOM)
export default function useScroll(wrapperRef, options, emit) {
const scroll = ref(null)
onMounted(() => {
const scrollVal = scroll.value = new BScroll(wrapperRef.value, {
observeDOM: true,
...options
})
if (options.probeType > 0) {
scrollVal.on('scroll', (pos) => {
// useScroll(rootRef, props, emit) 通过 emit 把位置信息派发出去
emit('scroll', pos)
})
}
})
onUnmounted(() => {
scroll.value.destroy()
})
return scroll
}
Better-Scroll debugger发现容器高度和内容高度不一致时,采用observe-dom开启对content以及content子元素DOM改变的探测。当插件被使用后,当这些DOM元素发生变化时,将会触发scroll的refresh方法:
1. 针对改变频繁的CSS属性,增加debounce;
2. 如果改变发生在scroll动画过程中,则不会触发refresh
问题三:图片懒加载的实现> assets-js-create-loading-like-directive
vue2.0 vue-lazyload会实现图片懒加载的效果,默认先加载chicken图片,再去加载新的图片
/** 实现loading 加载的处理 */
import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'
/**
* base.scss 定义的属性 --- 处理歌手详情页 边界效果的处理
* .g-relative { position: relative; }
*/
const relativeCls = 'g-relative'
export default function createLoadingLikeDirective(Comp) {
return {
mounted(el, binding) {
const app = createApp(Comp)
const instance = app.mount(document.createElement('div'))
const name = Comp.name
if (!el[name]) {
el[name] = {}
}
el[name].instance = instance
const title = binding.arg
if (typeof title !== 'undefined') {
instance.setTitle(title)
}
if (binding.value) {
append(el)
}
},
updated(el, binding) {
const title = binding.arg
const name = Comp.name
if (typeof title !== 'undefined') {
el[name].instance.setTitle(title)
}
if (binding.value !== binding.oldValue) {
binding.value ? append(el) : remove(el)
}
}
}
function append(el) {
const name = Comp.name
const style = getComputedStyle(el)
if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
addClass(el, relativeCls)
}
el.appendChild(el[name].instance.$el)
}
function remove(el) {
const name = Comp.name
removeClass(el, relativeCls)
el.removeChild(el[name].instance.$el)
}
}
在组件中引入:
(2)歌手页面开发:
问题:IndexList组件功能实现、use-fix歌手列表固定标题实现、use-shortcut歌手列表快速导航入口实现
问题一:IndexList组件功能 界面实现
歌手列表导航实现: 调用接口,获取封装后的数据,如何根据序列化找到对应歌手
界面实现: 上方有固定标题栏,左侧 右侧是序列分成了不同组设置快速导航入口A-Z排序
<!-- 歌手列表 将数据呈现-->
<template>
<scroll
class="index-list"
:probe-type="3"
@scroll="onScroll"
ref="scrollRef"
>
<!-- 采用 groupRef 来获取DOM -->
<ul ref="groupRef">
<li
v-for="group in data"
:key="group.title"
class="group"
>
<h2 class="title">{{group.title}}</h2>
<ul>
<li
v-for="item in group.list"
:key="item.id"
class="item"
@click="onItemClick(item)"
>
<img class="avatar" v-lazy="item.pic">
<span class="name">{{item.name}}</span>
</li>
</ul>
</li>
</ul>
<!-- 歌手列表固定标题 -->
<div
class="fixed"
v-show="fixedTitle"
:style="fixedStyle"
>
<div class="fixed-title">{{fixedTitle}}</div>
</div>
<!-- 快速导航入口 设置 A-Z 排序 -->
<div
class="shortcut"
@touchstart.stop.prevent="onShortcutTouchStart"
@touchmove.stop.prevent="onShortcutTouchMove"
@touchend.stop.prevent
>
<ul>
<li
v-for="(item, index) in shortcutList"
:key="item"
:data-index="index"
class="item"
:class="{'current': currentIndex === index}">
{{ item }}
</li>
</ul>
</div>
</scroll>
</template>
问题二:use-shortcut 设置快速导航入口:
function onShortcutTouchStart(e) {
console.log(e.target)
const anchorIndex = parseInt(e.target.dataset.index)
touch.y1 = e.touches[0].pageY
touch.anchorIndex = anchorIndex
// 滑动到目标锚点
scrollTo(anchorIndex)
}
// 根据鼠标move 快速移动
function onShortcutTouchMove(e) {
touch.y2 = e.touches[0].pageY
// 接近栏 距离 上一栏 距离 判断
const delta = (touch.y2 - touch.y1) / ANCHOR_HEIGHT | 0
const anchorIndex = touch.anchorIndex + delta
// 滑动到目标锚点
scrollTo(anchorIndex)
}
function scrollTo(index) {
if (isNaN(index)) {
return
}
// 设置index 的值
index = Math.max(0, Math.min(shortcutList.value.length - 1, index))
const targetEl = groupRef.value.children[index]
const scroll = scrollRef.value.scroll
scroll.scrollToElement(targetEl, 0)
}
问题三:标题固定
当calculate() 完成之后 设置nextTick 执行延迟回调
probeType
- 类型:Number
- 默认值:0
- 可选值:1、2、3
- 作用:有时候我们需要知道滚动的位置。
- 当 probeType 为 1 的时候,会非实时(屏幕滑动超过一定时间后)派发scroll 事件;
- 当 probeType 为 2 的时候,会在屏幕滑动的过程中实时的派发 scroll 事件;
- 当 probeType 为 3 的时候,不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。
- 如果没有设置该值,其默认值为 0,即不派发 scroll 事件。
存储singer关键字
(3) 歌手页面开发 singer-detail
歌手的唯一标识 singerMid: req.requery.mid
异步获取关键歌手信息:
在上推的时候 图层变模糊 列表往上滑动 overflow-hidden: false
需要设置图片层级 上面的标题 在上推的时候,标题不会被掩盖
在下拉的时候 图片整体放大 有回弹效果
<scroll
class="list"
:style="scrollStyle"
v-loading="loading"
v-no-result:[noResultText]="noResult"
:probe-type="3"
@scroll="onScroll" //设置scroll 来监听事件
>
核心代码:具体的含义是什么
为什么设置maxTranslateY
const RESERVED_HEIGHT = 40 作用是什么
Z-Index 折叠层 较大的元素会覆盖较小的元素在上层进行显示
const RESERVED_HEIGHT = 40 // 设置默认标签头高度
export default {
name: 'music-list',
components: {
SongList,
Scroll
},
props: {
songs: {
type: Array,
default() {
return []
}
},
title: String,
pic: String,
loading: Boolean,
noResultText: {
type: String,
default: '抱歉,没有找到可播放的歌曲'
},
// rank 榜单标签 默认 boolean
rank: Boolean
},
data() {
return {
imageHeight: 0,
scrollY: 0,
maxTranslateY: 0
}
},
computed: {
noResult() {
return !this.loading && !this.songs.length
},
playBtnStyle() {
let display = ''
if (this.scrollY >= this.maxTranslateY) {
display = 'none'
}
return {
display
}
},
bgImageStyle() {
const scrollY = this.scrollY
let zIndex = 0
let paddingTop = '70%'
let height = 0
// 兼容 iPhone 设备
let translateZ = 0
// 可以滚动到的最大高度
if (scrollY > this.maxTranslateY) {
zIndex = 10
paddingTop = 0
height = `${RESERVED_HEIGHT}px`
translateZ = 1
}
// 实现向上推的效果 默认值为1
let scale = 1
// 向下推 Y坐标为负数
if (scrollY < 0) {
scale = 1 + Math.abs(scrollY / this.imageHeight)
}
return {
zIndex,
paddingTop,
height,
backgroundImage: `url(${this.pic})`,
transform: `scale(${scale})translateZ(${translateZ}px)`
}
},
scrollStyle() {
const bottom = this.playlist.length ? '60px' : '0'
return {
top: `${this.imageHeight}px`,
bottom
}
},
// 设置遮罩层
filterStyle() {
let blur = 0
const scrollY = this.scrollY
const imageHeight = this.imageHeight
if (scrollY >= 0) {
blur = Math.min(this.maxTranslateY / imageHeight, scrollY / imageHeight) * 20 // 设置blur缩放比
}
return {
backdropFilter: `blur(${blur}px)`
}
},
...mapState([
'playlist'
])
},
mounted() {
this.imageHeight = this.$refs.bgImage.clientHeight
this.maxTranslateY = this.imageHeight - RESERVED_HEIGHT // 可以移动的最大高度
},
methods: {
goBack() {
this.$router.back()
},
onScroll(pos) {
this.scrollY = -pos.y
},
// 学到这里的时候对其他部分进行处理
selectItem({ index }) {
this.selectPlay({
list: this.songs,
index
})
},
random() {
this.randomPlay(this.songs)
},
...mapActions([
'selectPlay',
'randomPlay'
])
}
}
(4)歌手详情页组件开发
问题一: 歌手详情页 详情页刷新 -- 刷新依然可以访问(有问题)-1:20
mid 总是加载不出来 依赖于传递的prop:singer 对象
分析 之前页面渲染会报错:现在的页面渲染依赖于prop singer这个数据,之前为什么可以 因为会通过歌手列表sing-list一级路由,点击某个歌手的时候 可以拿到这个歌手对应的singer对象,渲染二级路由的时候将singer对象进行传递,所以页面可以正常渲染,但是一旦页面进行刷新之后 内存中的数据都丢失了,因为刷新之后 没有经过一级路由 不知道点击了对应的哪个歌手,这样就拿不到对应的歌手对象 拿不到歌手对象 页面自然不能正常渲染 要保证可以正常渲染 借助浏览器的本地存储能力 使用存储一个使用localstorage和sessionstorage(关掉页面就没有了 但是刷新还是在的),当然 这里页面刷新使用sessionstorage 就足够实现
借助浏览器的本地存储能力 来解决这个问题:
使用 good-storage的方式
import storage from 'good-storage'
// localStorage
Storage.set(key, val)
Storage.get(key, def)
// sessionStorage
storage.session.set(key, val)
storage.session.get(key, val)
通过 constance.js 来存储项目中的共享常量
// 定义当前的singer_key 对象
export const SINGER_KEY = '__singer__'
export const FAVORITE_KEY = '__favorite__'
export const ALBUM_KEY = '__album__'
export const TOP_KEY = '__top__'
export const SEARCH_KEY = '__search__'
export const PLAY_KEY = '__play__'
/**
* 歌手详情页_音乐播放器PLAY_MODE
* sequence 顺序播放
* loop 循环播放
* random 随机播放
*/
export const PLAY_MODE = {
sequence: 0,
loop: 1,
random: 2
}
(2)问题二:transitions 实现歌手详情页路由的过渡效果 transform: translate3d(0, 0, 0);
<template>
<!-- 当singers数组为空的时候 加载loading -->
<div class="singer" v-loading="!singers.length">
<index-list
:data="singers"
@select="selectSinger"
></index-list>
<router-view v-slot="{ Component }">
<transition appear name="slide">
<!-- 传递 对应的singer数据 -->
<component :is="Component"
:data="selectedSinger" />
</transition>
</router-view>
</div>
</template>
(3)歌手详情页歌曲列表随机播放 shuffle 函数引入 实现随机播放
// 实现shuffle 洗牌功能
export function shuffle(source) {
const arr = source.slice()
for (let i = 0; i < arr.length; i++) {
const j = getRandomInt(i)
swap(arr, i, j)
}
return arr
}
function getRandomInt(max) {
return Math.floor(Math.random() * (max + 1))
}
function swap(arr, i, j) {
const t = arr[i]
arr[i] = arr[j]
arr[j] = t
}
(5)播放器内核组件开发
(6)歌单详情页与排行榜页面开发
歌单详情页: 歌手详情获取 sessionstorage
在渲染歌手详情页面 要获取到对应 歌曲的cached.id来拿到数据,对singer拿到数据是singer.mid
computedSinger() {
let ret = null
const data = this.data
if (data) {
ret = data
} else {
const cached = storage.session.get(key)
// 歌手详情获取的getSingerDetail.mid 但对于 歌曲详情 是数字id 这里需要转换成字符串 所以是catche.id
if (cached && (cached.mid || cached.id + '') === this.$route.params.id) {
ret = cached
}
}
return ret
}
歌单排行榜 top-list.vue 根据Key 值来获取到 排行榜列表
排行榜 的编号:获取TopDetail 具体信息
export function getTopDetail(top) {
return get('/api/getTopDetail', {
id: top.id,
period: top.period
})
}
歌手排行榜和 图标
<template>
<ul class="song-list">
<li
class="item"
v-for="(song, index) in songs"
:key="song.id"
@click="selectItem(song, index)"
>
<div class="rank" v-if="rank">
<span :class="getRankCls(index)">{{ getRankText(index) }}</span>
</div>
<div class="content">
<h2 class="name">{{song.name}}</h2>
<p class="desc">{{getDesc(song)}}</p>
</div>
</li>
</ul>
</template>
<script>
export default {
name: 'song-list',
props: {
songs: {
type: Array,
default() {
return []
}
},
rank: Boolean
},
emits: ['select'],
methods: {
getDesc(song) {
return `${song.singer}·${song.album}`
},
selectItem(song, index) {
this.$emit('select', { song, index })
},
getRankCls(index) {
if (index <= 2) {
return `icon icon${index}` // 在css中设置了对应的样式
} else {
return 'text'
}
},
getRankText(index) {
if (index > 2) {
return index + 1
}
}
}
}
</script>
<style lang="scss" scoped> 省略一部分
.icon {
display: inline-block;
width: 25px;
height: 24px;
background-size: 25px 24px;
&.icon0 {
@include bg-image('first');
}
&.icon1 {
@include bg-image('second');
}
&.icon2 {
@include bg-image('third');
}
}
<style>
(7)搜索页面开发:
具备搜索框(在搜索框中输入文字 就会显示出歌曲或者歌手,向上滑动的时候就会进行歌单的加载-上拉加载)、热门搜索 点击表单会在输入框进行回显、搜索历史呈现
v-model 监听事件 监听input框中数据的变化
v-model是双向绑定,当值进行传递的时候,会根据传输的值进行渲染,子组件是不能修改值的,子组件的值是从父组件而来。但是data中定义的变量是可以进行修改的,再设置watch进行监听
所以不能写成: v-model: modelValue
当输入框中频繁输入进行内容提交时
采用 throttle-debounce 实现节流效果 输入内容较多时候(JS高级 之 防抖 debounce - throttle 节流_js防抖debounce_玄鱼殇的博客-CSDN博客)
防抖 debounce
1. 概念
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间
- 当事件密集触发时,函数的触发会被频繁的推迟,并把上一次的给取消掉
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数
2. 应用场景
- 输入框中频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定操作
- 用户缩放浏览器的resize事件
节流 throttle
1. 概念
- 当事件触发时,会执行这个事件的响应函数
- 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数
- 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的
2. 应用场景
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 轮播图的按钮滚动
this.$watch('query', debounce(300, (newQuery) => {
this.$emit('update:modelValue', newQuery.trim())
}))
<template>
<div class="search-input">
<i class="icon-search"></i>
<input
class="input-inner"
v-model="query"
:placeholder="placeholder"
/>
<i
v-show="query"
class="icon-dismiss"
@click="clear"
></i>
</div>
</template>
<script>
import { debounce } from 'throttle-debounce'
export default {
name: 'search-input',
props: {
modelValue: String,
placeholder: {
type: String,
default: '搜索歌曲、歌手'
}
},
data() {
return {
query: this.modelValue
}
},
created() {
this.$watch('query', debounce(300, (newQuery) => {
this.$emit('update:modelValue', newQuery.trim())
}))
this.$watch('modelValue', (newVal) => {
this.query = newVal
})
},
methods: {
clear() {
this.query = ''
}
}
}
</script>
① 实现数据绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)。
搜索suggest完成组件化开发 (写完)
设置use-pull-up-load 进行下拉框加载
onMounted(() => {
const scrollVal = scroll.value = new BScroll(rootRef.value, {
pullUpLoad: true,
observeDOM: true,
click: true
})
scrollVal.on('pullingUp', pullingUpHandler)
async function pullingUpHandler() {
if (preventPullUpLoad.value) {
scrollVal.finishPullUp()
return
}
isPullUpLoad.value = true
await requestData()
scrollVal.finishPullUp()
scrollVal.refresh()
isPullUpLoad.value = false
}
})
设置use-search-history 获取搜索历史记录
export default function useSearchHistory() {
const maxLen = 200
const store = useStore()
function saveSearch(query) {
const searches = save(query, SEARCH_KEY, (item) => {
return item === query
}, maxLen)
store.commit('setSearchHistory', searches)
}
function deleteSearch(query) {
const searches = remove(SEARCH_KEY, (item) => {
return item === query
})
store.commit('setSearchHistory', searches)
}
function clearSearch() {
const searches = clear(SEARCH_KEY)
store.commit('setSearchHistory', searches)
}
return {
saveSearch,
deleteSearch,
clearSearch
}
}
(8)添加歌曲到用户中心
(9)性能优化
利用keep-alive实现组件应用:每次切换Tab的时候 都会重新渲染组件并发送异步请求,即使这个组件被访问过还是会重复这个逻辑。但这个项目要求没有这么高,所以下次重复访问的时候,存储的数据还是可以再次使用的
Dom 随着Tab切换缓存依然存在,需要设置 重置onActivated和清理onDeactivated的工作-Scroll Slide use-mini-slide use-pull-up-load
<router-view :style="viewStyle" v-slot="{ Component }">
<keep-alive>
<component :is="Component"/>
</keep-alive>
</router-view>
(2)路由组件异步加载:实现非首屏代码异步加载,减缓首屏代码加载执行速率(路由懒加载)
依靠webpack+ES6的的方式
在设置路由后 进行模式注释
configureWebpack: (config) => {
if (process.env.npm_config_report) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
config.plugins.push(new BundleAnalyzerPlugin())
}
},
路由懒加载
webpack提供的require.ensure()
vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。
这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
/* const Home = () => import('@/components/home')
const Index = () => import('@/components/index')
const About = () => import('@/components/about') */
// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。 把组件按组分块
const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home')
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index')
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about')
(3)通过webpack组件化管理进行项目部署