vue项目总结

栏目: 编程语言 · 发布时间: 5年前

内容简介:项目总结上部分是一个轮播图组件,使用第三方库 better-scroll 辅助实现,抓取 QQ音乐(移动端)数据下部分是一个歌单推荐列表,使用 axios + Node.js 代理后端请求,绕过主机限制 (伪造 headers),抓取 QQ音乐(PC端)数据

项目总结

项目总结

推荐页

上部分是一个轮播图组件,使用第三方库 better-scroll 辅助实现,抓取 QQ音乐(移动端)数据

下部分是一个歌单推荐列表,使用 axios + Node.js 代理后端请求,绕过主机限制 (伪造 headers),抓取 QQ音乐(PC端)数据

歌单推荐列表图片,使用图片懒加载技术 vue-lazyload,优化页面加载速度

为了更好的用户体验,当数据未请求到时,显示 loading 组件

推荐 recommend 页 -> 歌单详情页

由于歌手的状态多且杂,这里使用 vuex 集中管理歌手状态

这个组件更加注重 UX,做了很多类原生 APP 动画,如下拉图片放大、跟随推动、ios 渐进增强的高斯模糊效果 backdrop-filter 等

歌手 singer 页

左右联动是这个组件的难点

左侧是一个歌手列表,抓取 QQ音乐(PC端)歌手数据 并 重组 JSON 数据结构

右侧是一个字母列表,与左侧歌手列表联动,滚动固定标题实现

列表图片使用懒加载技术 vue-lazyload,优化页面加载速度

排行 rank 页

普通组件,很简单

排行页 -> 歌单详情页

复用歌单详情页

搜索 search 页

抓数据,写组件,另外,根据抓取的数据特征,做了上拉刷新的功能

考虑到数据量大且频繁的问题,对请求做了节流处理

考虑到移动端键盘占屏的问题,对滚动前的 input 做了 blur() 操作

对搜索历史进行了 localstorage 缓存,清空搜索历史时使用了改装过的 confirm 组件

支持将搜索的歌曲添加到播放列表

个人中心 user-center

localstorage 中 “我的收藏” 和 “最近播放” 反映到界面上

播放器内核页 player

核心组件。用 vuex 管理各种播放时状态,播放、暂停等功能调用 audio API

播放器可以最大化和最小化

中部唱片动画使用第三方 JS 动画库 create-keyframe-animation 实现

底部操作区图标使用 iconfonts

抽象了一个 横向进度条组 件和一个 圆形进度条组件 ,横向进度条可以拖动小球和点击进度条来改变播放进度,圆形进度条组件使用 SVG <circle> 元素

播放模式有:顺序播放、单曲循环、随机播放,原理是调整歌单列表数组

歌词的爬取利用 axios 代理后端请求,伪造 headers 来实现,先将歌词 jsonp 格式转换为 json 格式,再使用第三方库 js-base64 进行 Base64 解码操作,最后再使用第三方库 lyric-parser 对歌词进行格式化

实现了侧滑显示歌词、歌词跟随进度条高亮等交互效果

增加了当前播放列表组件,可在其中加入/删除歌曲

其他

此应用的全部数据来自 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 知识 & 技巧

一般来说 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,原因是浏览器会等待以确定你是否执行双击事件

何时不需要使用

  1. FastClick 不会伴随监听任何桌面浏览器
  2. Android 系统中,在头部 meta 中设置 width=device-width 的Chrome32+ 浏览器不存在300ms 延时,所以,也不需要
    <meta name="viewport" content="width=device-width, initial-scale=1">
  3. 同样的情况也适用于 Android设备(任何版本),在viewport 中设置 user-scalable=no,但这样就禁止缩放网页了
  4. 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-shrinkflex-basis 的简写,默认值为 0 1 auto。后两个属性可选。

flex-grow
flex-shrink
flex-basis

右侧 text 区块 自适应占据剩下的空间,并且内部也采用 flex,使用 flex-direction: column; justify-content: center; 来达到纵向居中排列

recommend 页面 利用 BScroll 滚动

Scroll 初始化但却没有滚动,是因为初始化时机不对,必须保证数据到来,DOM 成功渲染之后 再去进行初始化

可以使用父组件 给 Scrol组件传 :data 数据,Scroll 组件自己 watch 这个 data,有变化就立刻 refesh 滚动

新版本 BScroll 已经自己实现检测 DOM 变化,自动刷新,大部分场景下无需传 data 了

所以也就 无需监听 img 的 onload 事件 然后执行 滚动刷新 了

<img @load="loadImage" class="needsclick" :src="item.picUrl">
loadImage() {
  if (!this.checkloaded) {
    this.checkloaded = true
    this.$refs.scroll.refresh()
  }
}

歌手页面 数据重构

歌手页面的结构是 热门、 A-Z 的顺序排列,但抓取的接口数据只是 100条常见的歌手,并且是乱序的,但我们可以利用接口的 Findex 进行数据的重构

首先可以定义一个 map 结构

let map = {
  hot: {
    title: HOT_NAME,
    item: []
  }
}

接着遍历得到的数据,将前10条添加到热门 hot 里

然后查看每条的 Findex ,如果 map[Findex] 没有,创建 map[Findex] push 进新条目,如果 map[Findex] 有,则向其 push 进新条目

list.forEach((item, index) => {
  if (index < HOT_SINGER_LEN) {
    map.hot.item.push(new SingerFormat({
      id: item.Fsinger_mid,
      name: item.Fsinger_name,
    }))
  }
  const key = item.Findex
  if (!map[key]) {
    map[key] = {
      title: key,
      items: []
    }
  }
  map[key].items.push(new SingerFormat({
    id: item.Fsinger_mid,
    name: item.Fsinger_name
  }))
})

这样就得到了一个 符合我们基本预期的 map 结构,但是因为 map 是一个对象,数据是乱序的,Chrome 控制台在展示的时候会对 key 做排序,但实际上我们代码并没有做。

所以还要将其进行排序,这里会用到 数组的 sort 方法,所以我们要先把 map对象 转为 数组

let hot = []
let ret = []
let un = []
for (let key in map) {
  let val = map[key]
  if (val.title.match(/[a-zA-z]/)) {
    ret.push(val)
  } else if (val.title === HOT_NAME) {
    hot.push(val)
  } else {
    un.push(val)
  }
}
ret.sort((a, b) => {
  return a.title.charCodeAt(0) - b.title.charCodeAt(0)
})
return hot.concat(ret, un)

根据 title 字母的 Unicode 编码大小 排序 的(比如:’A’.charCodeAt(0)=65;’B’.charCodeAt(0)=66)然后就a,b,c,d…的顺序了

歌手页面

shortcut 定位

因为 shortcut 整体的高度是不确定的,所以采用的是 top:50% 之后, transform: translateY(-50%); 这样就能动态的根据内容高度而垂直居中

歌手页面 区块与锚点 的联动

点击或滑动 shortcut 不同的锚点 ,自动滚动至相应的标题列表

利用了 BScroll 的 api ,scrollToElement

  • scrollToElement 可以滚动至相应的 index 值的区块

第一次点击触碰 shortcut ,获取点击具体锚点的 index 值,记录触碰位置的 index ,利用 scrollToElement ,滚动至相应 index 的区块

而之后,滑动锚点实现滚动是利用 touchmove 事件,将两次触碰的的位置计算值变成 delta 差值:变成改变后的锚点区块 index 值,再将首次触碰的 index 值 + 改变的 delta 值,再利用 scrollToElement ,滚动至相应的区块

onShortcutTouchStart(e) {
  let anchorIndex = getData(e.target, 'index')  // 获取 点击具体锚点的 index 值
  let firstTouch = e.touches[0]   // 第一次触碰的位置
  this.touch.y1 = firstTouch.pageY  // 保存 第一次触碰的位置的Y值
  this.touch.anchorIndex = anchorIndex  // 保存 第一次触碰时的锚点 index 值
  this._scrollTo(anchorIndex)
},
onShortcutTouchMove(e) {
  let firstTouch = e.touches[0]
  this.touch.y2 = firstTouch.pageY
  let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 // 两次触碰 Y 轴的偏移锚点值
  let anchorIndex = +this.touch.anchorIndex + delta  // 获取 偏移了多少 index 值  ,因为 anchorIndex 是字符串,所以要转成数字再相加
  this._scrollTo(anchorIndex)
},
_scrollTo(index) {
  this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 200)
}
<Scroll class="listview" ref="listview">
    <!--歌手列表-->
    <ul>
      <li v-for="group in data" class="list-group" ref="listGroup">
        <h2 class="list-group-title">{{group.title}}</h2>
        <!--首字母条目-->
        <ul>
          <li v-for="item in group.items" class="list-group-item">
            <img :src="item.avatar" class="avatar">
            <span class="name">{{item.name}}</span>
          </li>
        </ul>
      </li>
    </ul>
    <div class="list-shortcut" @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove">
      <ul>
        <li v-for="(item, index) in shortcutlist" :data-index="index" class="item">
          {{item}}
        </li>
      </ul>
    </div>
</Scroll>

滑动主列表,侧边 shortcut 自动高亮不同锚点

  1. 首先 BScroll 组件 监听滚动事件,并派发事件以供父组件监听,将 pos 值传出去

    if (this.listenScroll) {
      let self = this
      this.scroll.on('scroll', (pos) => { // 实时监测滚动事件,派发事件:Y轴距离
        self.$emit('scroll', pos)
      })
    }
    
  2. 父组件监听到滚动派发的事件

    @scroll="scroll"
    

将 pos.y 存在 this.scrollY

scroll(pos) {
  this.scrollY = pos.y    // 实时获取 BScroll 滚动的 Y轴距离
}
  1. 再用 watch 检测数据的变化,一旦变化,重新计算每个区块的高度列表。再判断当前滚动的 Y轴值 是否落在相应的 group 高度区间,然后更新 currentIndex ,使 shortcut 的锚点高亮
watch: {
  data() {
    // 延时,确保DOM渲染之后执行,通常是nextTick,这里用setTimeout是为了兼容更低
    setTimeout(() => {
      this._calculateHeight()
    }, 20)
  },

  // 这里的 scrollY 是当前组件上的,和 BScroll 的并不是一个
  scrollY(newY) {
  const listHeight = this.listHeight
  // 1. 当滚动至顶部以上
  if (newY > 0) {
    this.currentIndex = 0
    return
  }
  // 2. 当在中间部分滚动,length之所以 -1 是因为 当初高度列表定义必须多一个
  for (let i = 0; i < listHeight.length - 1; i++) {
    let height1 = listHeight[i]
    let height2 = listHeight[i + 1]
    if (-newY >= height1 && -newY < height2) {
      this.currentIndex = i
      this.diff = height2 + newY  // height 上限 - newY 的值
      return
    }
  }
  // 3. 当滚动至底部,且 newY 大于最后一个元素的上限
  this.currentIndex = listHeight.length - 2
  }
}

每个区块的高度列表是 通过 _calculateHeight 函数实现的

_calculateHeight() {
  this.listHeight = []
  const list = this.$refs.listGroup
  let height = 0
  this.listHeight.push(height)
  for (let i = 0; i < list.length; i++) {
    let item = list[i]
    height += item.clientHeight
    this.listHeight.push(height)
  }
}
  1. 最后只要在 li 上绑定class就可以实现不同位置的锚点高亮了
    :class="{'current': currentIndex === index}"
    

这里的 Vue 用法提示:

watchscrollY(newY){}

  1. 当我们在 Vue 里修改了在 data 里定义的变量,就会出发这个变量的 setter,经过一系列的处理,会触发 watch 的回调函数,也就是 scrollY(newY) {} 这里的函数会执行,同时,newY 就是我们修改后的值。
  2. scrollY 是定义在 data 里的,列表滚动的时候,scroll 事件的回调函数里有修改 this.scrollY,所以能 watch 到它的变化。
  3. watch 的回调函数的第一个参数表示变化的新值

滚动固定标题 效果实现

在中间部分滚动时,会不断设置 diff 值,每个区块的高度上限(也就是底部)减去 Y轴偏移的值

this.diff = height2 + newY  // 就是 height 上限 - newY 的值

watch 检测 diff 变化,判断如果 diff>0 且 小于 title 块的高度,设为差值,否则为0

再将 fixed 的 title 块 translate 偏移

diff(newVal) {
  let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
  if (this.fixedTop === fixedTop) return   // 判断如果两个title区块没有碰到,是不会触发 DOM 操作的
  this.fixedTop = fixedTop
  this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
}

歌手详情页

singer page 页面 引入 singer-detail 二级路由

index.js 路由里配置

{
  path: '/singer',
  component: Singer,
  children: [
    {
      path: ':id', // 表示 id 为变量
      component: SingerDetail
    }
  ]
}

singer.vue 里设定跳转路由 this.$router.push({})
html:

<router-view></router-view>

js:

selectSinger(singer){
  this.$router.push({
    path: `/singer/${singer.id}`
  })
}

Vuex

Vuex 教程见: Vuex

通常的流程为:

  1. 定义 state,考虑项目需要的原始数据(最好为底层数据)
  2. getters,就是对原始数据的一层映射,可以只为底层数据做一个访问代理,也可以根据底层数据映射为新的计算数据(相当于 vuex 的计算属性)
  3. 修改数据:mutations,定义如何修改数据的逻辑(本质是函数)。
    在定义 mutations 之前 要先定义 mutation-types (通常为动词+名词)

actions.js 通常是两种操作

  1. 异步操作
  2. 是对mutation的封装,比如一个动作需要触发多个mutation的时候,就可以把多个mutation封装到一个action中,达到调用一个action去修改多个mutation的目的。

歌手页面,数据利用 vuex 传递

1. 首先 listview.vue 检测点击事件,将具体点击的歌手派发出去,以供父组件 singer 监听

selectItem(item) {
  this.$emit('select', item)
},

2. 父组件监听事件执行 selectSinger(singer)

singer.id
SET_SINGER
selectSinger(singer) {
  this.$router.push({
    path: `/singer/${singer.id}`
  })
  this.setSinger(singer)
},

...mapMutations({ // 语法糖,'...'将多个对象注入当前对象
  setSinger: 'SET_SINGER' // 将 this.setSinger() 映射为 this.$store.commit('SET_SINGER')
})

mapMutations (语法糖) 映射 mutations , this.setSinger(singer) 相当于执行 this.$store.commit('SET_SINGER') (singer 为 mutation 的第二个参数)

而 mutations 内 SET_SINGER 的逻辑为

[types.SET_SINGER](state, singer) {
  state.singer = singer
}

3. singer-detail 取 vuex 中存好的数据

computed: {
  ...mapGetters([
    'singer'
  ])
}

getters 内 singer 的逻辑为

singer = state => state.singer

musiclist 与 songlist

滑动 songlist 与背景图的联动

主要是 监听滚动距离,根据不同的距离条件发生不同的效果

mounted() {
  this.imageHeight = this.$refs.bgImage.clientHeight
  this.$refs.list.$el.style.top = `${this.imageHeight}px` // 对于 Vue 组件,this.$refs.xxx 拿到的是 Vue 实例,所以需要再通过 $el 拿到真实的 dom
  this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT
},

watch: {
  scrollY(newY) {
    let translateY = Math.max(this.minTransalteY, newY)   // 最远滚动改变的距离就是 minTransalteY
    let zIndex = 0
    let scale = 1
    const percent = Math.abs(newY / this.imageHeight)

    this.$refs.layer.style.transform = `translate3d(0,${translateY}px,0)`
    this.$refs.layer.style.webkitTransform = `translate3d(0,${translateY}px,0)`
    if (newY < this.minTransalteY) {
      zIndex = 10
      this.$refs.bgImage.style.paddingTop = 0
      this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
    } else {
      this.$refs.bgImage.style.paddingTop = '70%'
      this.$refs.bgImage.style.height = 0
    }
    if (newY > 0) {
      scale = 1 + percent
      zIndex = 10
    }
    this.$refs.bgImage.style.zIndex = zIndex
    this.$refs.bgImage.style.transform = `scale(${scale})`
    this.$refs.bgImage.style.webkitTransform = `scale(${scale})`
  }
}

自动判断浏览器加CSS兼容前缀 prefixStyle

let elementStyle = document.createElement('div').style

let vendor = (() => {
  let transformNames = {
    webkit: 'webkitTransform',
    Moz: 'MozTransform',
    O: 'OTransform',
    ms: 'msTransform',
    standard: 'transform'
  }

  for (let key in transformNames) {
    if (elementStyle[transformNames[key]] !== undefined) return key
  }
  return false
})()

export function prefixStyle(style) {
  if (vendor === false) return false

  if (vendor === 'standard') return style

  return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}
  1. 首先生成基于用户浏览器的div样式
  2. 根据 vendor 供应商定义的不同浏览器前缀,去测试用户浏览器。
    方法就是判断创建的 div 样式是否有相应的前缀样式,如果有,则返回前缀样式的key,也就是需要的 前缀
  3. 通过 prefixStyle 函数,参数为我们需要兼容的样式。如果需要加签注,返回的格式是 前缀 + 首字母大写的样式(应为通常前缀样式为 -webkit-transform-origin ,JS操作时,不能写 - ,可以采用驼峰写法,也就是样式首字母大写)

播放器 player

把播放器组件放在 App.vue 下,因为它是一个跟任何路由都不相关的东西。在任何路由下,它都可以去播放。切换路由并不会影响播放器的播放。

播放器 vuex 设计

点击 歌手/歌单 都会进入详情页,详情页 created() 会根据点击的歌手请求相应的数据,然后利用 _normalizeSongs 将数据整理,其中很重要的函数是 createSong ,生成自定义 song 类,方便以后读取

播放器 图片旋转

animation-play-state

animation-play-state CSS 属性定义一个动画是否运行或者暂停。可以通过查询它来确定动画是否正在运行。另外,它的值可以被设置为暂停和恢复的动画的重放。

恢复一个已暂停的动画,将从它开始暂停的时候,而不是从动画序列的起点开始在动画。

修复BUG:ios下safari与chrome浏览器,animation-play-state样式失效 #60

点击暂停播放的时候,歌曲的图片会继续转动,导致的原因是因为animation-play-state:paused这个样式失效了

修复具体代码

核心代码:

/**
 * 计算内层Image的transform,并同步到外层容器
 * @param wrapper
 * @param inner
 */
syncWrapperTransform(wrapper, inner) {
  if (!this.$refs[wrapper]) return

  let imageCdWrapper = this.$refs[wrapper]
  let image = this.$refs[inner]
  let wTransform = getComputedStyle(imageCdWrapper)[transform]
  let iTransform = getComputedStyle(image)[transform]
  imageCdWrapper.style[transform] = wTransform === 'none' ? iTransform : iTransform.concat(' ', wTransform)
}

解决快速切换歌曲引发的错误

这个错误是由于切换的太快,歌曲并未获取到播放地址,而提前播放

利用了H5新api: canplay

当终端可以播放媒体文件时触发该canplay事件,估计加载足够的数据来播放媒体直到其结束,而不必停止以进一步缓冲内容。

利用这个api,在audio上监听 canplay 派发的事件,做成标志位

后来 api 改至 playing

播放器 进度条 功能

normal 的长形进度条

在 progress 上监听 touchstart , touchmove , touchend 三个事件

  • touchstart: 获取第一次点击的横坐标和已播放的进度条长度
  • touchmove: 获取移动后的横坐标,并定义 delta 为 移动后坐标 - 第一次点击的横坐标
    设置 偏移量 offsetWidth 为 已播放的进度条长度 + delta
    在去设置 progress 和 progressBtn 的宽度和transform 量都为 offsetWidth
  • touchend: 一些组件特有的逻辑,和进度条不太相关暂不赘述

而点击任意位置,移动进度按钮,则是通过为 progress 进度条添加点击事件

progressClick(e) {
  this._offset(e.offsetX - progressBtnWidth / 2)
  this._triggerPercent()
}

mini 的圆形进度条

利用了 SVG 实现,其中有两个圆,一个是背景圆形,另一个为已播放的圆形进度

<div class="progress-circle">
  <svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
    <circle class="progress-background" r="50" cx="50" cy="50" fill="transparent"/>
    <circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent"    :stroke-dasharray="dashArray"
    :stroke-dashoffset="dashOffset"/>
  </svg>
  <slot></slot>
</div>

修复进度条的 BUG

迷你播放器暂停状态,进入全屏,按钮在进度条最左边

  • 原因:当播放器最小化的时候,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项目总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

OKR工作法

OKR工作法

克里斯蒂娜•沃特克 (Christina Wodtke) / 明道团队 / 中信出版社 / 2017-9-1 / CNY 42.00

《OKR工作法》讲述了一种风靡硅谷科技企业的全新工作模式。 如何激励不同的团队一起工作,全力以赴去实现一个有挑战性的目标? 硅谷的两个年轻人汉娜和杰克,像很多人一样,在萌生了一个创意后,就走上创业之路。但是,很快他们发现好的想法远远不够,必须还有一套适合的管理方法确保梦想能实现。为了让创业团队生存下来,汉娜和杰克遭受了内心的苦苦挣扎和煎熬。他们患上“新奇事物综合症”,什么都想做,导致无......一起来看看 《OKR工作法》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码