Preface#
This project is something I came up with one day while listening to music in my dormitory. It has been five years since I registered on Netease Cloud Music in 2016. Although this platform has been increasingly disappointing, I have developed an emotional attachment to it after listening for five years. So, during this vacation, I spent nearly a month working on this project. As for why I chose to create a mobile page, it's because I used my phone to listen to music at that time... So, I imitated the UI of the Android version of Netease Cloud Music. Although not all functionalities have been implemented, the core playback page has been completed. This blog post is also a record of the implementation process and some challenges of this project.
The project is implemented using Vue + Typescript + Vuetify UI.
Project link: CloudMusic
Implemented functionalities:
- Login
- Get playlist
- Create playlist
- Delete/unfavorite playlist
- Play songs
- Rankings
- Daily recommendations
- Recommended playlists
Project screenshots:
Playback#
This can be considered the core functionality because a music platform cannot be considered good if it cannot play music.
Firstly, the song selection can come from playlists, the carousel on the discovery page, or the selection on the playback page. The control of playing and pausing songs can be done through the playback tab at the bottom of other pages or through the playback page itself. Since using Prop and Emit would be too messy and complicated, I used Vuex instead.
Vuex is a state management pattern library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
Basically, it extracts the shared state of the components we need and manages it as a global singleton. We can then modify the state or value of any component using Vuex. So, I passed the selected song ID and the current playlist information ID into Vuex. In the playback page, I only need to listen for changes in the song ID in Vuex to play a new song, and listen for changes in the pause state in Vuex to control the playback or pause of the audio component.
Playback Progress#
I used the slider component from Vuetify UI for the progress bar and set its value using v-model.
In the audio component, I used @timeupdate to listen to the current playback timestamp of the audio. Then, I divided it by the total playback duration and multiplied it by 100 to get the current playback progress percentage. I assigned this value to the slider component. The time on the left side of the progress bar is also converted from the current playback timestamp to time.
However, there is a problem here. I need to be able to modify the playback progress by moving the slider. In the method bound to @timeupdate, I only assign the progress value to the slider component unidirectionally. And this method keeps running while the music is playing. So, no matter how I move the slider, it immediately jumps to the current playback position. Therefore, I need to add a method to listen for whether the slider is being moved. When the slider is being moved, I don't assign the current playback progress to the slider. Instead, when I release the slider, I convert the current progress of the slider to a timestamp and assign it back to the playback time of the audio.
My solution was to introduce @mousedown and @mouseup to the slider component. When the mouse is pressed, I assign a variable to true, and when it is released, I assign it to false. I modified the method bound to @timeupdate so that it only assigns the value to the slider component when this variable is false. However, I still need to modify the current playback time when sliding the slider. So, I introduced @change to the slider component. When the value of the slider is manually changed, this method is triggered. In this method, I modify the playback progress of the audio.
Lyrics Implementation#
The lyrics obtained from the backend are in the form of a string, with each line of lyrics separated by \n. So, by splitting the string using \n, I can get each line of lyrics. Each line of lyrics is divided by square brackets, and finally, the time is converted to a timestamp. The lyrics and timestamp are then stored as a class in an array.
I set an index to keep track of which line of lyrics it is. By getting the height of the lyrics component using offsetHeight, I can determine the position of the line in the lyrics where the line is. By multiplying the index by the height of each line of lyrics, I can determine whether to scroll.
Basically, I set the outer div to have an overflow hidden and use margin-top to achieve a scrolling effect. The value of margin-top is determined by subtracting the height of the line from the index multiplied by the height of each line of lyrics. The transition property can be used to set the duration of the change.
For example: transition: margin-top 1s;
This means that the margin-top property will be completed within one second.
About Vue Custom Directives#
During the development process, I wanted to implement a feature where clicking outside a certain component would hide it. In other words, when the click is not on the component, a certain method would be executed.
When I searched for information, I found that Vue does not have such a directive. However, I can create a custom directive to achieve this functionality.
First, let's take a look at the hook functions of a custom directive object:
- bind: Called only once, when the directive is first bound to the element. Initialization settings can be done here.
- inserted: Called when the bound element is inserted into its parent node (but not necessarily inserted into the document yet).
- update: Called when the VNode of the component that the directive is on has been updated. The value of the directive may or may not have changed. You can compare the old and new values to ignore unnecessary template updates (see detailed hook function parameters below).
- componentUpdated: Called after the VNode and its child VNodes of the directive's component have been updated.
- unbind: Called only once, when the directive is unbound from the element.
The hook functions have the following parameters:
- el: The element the directive is bound to. It can be used to directly manipulate the DOM.
- binding: An object containing the following properties:
- name: The name of the directive, without the v- prefix.
- value: The value bound to the directive. For example, in v-my-directive="1 + 1", the bound value is 2.
- oldValue: The previous value bound to the directive. Only available in the update and componentUpdated hooks. It is available regardless of whether the value has changed.
- expression: The string expression of the directive. For example, in v-my-directive="1 + 1", the expression is "1 + 1".
- arg: The argument passed to the directive, optional. For example, in v-my-directive, the argument is "foo".
- modifiers: An object containing the modifiers. For example, in v-my-directive.foo.bar, the modifiers object is { foo: true, bar: true }.
- vnode: The virtual node generated by Vue's compilation. Refer to the VNode API for more details.
- oldVnode: The previous virtual node. Only available in the update and componentUpdated hooks.
The above information is from the Vue documentation. Although there are many hook functions and variables, they are all optional. Only use the ones that are needed. In implementing this feature, I only used bind and unbind, and only used el and binding from the variables.
import {DirectiveOptions} from "vue";
// Custom directive clickoutside, executes the bound method when clicking outside the current element
const clickoutside: DirectiveOptions = {
// Initialize the directive
bind (el: any, binding: any, vnode) {
function documentHandler (e: any) {
// Check if the clicked element is itself, if so, return
if (el.contains(e.target)) {
return false
}
// Check if the directive is bound to a function
if (binding.expression) {
// If a function is bound, call that function
binding.value(e)
}
}
// Bind a private variable to the current element to facilitate event unbinding in unbind
el.vueClickOutside = documentHandler
document.addEventListener('click', documentHandler)
},
unbind (el: any, binding) {
// Unbind the event listener
document.removeEventListener('click', el.vueClickOutside)
delete el.vueClickOutside
}
}
export default clickoutside;
Import where needed:
@Component({
directives:{
clickoutside,
}
})
Usage (when clicking outside the div, call the outside method):
<div v-clickoutside="outside"></div>
After implementing this, I realized that Vuetify already integrated such a directive, which I could have used directly. This is the consequence of not reading the documentation properly.
Finally⭐️#
🙏 Thanks to the Netease Cloud Music API for providing the interface and documentation.
Also, this project was made for personal learning purposes. For normal use, please go to Netease Cloud Music👈