Let’s take a music tour: Music APP development

Recently, the term “net cloud suppression” has become popular. I do not know whether you will still silently turn over the comments of netease Cloud music and think of the past “unbearable”? As a front-end worker, if you can tap code while listening to songs at work, it is already very gratifying. Therefore, after learning the development of this music APP, let’s enter this music journey together!

The overall architecture

The directory structure

├ ─ ─ public │ ├ ─ ─ index. The HTML / / project entry documents └ ─ ─ SRC / / source directory ├ ─ ─ the API interface file / / project ├ ─ ─ assets / / where static files ├ ─ ─ │ components / / public components ├─ Add-song │ ├─ Heavy Metal │ ├─ Heavy Metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy metal │ ├─ Heavy Metal Guitar School │ ├─ Heavy Metal Guitar School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School │ ├─ Heavy metal Flag School ├─ Progress-bar │ ├─ Progress-bar │ ├─ Scroll │ ├─ Search │ ├─ Search - the list / / search list component │ ├ ─ ─ the slider / / by component │ ├ ─ ─ song list / / song list component │ ├ ─ ─ suggest / / search results component │ ├ ─ ─ switches / / menu to switch components │ ├ ─ ─ the TAB / / at the top of the navigation bar component │ └ ─ ─ top tip / / top tip component ├ ─ ─ the router / / routing file ├ ─ ─ store / / vuex state management files ├ ─ ─ style / / storage style file │ ├ ─ ─ fonts / / font file │ └ ─ ─ stylus / / global stylus style file ├ ─ ─ utils / / storing way global file └ ─ ─ views/store/page ├ ─ ─ disc/song/single details page ├ ─ ─ rank / / page ranking ├ ─ ─ How/page/recommend ├ ─ ─ the search / / search page ├ ─ ─ singer/page/singer ├ ─ ─ singer - detail / / singer details ├ ─ ─ top the list/version/top page └ ─ ─ the user - center / / The user centerCopy the code

The UI design draft

Overall Architecture

Encapsulation of functional components

Slider (slider component)

In the usual business, the rotation chart is often seen, especially in the mobile terminal and small programs, need to provide more customized requirements, such as automatic rotation, rotation interval, provided pictures and so on

<template> <div class="slider" ref="slider"> <div class="slider-group" ref="sliderGroup"> <slot></slot> </div> <div class="dots"> <span class="dot" :class="{active: currentPageIndex === index }" v-for="(item, index) in dots" :key="index" ></span> </div> </div> </template> <script> import { addClass } from 'utils/dom' import BScroll from 'better-scroll' export default {name: 'slider', props: {loop: {// Type: Boolean, default: true}, interval: {// Type: Number, default: 4000 } }, data() { return { dots: [], currentPageIndex: 0}}, mounted () { this.$nextTick(() => { this._setSliderWidth() this._initDots() this._initSlider() if (this.autoPlay) { this._play() } }) window.addEventListener('resize', () => { if (! this.slider) { return } this._setSliderWidth(true) this.slider.refresh() }) }, If (this.autoplay) {this._play()}}, if (this.autoplay) {this._play()}}, deactivated() { clearTimeout(this.timer) window.removeEventListener('resize', () => { if (! this.slider) { return } this._setSliderWidth(true) this.slider.refresh() }) }, beforeDestroy() { clearTimeout(this.timer) window.removeEventListener('resize', () => { if (! this.slider) { return } this._setSliderWidth(true) this.slider.refresh() }) }, methods: { _setSliderWidth(isResize) { this.children = this.$refs.sliderGroup.children let width = 0 let sliderWidth = this.$refs.slider.clientWidth for (let i = 0; i < this.children.length; i++) { let child = this.children[i] addClass(child, 'slider-item') child.style.width = sliderWidth + 'px' width += sliderWidth } if (this.loop && ! isResize) { width += 2 * sliderWidth } this.$refs.sliderGroup.style.width = width + 'px' }, _initSlider() { this.slider = new BScroll(this.$refs.slider, { scrollX: true, scrollY: false, momentum: false, snap: True, snapLoop: this.loop, snapThreshold: 0.3, snapSpeed: 400 }) this.slider.on('scrollEnd', () => { let pageIndex = this.slider.getCurrentPage().pageX if (this.loop) { pageIndex -= 1 } this.currentPageIndex = pageIndex if (this.autoPlay) { this._play() } }) this.slider.on('beforeScrollStart', () => { if (this.autoPlay) { clearTimeout(this.timer) } }) }, _initDots() { this.dots = new Array(this.children.length) }, _play() { let pageIndex = this.currentPageIndex + 1 if (this.loop) { pageIndex += 1 } this.timer = setTimeout(() => { this.slider.goToPage(pageIndex, 0, 400) }, this.interval) } } } </script> <style scoped lang="stylus"> @import '~@/style/stylus/variable' .slider { min-height: 1px; .slider-group { position: relative; overflow: hidden; white-space: nowrap; .slider-item { float: left; box-sizing: border-box; overflow: hidden; text-align: center; a { display: block; width: 100%; overflow: hidden; text-decoration: none; } img { display: block; width: 100%; } } } .dots { position: absolute; right: 0; left: 0; bottom: 12px; text-align: center; font-size: 0; .dot { display: inline-block; margin: 0 4px; width: 8px; height: 8px; border-radius: 50%; background: $color-text-l; &.active { width: 20px; border-radius: 5px; background: $color-text-ll; } } } } </style>Copy the code

A small bug that you often encounter when encapsulating a multicast component is that when you leave the page of the current multicast component and come back, the multicast graph will flicker or twitch. The main reason is that js will always be executed, so you need to cache the component, which is wrapped with the ‘keep-alive’ component.

Scroll component

Scrolling lists are also used frequently in apps:

<template>
  <div ref="wrapper">
    <slot></slot>
  </div>
</template>

<script>
import BScroll from 'better-scroll'

export default {
  props: {
    probeType: {
      type: Number,
      default: 1
    },
    click: {
      type: Boolean,
      default: true
    },
    listenScroll: {
      type: Boolean,
      default: false
    },
    data: {
      type: Array,
      default: null
    },
    pullup: {
      type: Boolean,
      default: false
    },
    beforeScroll: {
      type: Boolean,
      default: false
    },
    refreshDelay: {
      type: Number,
      default: 20
    }
  },
  mounted() {
    this.$nextTick(() => {
      this._initScroll()
    })
  },
  methods: {
    _initScroll() {
      this.scroll = new BScroll(this.$refs.wrapper, {
        probeType: this.probeType,
        click: this.click
      })

      if (this.listenScroll) {
        let me = this
        this.scroll.on('scroll', (pos) => {
          me.$emit('scroll', pos)
        })
      }

      if (this.pullup) {
        this.scroll.on('scrollEnd', () => {
          if (this.scroll.y <= (this.scroll.maxScrollY + 50)) {
            this.$emit('scrollToEnd')
          }
        })
      }

      if (this.beforeScroll) {
        this.scroll.on('beforeScrollStart', () => {
          this.$emit('beforeScroll')
        })
      }
    },
    disable() {
      this.scroll && this.scroll.disable()
    },
    enable() {
      this.scroll && this.scroll.enable()
    },
    refresh() {
      this.scroll && this.scroll.refresh()
    },
    scrollTo() {
      this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
    },
    scrollToElement() {
      this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
    }
  },
  watch: {
    data() {
      setTimeout(() => {
        this.refresh()
      }, this.refreshDelay)
    }
  }
}
</script>
Copy the code

Progress-bar (Progress bar component)

<template> <div class="progress-bar" ref="progressBar" @click="progressClick"> <div class="bar-inner"> <div class="progress" ref="progress"></div> <div class="progress-btn-wrapper" ref="progressBtn" @touchstart.prevent="progressTouchStart" @touchmove.prevent="progressTouchMove" @touchend="progressTouchEnd" > <div class="progress-btn"></div> </div> </div> </div> </template> <script> import { prefixStyle } from 'utils/dom' const progressBtnWidth = 16 const transform = prefixStyle('transform') export default { props: { percent: { type: Number, default: 0 } }, created() { this.touch = {} }, methods: { progressTouchStart(e) { this.touch.initiated = true this.touch.startX = e.touches[0].pageX this.touch.left = this.$refs.progress.clientWidth }, progressTouchMove(e) { if (! this.touch.initiated) { return } const deltaX = e.touches[0].pageX - this.touch.startX const offsetWidth = Math.min( this.$refs.progressBar.clientWidth - progressBtnWidth, Math.max(0, this.touch.left + deltaX) ) this._offset(offsetWidth) }, progressTouchEnd() { this.touch.initiated = false this._triggerPercent() }, progressClick(e) { const rect = this.$refs.progressBar.getBoundingClientRect() const offsetWidth = e.pageX - rect.left This._offset (offsetWidth) // this._offset(e.offsetx) this._triggerPercent()}, _triggerPercent() { const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth const percent = this.$refs.progress.clientWidth / barWidth this.$emit('percentChange', percent) }, _offset(offsetWidth) { this.$refs.progress.style.width = `${offsetWidth}px` this.$refs.progressBtn.style[ transform ] = 'translate3d(${offsetWidth}px,0,0)'}}, watch: {percent(newPercent) {if (newPercent >= 0 &&! this.touch.initiated) { const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth const offsetWidth = newPercent * barWidth this._offset(offsetWidth) } } } } </script> <style scoped lang="stylus"> @import '~@/style/stylus/variable' .progress-bar { height: 30px; .bar-inner { position: relative; top: 13px; height: 4px; Background: Rgba (0, 0, 0, 0.3); .progress { position: absolute; height: 100%; background: $color-theme; } .progress-btn-wrapper { position: absolute; left: -8px; top: -13px; width: 30px; height: 30px; .progress-btn { position: relative; top: 7px; left: 7px; box-sizing: border-box; width: 16px; height: 16px; border: 3px solid $color-text; border-radius: 50%; background: $color-theme; } } } } </style>Copy the code

Progress-circle (Circular progress bar component)

<template> <div class="progress-circle"> < SVG :width="radius" :height="radius" viewBox="0 0 100 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> </template> <script> export default { props: { radius: { type: Number, default: 100 }, percent: { type: Number, default: 0 } }, data() { return { dashArray: Math.PI * 100 } }, computed: { dashOffset() { return (1 - this.percent) * this.dashArray } } } </script> <style scoped lang="stylus"> @import '~@/style/stylus/variable' .progress-circle { position: relative; circle { stroke-width: 8px; transform-origin: center; &. Progress - background {transform: scale (0.9); stroke: $color-theme-d; } &. Progress-bar {transform: scale(0.9) rotate(-90deg); stroke: $color-theme; } } } </style>Copy the code

SVG’s Circle tag is used here to display progress using stroke-Dasharray and stroke-Dashoffset properties. To learn more about these two properties, click here.

Left and right menu linkage components

Better Scroll is also used here to encapsulate

Vuex design

As can be seen from the figure above, vuex probably needs these global states for song playback:

Const state = {playing: false, // Playing state fullScreen: false, // Whether playList: [], // Sequence/sequenceList: [], // Playlist mode: playmode. sequence, // Playlist mode: playmode. sequence, // Playlist mode: playmode. sequence, // Playlist mode: playmode. sequence, // Playlist mode: playmode. sequence, //Copy the code

CurrentSong can be calculated using playList and currentIndex:

const getters = {
  currentSong: state => state.playlist[state.currentIndex] || {}
}
Copy the code

When we click on a song to play, we perform multiple mutations:

selectPlay ({ commit, state }, {list, index}) { commit(types.SET_SEQUENCE_LIST, List) // change suquenceList if (state.mode === playmode. random) {let randomList = shuffle(list) // shuffle is a random method Commit (types.set_playlist, randomList) Set playList to randomList index = findIndex(randomList, List [index])} else {commit(types.set_playlist, Set playList to list} commit(types.set_current_index, Commit (types.set_full_screen, true) commit(types.set_playing_state, True) // Set playback state}Copy the code

What do we need to do when we need to insert or remove another song from a playlist?

const actions = { randomPlay ({commit}, {list}) { commit(types.SET_PLAY_MODE, playMode.random) commit(types.SET_SEQUENCE_LIST, list) let randomList = shuffle(list) commit(types.SET_PLAYLIST, randomList) commit(types.SET_CURRENT_INDEX, 0) commit(types.SET_FULL_SCREEN, true) commit(types.SET_PLAYING_STATE, true) }, insertSong ({commit, state}, Song) {let playlist = state.playlist.slice() // Make a shallow copy of playlist let sequenceList = state.sequencelist. Slice () // SequenceList let currentIndex = state.currentIndex // record the currentSong let currentSong = playlist[currentIndex] // Let fpIndex = findIndex(playList, song) // Playlist.splice (currentIndex, 0, If (currentIndex > fpIndex) {playlist.splice(fpIndex) {playlist.splice(fpIndex, 1) currentIndex-- } else { playlist.splice(fpIndex + 1, 1) } } let currentSIndex = findIndex(sequenceList, currentSong) + 1 let fsIndex = findIndex(sequenceList, song) sequenceList.splice(currentSIndex, 0, song) if (fsIndex > -1) { if (currentSIndex > fsIndex) { sequenceList.splice(fsIndex, 1) } else { sequenceList.splice(fsIndex + 1, 1) } } commit(types.SET_PLAYLIST, playlist) commit(types.SET_SEQUENCE_LIST, sequenceList) commit(types.SET_CURRENT_INDEX, CurrentIndex) commit(types.set_full_screen, true) commit(types.set_playing_state, true)},  deleteSong ({commit, state}, song) { let playlist = state.playlist.slice() let sequenceList = state.sequenceList.slice() let currentIndex = state.currentIndex let pIndex = findIndex(playlist, song) playlist.splice(pIndex, 1) let sIndex = findIndex(sequenceList, song) sequencelist.splice (sIndex, 1) sequencelist.splice (sIndex, 1) sequencelist.splice (sIndex, 1) If current play songs index need to minus one (currentIndex > pIndex | | currentIndex = = = playlist. Length) {currentIndex -} commit(types.SET_PLAYLIST, playlist) commit(types.SET_SEQUENCE_LIST, sequenceList) commit(types.SET_CURRENT_INDEX, currentIndex) if (! Playlist.length) {// If the list has no songs, Commit (types.set_playing_state, false)} else {commit(types.set_playing_state, true)}}}Copy the code

It can be seen from this that when a global variable will lead to changes in multiple variables, we need to have a global concept. Considering various possible situations, it is often better to design VUEX first and then put data into the page than to consider VUEX while writing the page.

conclusion

In fact, I have seen a lot of tutorials about music APP projects, and their functions are almost the same, but there are also many details to deal with, so you will have a new experience after knocking the code by yourself.