Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第六章 歌手详情页开发)

Vue 2.0 高级实战-开发移动端音乐WebApp 课程笔记(第六章 歌手详情页开发)

子路由配置以及转场动画实现

  1. 添加组件singer-detail

    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
    <template>
    <transition name="slide">
    <div class="singer-detail"></div>
    </transition>
    </template>

    <script>
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"

    .singer-detail
    position: fixed
    z-index: 100
    top: 0
    left: 0
    right: 0
    bottom: 0
    background: $color-background
    .slide-enter-active, .slide-leave-active
    transition: all 0.3s
    .slide-enter, .slide-leave-to
    transform: translate3d(100%, 0, 0)
    </style>
  2. 路由配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    path: '/singer',
    component: Singer,
    children: [
    {
    path: ':id',
    component: SingerDetail
    }
    ]
    }
  3. singer.vue添加<view-router>

  4. listview.vue添加点击事件

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

基础组件不会有业务逻辑, 仅仅派发事件

  1. singer.vue添加事件
    1
    2
    3
    4
    5
    selectSinger(singer) {
    this.$router.push({
    path: `/singer/${singer.id}`
    })
    },

初识 Vuex

学习一个技术栈时, 首先通过3w方式

  1. vuex是什么
    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
  2. vuex解决什么问题
    多个组件的数据共享
    路由间的复杂数据传递
  3. 怎么使用vuex

Vuex 初始化及歌手数据的配置

创建目录

  • store
    • index.js 入口
    • state.js 所有状态
    • mutations.js
    • mutations-type.js mutations相关字符串常量
    • actions.js 异步操作, mutations封装
    • getter.js 获取state映射

为什么要有mutations-type

  1. 书写方便
  2. 方便lint工具检测

  3. index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import Vue from 'vue'
    import Vuex from 'vuex'
    import * as actions from './actions'
    import * as getters from './getters'
    import state from './state'
    import mutations from './mutations'
    import createLogger from 'vuex/dist/logger'

    Vue.use(Vuex)

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

    export default new Vuex.Store({
    actions,
    getters,
    state,
    mutations,
    strict: debug,
    plugins: debug ? [createLogger()] : []
    })
  4. state.js

    1
    2
    3
    4
    5
    const state = {
    singer: {}
    }

    export default state
  5. mutation-types.js

    1
    export const SET_SINGER = 'SET_SINGER'
  6. mutations

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import * as types from './mutation-types'

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

    export default matutaions
  7. getters.js

    1
    export const singer = state => state.singer
  8. singer.vue 设置数据

    1
    2
    3
    4
    5
    6
    7
    import {mapMutations} from 'vuex'

    methods: {
    ...mapMutations({
    setSinger: 'SET_SINGER'
    })
    }
  9. singer.vue 设置数据

    1
    2
    3
    4
    5
    6
    7
    import {mapMutations} from 'vuex'

    methods: {
    ...mapMutations({
    setSinger: 'SET_SINGER'
    })
    }

歌手详情数据抓取

  1. api/singer.js 添加方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export function getSingerDetail(singerId) {
    const url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg'

    const data = Object.assign({}, commonParams, {
    hostUin: 0,
    needNewCode: 0,
    platform: 'yqq',
    order: 'listen',
    begin: 0,
    num: 80,
    songstatus: 1,
    singermid: singerId
    })

    return jsonp(url, data, options)
    }

歌手详情数据处理和Song类的封装

创建class Song

1
2
3
4
5
6
7
8
9
10
11
12
export default class Song {
constructor({id, mid, singer, name, album, duration, image, url}) {
this.id = id
this.mid = mid
this.singer = singer
this.name = name
this.album = album
this.duration = duration
this.image = image
this.url = url
}
}

设计成类的好处

  1. 集中一处维护
  2. 面向对象的方式, 扩展性强

添加工厂方法createSong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function createSong(musicData) {
return new Song({
id: musicData.songid,
mid: musicData.songmid,
singer: filterSinger(musicData.singer),
name: musicData.songname,
album: musicData.albumname,
duration: musicData.interval,
image: `https://y.gtimg.cn/music/photo_new/T002R300x300M000${musicData.albummid}.jpg?max_age=2592000`,
url: `http://ws.stream.qqmusic.qq.com/${musicData.songid}.m4a?fromtag=46`
})
}

export function filterSinger(singer) {
let ret = []
if (!singer) {
return ''
}
singer.forEach((s) => {
ret.push(s.name)
})
return ret.join('/')
}

music-list 组件开发

  1. 添加业务组件music-list.vue

    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    <template>
    <div class="music-list">
    <div class="back">
    <i class="icon-back"></i>
    </div>
    <h1 class="title" v-html="title"></h1>
    <div class="bg-image" :style="bgStyle" ref="bgImage">
    <div class="play-wrapper">
    <div ref="playBtn" v-show="songs.length>0" class="play">
    <i class="icon-play"></i>
    <span class="text">随机播放全部</span>
    </div>
    </div>
    <div class="filter" ref="filter"></div>
    </div>
    </div>
    </template>

    <script>

    export default {
    props: {
    bgImage: {
    type: String,
    default: ''
    },
    songs: {
    type: Array,
    default: []
    },
    title: {
    type: String,
    default: ''
    }
    },
    computed: {
    bgStyle() {
    return `background-image:url(${this.bgImage})`
    }
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    @import "~common/stylus/mixin"
    .music-list
    position: fixed
    z-index: 100
    top: 0
    left: 0
    bottom: 0
    right: 0
    background: $color-background
    .back
    position absolute
    top: 0
    left: 6px
    z-index: 50
    .icon-back
    display: block
    padding: 10px
    font-size: $font-size-large-x
    color: $color-theme
    .title
    position: absolute
    top: 0
    left: 10%
    z-index: 40
    width: 80%
    no-wrap()
    text-align: center
    line-height: 40px
    font-size: $font-size-large
    color: $color-text
    .bg-image
    position: relative
    width: 100%
    height: 0
    padding-top: 70%
    transform-origin: top
    background-size: cover
    .play-wrapper
    position: absolute
    bottom: 20px
    z-index: 50
    width: 100%
    .play
    box-sizing: border-box
    width: 135px
    padding: 7px 0
    margin: 0 auto
    text-align: center
    border: 1px solid $color-theme
    color: $color-theme
    border-radius: 100px
    font-size: 0
    .icon-play
    display: inline-block
    vertical-align: middle
    margin-right: 6px
    font-size: $font-size-medium-x
    .text
    display: inline-block
    vertical-align: middle
    font-size: $font-size-small
    .filter
    position: absolute
    top: 0
    left: 0
    width: 100%
    height: 100%
    background: rgba(7, 17, 27, 0.4)
    .bg-layer
    position: relative
    height: 100%
    background: $color-background
    .list
    position: fixed
    top: 0
    bottom: 0
    width: 100%
    background: $color-background
    .song-list-wrapper
    padding: 20px 30px
    .loading-container
    position: absolute
    width: 100%
    top: 50%
    transform: translateY(-50%)
    </style>
  2. 添加基础组件song-list.vue

    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    <template>
    <div class="song-list">
    <ul>
    <li class="item" v-for="song in songs">
    <div class="content">
    <h2 class="name">{{song.name}}</h2>
    <p class="desc">{{getDesc(song)}}</p>
    </div>
    </li>
    </ul>
    </div>
    </template>

    <script>
    export default {
    props: {
    songs: {
    type: Array,
    default: []
    }
    },
    methods: {
    getDesc(song) {
    return `${song.singer}·${song.album}`
    }
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    @import "~common/stylus/mixin"
    .song-list
    .item
    display: flex
    align-items: center
    box-sizing: border-box
    height: 64px
    font-size: $font-size-medium
    .content
    flex: 1
    line-height: 20px
    overflow: hidden
    .name
    no-wrap()
    color: $color-text
    .desc
    no-wrap()
    margin-top: 4px
    color: $color-text-d
    </style>
  3. dom.js 添加 prefixStyle

    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
    // js中的css前缀补全
    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)
    }
  4. 完善music-list.vue

    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    <template>
    <div class="music-list">
    <div class="back" @click="back">
    <i class="icon-back"></i>
    </div>
    <h1 class="title" v-html="title"></h1>
    <div class="bg-image" :style="bgStyle" ref="bgImage">
    <div class="play-wrapper">
    <div ref="playBtn" v-show="songs.length>0" class="play">
    <i class="icon-play"></i>
    <span class="text">随机播放全部</span>
    </div>
    </div>
    <div class="filter" ref="filter"></div>
    </div>
    <div class="bg-layer" ref="layer"></div>
    <scroll :data="songs" @scroll="scroll"
    :listen-scroll="listenScroll" :probe-type="probeType" class="list" ref="list">
    <div class="song-list-wrapper">
    <song-list :songs="songs"></song-list>
    </div>
    <div v-show="!songs.length" class="loading-container">
    <loading></loading>
    </div>
    </scroll>
    </div>
    </template>

    <script>
    import Scroll from 'base/scroll/scroll'
    import Loading from 'base/loading/loading'
    import SongList from 'base/song-list/song-list'
    import {prefixStyle} from 'common/js/dom'

    const RESERVED_HEIGHT = 40
    const transform = prefixStyle('transform')
    const backdrop = prefixStyle('backdrop-filter')

    export default {
    props: {
    bgImage: {
    type: String,
    default: ''
    },
    songs: {
    type: Array,
    default: []
    },
    title: {
    type: String,
    default: ''
    }
    },
    data() {
    return {
    scrollY: 0
    }
    },
    computed: {
    bgStyle() {
    return `background-image:url(${this.bgImage})`
    }
    },
    created() {
    this.probeType = 3
    this.listenScroll = true
    },
    mounted() {
    this.imageHeight = this.$refs.bgImage.clientHeight
    this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT
    this.$refs.list.$el.style.top = `${this.imageHeight}px`
    },
    methods: {
    scroll(pos) {
    this.scrollY = pos.y
    },
    back() {
    this.$router.back()
    }
    },
    watch: {
    scrollY(newVal) {
    let translateY = Math.max(this.minTransalteY, newVal)
    let scale = 1
    let zIndex = 0
    let blur = 0
    // 计算图片放大比例
    const percent = Math.abs(newVal / this.imageHeight)
    if (newVal > 0) {
    scale = 1 + percent
    zIndex = 10
    } else {
    // 计算模糊比例
    blur = Math.min(20, percent * 20)
    }

    this.$refs.layer.style[transform] = `translate3d(0,${translateY}px,0)`
    this.$refs.filter.style[backdrop] = `blur(${blur}px)`
    if (newVal < this.minTransalteY) {
    // 向上滑动超过minTransalteY, 改变图片zIndex和高度, 按钮消失
    zIndex = 10
    this.$refs.bgImage.style.paddingTop = 0
    this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`
    this.$refs.playBtn.style.display = 'none'
    } else {
    // 反之, 重置
    this.$refs.bgImage.style.paddingTop = '70%'
    this.$refs.bgImage.style.height = 0
    this.$refs.playBtn.style.display = ''
    }
    this.$refs.bgImage.style[transform] = `scale(${scale})`
    this.$refs.bgImage.style.zIndex = zIndex
    }
    },
    components: {
    Scroll,
    Loading,
    SongList
    }
    }
    </script>

    <style scoped lang="stylus" rel="stylesheet/stylus">
    @import "~common/stylus/variable"
    @import "~common/stylus/mixin"
    .music-list
    position: fixed
    z-index: 100
    top: 0
    left: 0
    bottom: 0
    right: 0
    background: $color-background
    .back
    position absolute
    top: 0
    left: 6px
    z-index: 50
    .icon-back
    display: block
    padding: 10px
    font-size: $font-size-large-x
    color: $color-theme
    .title
    position: absolute
    top: 0
    left: 10%
    z-index: 40
    width: 80%
    no-wrap()
    text-align: center
    line-height: 40px
    font-size: $font-size-large
    color: $color-text
    .bg-image
    position: relative
    width: 100%
    height: 0
    padding-top: 70%
    transform-origin: top
    background-size: cover
    .play-wrapper
    position: absolute
    bottom: 20px
    z-index: 50
    width: 100%
    .play
    box-sizing: border-box
    width: 135px
    padding: 7px 0
    margin: 0 auto
    text-align: center
    border: 1px solid $color-theme
    color: $color-theme
    border-radius: 100px
    font-size: 0
    .icon-play
    display: inline-block
    vertical-align: middle
    margin-right: 6px
    font-size: $font-size-medium-x
    .text
    display: inline-block
    vertical-align: middle
    font-size: $font-size-small
    .filter
    position: absolute
    top: 0
    left: 0
    width: 100%
    height: 100%
    background: rgba(7, 17, 27, 0.4)
    .bg-layer
    position: relative
    height: 100%
    background: $color-background
    .list
    position: fixed
    top: 0
    bottom: 0
    width: 100%
    background: $color-background
    .song-list-wrapper
    padding: 20px 30px
    .loading-container
    position: absolute
    width: 100%
    top: 50%
    transform: translateY(-50%)
    </style>