摘要:后两个属性可选。属性定义了项目的缩小比例,默认为,即如果空间不足,该项目将缩小。属性定义了在分配多余空间之前,项目占据的主轴空间。它的默认值为,即项目的本来大小。结合的异步组件和的代码分割功能,轻松实现路由组件的懒加载。
项目总结
这是我第二个用 Vue 实现的项目,下面内容包括了在实现过程中所记录的知识点以及一些小技巧
项目演示地址:https://music-vue.n-y.io
源代码地址:https://github.com/nanyang24/...
此应用的全部数据来自 QQ音乐,利用 axios 结合 node.js 代理后端请求抓取
全局通用的应用级状态使用 vuex 集中管理
全局引入 fastclick 库,消除 click 移动浏览器 300ms 延迟
页面是响应式的,适配常见的移动端屏幕,采用 flex 布局
疑难总结 & 小技巧 关于 Vue 知识 & 使用技巧 v-html 可以转义字符,处理特定接口很有用 watch 对象可以观测 属性 的变化 像这种父组件传达子组件的参数通常都是在data()里面定义的,为什么这里要放到created()定义,两者有什么区别呢?因为这个变量不需要观测它的变化,因此不用定义在 data 里,这样也会对性能有所优化
不明白什么时候要把变量放在data()里,什么时候又不需要放 ?需要监测这个数据变化的时候,放在 data() 里,会给数据添加 getter 和 setter
生命周期 钩子函数生命周期钩子函数,比如 mounted 是先触发子组件的 mounted,再会触发父组件的 mounted,但是对于 created 钩子,又会先触发父组件,再触发子组件。
销毁计数器如果组件有计数器,在组件销毁时期要记得清理,好习惯
对于 Vue 组件,this.$refs.xxx 拿到的是 Vue 实例,所以需要再通过 $el 拿到真实的 dom 关于 JS 知识 & 技巧 setTimeout(fn, 20)一般来说 JS 线程执行完毕后一个 Tick 的时间约17ms内 DOM 就可以渲染完毕所以课程中 setTimeout(fn, 20) 是非常稳妥的写法
关于 webpack 知识 & 技巧 " ~ " 使 SCSS 可以使用 webpack 的相对路径@import "~common/scss/mixin"; @import "~common/scss/variable";babel-runtime 会在编译阶段把 es6 语法编译的代码打包到业务代码中,所以要放在dependencies里。 Fast Click 是一个简单、易用的库,专为消除移动端浏览器从物理触摸到触发点击事件之间的300ms延时 为什么会存在延迟呢?
从触摸按钮到触发点击事件,移动端浏览器会等待接近300ms,原因是浏览器会等待以确定你是否执行双击事件
何时不需要使用FastClick 不会伴随监听任何桌面浏览器
Android 系统中,在头部 meta 中设置 width=device-width 的Chrome32+ 浏览器不存在300ms 延时,所以,也不需要
同样的情况也适用于 Android设备(任何版本),在viewport 中设置 user-scalable=no,但这样就禁止缩放网页了
IE11+ 浏览器中,你可以使用 touch-action: manipulation; 禁止通过双击来放大一些元素(比如:链接和按钮)。IE10可以使用 -ms-touch-action: manipulation
请求接口jsonp:
XHR:
手写轮播图利用 BScroll
BScroll 设置 loop 会自动 clone 两个轮播插在前后位置
如果轮播循环播放,是前后各加一个轮播图保证无缝切换,所以需要再加两个宽度
if (this.loop) { width += 2 * sliderWidth }
初始化 dots 要在 BScroll 克隆插入两个轮播图之前
dots active状态 是通过判断 currentIndex 与 index 是否相等
currentIndex 更新是通过获取 scroll 当前 page,BScroll 提供了 api 方便调用
this.currentPageIndex = this.scroll.getCurrentPage().pageX
为了保证改变窗口大小依然正常轮播,监听窗口 resize 事件,重新渲染轮播图
window.addEventListener("resize", () => { if (!this.scroll || !this.scroll.enabled) return clearTimeout(this.resizeTimer) this.resizeTimer = setTimeout(() => { if (this.scroll.isInTransition) { this._onScrollEnd() } else { if (this.autoPlay) { this._play() } } this.refresh() }, 60) })
在切换 tab 相当于 切换了 keep-alive 的组件
轮播会出问题,需要手动帮助执行,利用了 activated , deactivated 钩子函数
activated() { this.scroll.enable() let pageIndex = this.scroll.getCurrentPage().pageX this.scroll.goToPage(pageIndex, 0, 0) this.currentPageIndex = pageIndex if (this.autoPlay) { this._play() } }, deactivated() { this.scroll.disable() clearTimeout(this.timer) }
实测,首次打开网页并不会执行 activated,只有在之后切换 tab ,切回来才会执行
在组件销毁之前 beforeDestroy 销毁定时器是好习惯,keep-alive 因为是将组件缓存了,所以不会触发
beforeDestroy() { this.scroll.disable() clearTimeout(this.timer) }后端接口代理
简单设置一下 Referer, Host,让别人直接通过浏览器抓到你的接口
但是这种方式防不了后端代理的方式
前端 XHR 会有跨域限制,后端发送 http 请求则没有限制,因此可以伪造请求
axios 可以在浏览器端发送 XMLHttpRequest 请求,在服务器端发送 http 请求
(在项目编写阶段,可以将后端代理请求写在 webpack 的 dev 文件的 before 函数内)
before(app) { app.get("/api/getDiscList", function (req, res) { const url = "https://c.y.qq.com/splcloud/fcgi-bin/fcg_get_diss_by_tag.fcg" axios.get(url, { headers: { referer: "https://c.y.qq.com/", host: "c.y.qq.com" }, params: req.query }).then((response) => { res.json(response.data) // axios 返回的数据在 response.data,要把数据透传到我们自定义的接口里面 res.json(response.data) }).catch((e) => { console.log(e) }) }); }
定义一个路由,get 到一个 /api/getDiscList 接口,通过 axios 伪造 headers,发送给QQ音乐服务器一个 http 请求,还有 param 参数。
得到服务端正确的响应,通过 res.json(response.data) 返回到浏览器端
另外 因为是 http 请求数据,是ajax,所以 format 参数要将原本接口的 jsonp 改为 json
大公司怎么防止被恶意代理呢?当你的访问量大的时候,出口ip会被查到获取封禁,还有一种就是参数验签,也就是请求人家的数据必须带一个签名参数,然后这个签名参数是很难拿到的这个正确的签名,从而达到保护数据的目的
当然,获取的数据并不能直接拿来用,需要做进一步的规格化,达到我们使用的要求,所以在这方面多带带封装了一个 class 来处理这方面的数据,具体请看src/common/js/song.js
flex 布局,热门歌单推荐左侧 icon 固定大小,flex: 0 0 60px
flex 属性是 flex-grow , flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto。后两个属性可选。
flex-grow 属性定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
flex-shrink 属性定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
右侧 text 区块 自适应占据剩下的空间,并且内部也采用 flex,使用 flex-direction: column; justify-content: center; 来达到纵向居中排列
recommend 页面 利用 BScroll 滚动Scroll 初始化但却没有滚动,是因为初始化时机不对,必须保证数据到来,DOM 成功渲染之后 再去进行初始化
可以使用父组件 给 Scrol组件传 :data 数据,Scroll 组件自己 watch 这个 data,有变化就立刻 refesh 滚动
新版本 BScroll 已经自己实现检测 DOM 变化,自动刷新,大部分场景下无需传 data 了
所以也就 无需监听 img 的 onload 事件 然后执行 滚动刷新 了
迷你播放器暂停状态,进入全屏,按钮在进度条最左边
原因:当播放器最小化的时候,progress-bar 仍然在监听 percent 的变化,所以在不断计算进度条的位置,然而这个时候由于播放器隐藏,进度条的宽度 this.$refs.progressBar.clientWidth 计算为0,因此计算出来的 offset 也是不对的,导致再次最大化播放器的时候,由于播放器是暂停状态, percent 并不会变化,也不会重新计算这个 offset ,导致 Bug。
解决方案:当播放器最大化的时候,手动去计算一次 offset,确保进度条的位置正确。
progress-bar 组件要 watch 下 fullScreen,在进入全屏的时候调用一下 移动按钮函数
歌词 lyric获取歌词,虽然我们约定返回数据是 json,但QQ音乐 返回的是依然是 jsonp,所以我们需要做一层数据的处理
const reg = /^w+(({.+}))$/
就是将返回的jsonp格式摘取出我们需要的json字段
ret = JSON.parse(matches[1])
将正则分组(就是正则括号内的内容)捕获的json字符串数据 转成 json 格式
然后我们在 player 组件中监听 currentSong 的变化,获取 this.currentSong.getLyric()
axios.get(url, { headers: { referer: "https://c.y.qq.com/", host: "c.y.qq.com" }, params: req.query }).then((response) => { let ret = response.data if (typeof ret === "string") { const reg = /^w+(({.+}))$/ const matches = ret.match(reg) if (matches) { ret = JSON.parse(matches[1]) } } res.json(ret) })
然后我们得到的返回数据的是 base64 的字符串,需要解码,这里用到了第三方库: js-base64
(我们这次用的是QQ音乐pc版的歌词,需要解码base64,而移动版的QQ音乐是不需要的)
this.lyric = Base64.decode(res.lyric)
之后利用第三方库: js-lyric ,解析我们的歌词,生成方便操作的对象
getLyric() { this.currentSong.getLyric() .then(lyric => { this.currentLyric = new Lyric(lyric) }) }歌词滚动
当前歌曲的歌词高亮是利用 js-lyric 会派发的 handle 事件
this.currentLyric = new Lyric(lyric, this.handleLyric)
js-lyric 会在每次改变当前歌词时触发这个函数,函数的参数为 当前的 lineNum 和 txt
而 使当前高亮歌词保持最中间 是利用了 BScroll 滚动至高亮的歌词let middleLine = isIphoneX() ? 7 : 5 // 鉴于iphonex太长了,做个小优化 if (lineNum > middleLine) { let lineEl = this.$refs.lyricLine[lineNum - middleLine] this.$refs.lyricList.scrollToElement(lineEl, 1000) } else { this.$refs.lyricList.scrollTo(0, 0, 1000) }cd 与 歌词 之间滑动
通过监听 middle 的 三个 touch 事件
offsetWidth 是为了计算歌词列表的一个偏移量的,首先它的偏移量不能大于0,也不能小于 -window.innerWidth。
left 是根据当前显示的是 cd 还是歌词列表初始化的位置,如果是 cd,那么 left 为 0 ,歌词是从右往左拖的,deltaX 是小于 0 的,所以最终它的偏移量就是 0+deltaX;如果已经显示歌词了,那么 left 为 -window.innerWidth,歌词是从左往右拖,deltaX 是大于 0 的,所以最终它的偏移量就是 -window.innerWidth + deltaX。
middleTouchStart(e) { this.touch.initiated = true this.touch.startX = e.touches[0].pageX this.touch.startY = e.touches[0].pageY }, middleTouchMove(e) { if (!this.touch.initiated) return const deltaX = e.touches[0].pageX - this.touch.startX const deltaY = e.touches[0].pageY - this.touch.startY if (Math.abs(deltaY) > Math.abs(deltaX)) { return } const left = this.currentShow === "cd" ? 0 : -window.innerWidth const offsetWidth = Math.min(0, Math.max(-window.innerWidth, left + deltaX)) this.touch.percent = Math.abs(offsetWidth / window.innerWidth) console.log(this.touch.percent) this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)` this.$refs.lyricList.$el.style[transitionDuration] = 0 this.$refs.middleL.style.opacity = 1 - this.touch.percent this.$refs.middleL.style[transitionDuration] = 0 }, middleTouchEnd() { let offsetWidth, opacity // 从右向左滑 的情况 if (this.currentShow === "cd") { if (this.touch.percent > 0.1) { offsetWidth = -window.innerWidth opacity = 0 this.currentShow = "lyric" } else { offsetWidth = 0 opacity = 1 } } else { // 从左向右滑 的情况 if (this.touch.percent < 0.9) { offsetWidth = 0 opacity = 1 this.currentShow = "cd" } else { offsetWidth = -window.innerWidth opacity = 0 } } const durationTime = 300 this.$refs.lyricList.$el.style[transform] = `translate3d(${offsetWidth}px,0,0)` this.$refs.lyricList.$el.style[transitionDuration] = `${durationTime}ms` this.$refs.middleL.style.opacity = opacity this.$refs.middleL.style[transitionDuration] = `${durationTime}ms` }优化
Vue 按需加载路由:
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import("./Foo.vue") // 返回 Promise
在我们的项目中的 router/index.js 是这样定义的:
// Vue 异步加载路由 // 引入5个 一级路由组件 const Recommend = () => import("components/recommend/recommend") const Singer = () => import("components/singer/singer") const Rank = () => import("components/rank/rank") const Search = () => import("components/search/search") const UserCenter = () => import("components/user-center/user-center") // 二级路由组件 const SingerDetail = () => import("components/singer-detail/singer-detail") const Disc = () => import("components/disc/disc") const TopList = () => import("components/top-list/top-list")
无需改动其他的代码
手机联调电脑,手机 同一WIFI下
配置 config 的 index.js 里的 host 为 "0.0.0.0",手机可以打开电脑的IP地址+端口查看
mac下 ifconfig 查看ip
移动端调试工具移动端console:vConsole
移动端抓包工具:charles
以上是在实现这个音乐 Vue 项目中遇到的难点以及一些使用技巧。在这里记录下来方便以后自己查阅,还能够给同样在前端这个小领域奋斗的大家提供一小些学习资料~
我的 Github:https://github.com/nanyang24
如果对你有帮助,欢迎 star 和 互粉 ~
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/107496.html
摘要:前言最近在自学打算自己仿一个项目来实战一下,由于本人很喜欢听歌,所以就选择了网易云音乐,在这与大家分享一下自己所遇到的问题,其中也有些不足之处也希望大家提一些宝贵的意见,互相学习,一起进步。 showImg(https://segmentfault.com/img/remote/1460000015805758); 前言 最近在自学vue,打算自己仿一个项目来实战一下,由于本人很喜欢听...
前言:当下音乐播放器不胜其数,为了更好的掌握一些东西,我们来自己制作一个音乐播放器。 文章目录: 一.开发环境:二.页面视图:1.主文件入口(首页):2.音乐播放界面: 三.功能实现(1)、index.html:(2)、播放音乐(music.html):(3)、样式文件(index.css): 四.项目地址: 一.开发环境: 开发工具:HbuliderX; 框架:Vant,Mui,V...
摘要:在中新建组件许文瑞正在吃屎。。。。在中添加如下代码三歌手组件开发歌手首页开发数据获取数据获取依旧从音乐官网获取歌手接口创建我们和以前一样,利用我们封装的等发放,来请求我们的接口,返回给。 Vue-Music 跟学一个网课老师做的仿原生音乐APP跟学的笔记,记录点滴,也希望对学习vue初学小伙伴有点帮助 showImg(https://segmentfault.com/img/remot...
摘要:每次用网易云音乐客户端播放听歌的时候,收藏的歌曲,在我的博客上也可以同步进行更新。 最近应该发现,我的博客https://blog.codelabo.cn左下角多了一个音乐播放器 showImg(https://segmentfault.com/img/remote/1460000016786096?w=1806&h=952); 这个是怎么实现的?一起来看看吧 APlayer 首先我们...
阅读 2615·2023-04-25 15:15
阅读 1291·2021-11-25 09:43
阅读 1552·2021-11-23 09:51
阅读 1065·2021-11-12 10:36
阅读 2865·2021-11-11 16:55
阅读 935·2021-11-08 13:18
阅读 696·2021-10-28 09:31
阅读 2016·2019-08-30 15:47