preface

Intermittently liver finished this semester’s big project, to a good review of the first time to do a small program process, summed up some experience and lessons. Generally speaking, it is not very difficult to learn small programs, especially after contacting Vue, it is easy to get started with official documents, mainly the idea of mobile terminal layout + componentization.

This replay will start from the function block, the difficulty is mainly focused on the music play this piece, reflect on the realization process of some page functions and how to better achieve the component development.

GitHub address of the project

Small program to achieve NetEase cloud music

API

Binaryify big guy grab NetEase cloud API

Technology stack

  • Native wechat small program
  • Less preprocessor language

The UI component library

Vant

preparation

Encapsulate the API

  1. Create a new api.js folder in the utils folder to encapsulate the apis needed for your project.

  2. Declare global variables, including pre-url, token, and cookie.

  3. Encapsulate the request function with a Promise, because Wx. Request itself does not support Promise style calls.

function request(method, url, data) {
 return new Promise((resolve, reject) = > {
   wx.request({
     url: baseUrl + url,
     data: data,
     header: {
       'content-type': 'application/json'.'Authorization': 'Bearer ' + token ? token : ' ',},method: method,
     dataType: 'json'.responseType: 'text'.success: (result) = > {
       resolve(result);
     },
     fail: (err) = >{ reject(err); }}); })}Copy the code
  1. Export interface functions: Encapsulates different interface methods in exports objects and calls them when needed.

Global style

In this project, I used Less for the first time. As a CSS preprocessing language, it makes CSS code style more close to programming habits, and it is more convenient to handle some global styles.

Planning components

Before writing the code, you can observe the interface of NetEase Cloud Music APP and design and plan some common components in advance to avoid reconstructing the page after writing and extracting components.

The planning component includes both styles and props.

For example, the song component must be involved in the home page, search, playlist page will be there.

  • By observing their similarities and differences, we can get the design of song components in style:

  • Once you’ve determined the style, you can get what the component needsprops, such as the song component in the image abovepropsThere are song titles, tags, artists, albums, album covers, serial numbers, etc.

There are two ways to pass props: an object or just the properties that the component needs. Passing in objects is easier and avoids lengthy component writing, but passing in attributes is more reusable because in many cases the property names of data objects retrieved from the API are different.

Sometimes a component needs to perform operations on the props data. For example, it wants to use props to send requests when the component is mounted, or it wants to modify the props.

  • When a component gets asynchronouslypropsWhen the data is mounted synchronously, it is not availablepropsThe solutions are as followstopropsSet up theobserversData listenerorConditional rendering of components in WXMLTo make sure that the operation we want is in the fetchpropsAfter that.
  • Based on the one-way flow of data between components, sub-component pairspropsShould not be directly modified, can passtriggerEventCommunicates with the parent component, which modifies the data. However, in some cases, data modification needs immediate feedback, such as the increase in the number of likes and the click on the song like, etc. If the data is refreshed after the communication, the user experience will be affected. In this casethepropsInternalize the component’s internal data, and when it needs to be modified, modify the internal data first (directly feed back to the view layer), and then send a request to actually modify the data.

In this project, in order to meet the reuse requirements, I pulled out many parts and made them into components:

  • Smaller components: Smaller functions such as following users, liking, and loading can be used flexibly as components
  • Components with high reusability: for example, the song component, song sheet component, song sheet user album in the search results page are all common components
  • Components that satisfy specific page requirements: a comment component, a comment content component, a player Wrapper component for playing a song page, and a song page tail component

Page implementation

Once the components are written, implementing the page is like putting together a jigsaw puzzle of things that should be put together. Here is a brief review of the implementation of each page.

  1. Home page

  • Page structure: search box + rotation map + other page entrance + home page display block

Among them, the home page display block (recommended music list, recommended song list, etc.) only needs to use components to display, the page structure is not complex.

  • The page data: Mainly throughGet home page informationThis interface obtains the home page display block information of the user. Since I do not need a lot of returned data, I make a layer of encapsulation for the data.
setBlocks: function () {
api.getIndexBlocks({}).then(res= > {
  if (res.data.code === 200) {
    // Filter out the playlist
    let indexPlaylists = res.data.data.blocks.filter(item= > {
      return item.showType === "HOMEPAGE_SLIDE_PLAYLIST"
    });
    / / modify the action
    indexPlaylists.map(item= > {
      item.action = "goToPlaylists"
    });
    this.setData({
      / / screening banner
      banners: res.data.data.blocks.find(item= > {
        return item.showType === "BANNER";
      }).extInfo.banners,
      indexPlaylists,
      // Filter the list of songs
      indexSongList: res.data.data.blocks.filter(item= > {
        return item.showType === "HOMEPAGE_SLIDE_SONGLIST_ALIGN"}),})}})},Copy the code
  1. Search page

  • Page structure: Search header component (including search box + search association) + (historical search + hot search list)/(search results TAB page)

Has merged the search page and a page search results page, because according to the design of netease cloud music app, search the head component is search and search results pages are some, the user can search results page again, I think on the same page can achieve more painless refresh effect, enhance the user experience.

  • Page logic:
  1. Historical search records: Note that the search records are displayed each time the search page is opened, and should be stored locally. The more recent the search records are, the more advanced they are in the array (unshift() should be used), and they need to be de-duplicated

      // Search, pass parameter is the search keyword
    onSearch: function (value) {
        this.setData({
          value: value
        });
        // Store search history
        // Note that history should be a stack that needs to be removed
        let history = wx.getStorageSync('history') | | []; history.unshift(this.data.value);
        history = Array.from(new Set(history));
        wx.setStorageSync("history", history);
        // Jump to the search display page
        this.setAllRes();
        this.setData({
          showType: 3
        });
     },
    Copy the code
  2. Display of search results: get the category value of the tag page, and send a request to get the data accordingly when switching the tag

  3. Playlist square page

  • Page structure: TAB page + container holding the Playlist Wrapper component + Playlist Wrapper component
  • Page data: Selected playlists and private playlists of the recommendation page need additional API access, and playlists of other tabs are obtained by the tag category value of the tag
  1. Playlist/chart details page

  • Page structure: arc playlist information + playlist interaction + Playlist songs

  • Page design: 800 years ago learned frosted glass effect come in handy!! Here’s a refresher on the frosted glass background (the rest of the page is plain Flex layout)

  1. <! Declare the box background as album cover with inline style -->
    <view class="top frosted-glass" style="background:url({{playlist.coverImgUrl}})"></view>
    Copy the code
  2. // Frosted glass effect outer box
    .frosted-glass {
      position: relative;
      overflow: hidden;
      z-index: 100;
    // Frosted glass effect implemented
      &::before {
        content: ' ';
        position: absolute;
        // The glass needs to inherit the background and be larger than the outer box
        background: inherit;
        width: 200%;
        height: 200%;
        top: -100rpx;
        left: -100rpx;
        filter: blur(50rpx);
        z-index: -1; }}Copy the code
  • Page data: the page data acquisition logic is also very simple, the playlist ID through the route, the page in the onLoad method to get, and then send the request
  1. Private FM& song playing page

  • Page structure: song information header (not available on private FM pages) + song playback Wrapper + song playback page tail component
  • Page logic: The song Player page is responsible for managing the jump and switch functions of the song by managing the audio manager object, as described in the song handling section of the main Function logic implementation
  1. Comments on the page

  • Page structure: TAB page + comment item component

  • Page logic: The comment page is only responsible for getting the song/playlist reviews (i.e. sending requests), and the actions to the reviews (liking, viewing floor reviews, etc.) are controlled by the comment item component.

My idea is to implement comments in two components: the commentContent component, which includes commenter info + comment text + like component, and the commentItem component, which includes commentContent component + reply popover, for decoupled floor comments and general comments. Sending a request to get a floor review in the comment item component also makes the functionality of the comment content component clearer.

  1. List page

  • Page structure: list item component + playlist Wrapper component
  • Page logic: Lists are also playlists, so playlist components with the same logic are easy to use.
  1. Login & Personal Center page

  • Page logic: If you need to log in to a page, the login page is displayed. You can only log in to the page using a mobile phone number and a verification code. After a successful login, the user ID, cookie, and token are stored locally.

Main function logic implementation

Display content implementation (loading + displayed content + pagination)

The implementation logic of display content is common in many places, including the comment page, playlist square page, search results page and so on. Here, take playlist square page as an example to review the implementation logic of display content.

  1. encapsulationLoadingcomponent

The controls for loading animations are used in many places, so encapsulation as a component is appropriate. The display state of the component is controlled by the page using the component, so only one props, isLoading, is accepted.

/ / Loading animation
 i {
   display: inline-block;
   width: 8rpx;
   height: 50%;
   margin: 0 3rpx;
   .round(8rpx);
   animation: load 1s ease infinite;

     // Recursive calls to mixins generate loops that give each child a different animation delay value
   .load-mixin(@selector) when(@selector< =5) {
     &:nth-child(@{selector}) {
       animation-delay: (@selector - 1) *0.2 s;
     }

     .load-mixin(@selector + 1)}.load-mixin(2);
 }

 @keyframes load {

   0%.100% {
     height: 20rpx;
     background-color: lighten(@font-gray.60%);
   }

   50% {
     height: 40rpx;
     background-color: @light-netease-red; }}Copy the code
  1. The list of rendering

Displaying the elements that need to be displayed (in the playlist square page, the playlist components need to be displayed) makes WXML’s structure clearer by using the block tag as a wrapper element for the loop. Note that the block tag is not rendered as a node, only as a wrapper element for the control loop.

<view class="container" wx:else>
<view class="list-box">
 <block wx:for="{{tagPlaylist}}" wx:key="item.id">
   <view class="list-item">
     <playlist-wrapper itemId="{{item.id}}" picUrl="{{item.coverImgUrl}}" name="{{item.name}}" count="{{item.playCount}}"></playlist-wrapper>
   </view>
 </block>
</view>
</view>
Copy the code
  1. The paging control

In applets, paging is usually done by the user pulling up the page, so the interaction process of paging is: the user pulls up the page –> show the Loading component, sends the request –> get the return result and renders, and hides the Loading component.

  • throughonReachBottomMethod listens for a user to pull up a page
// Hit bottom load more
onReachBottom: function () {
    if (this.data.hasMore) {
        // To switch to the Loading state
      this.setLoading();
        // Send the request
      this.setTagPlaylist({
        cat: this.data.tabsActive,
        before: this.data.offset,
        limit: 18
      });
    };
},
Copy the code

The paging parameter before is set to data offset because the interface (playlist) requires that the paging parameter be the updateTime property of the last playlist on the previous page, so the value of offset is set directly after the return result.

If the API needs to page n, the offset value should be +1 before the request is sent.

  • To display paging data on mobile, my idea is to concatenate the returned results onto the original array and display them directly after loading.
this.setData({
  offset: res.data.lasttime,
  tagPlaylist: this.data.tagPlaylist.concat(res.data.playlists),
  hasMore: res.data.more
});
Copy the code
  • When displaying content in a TAB page, note that each TAB must be resetoffsetThe value of the.

Song processing

  1. Process the audio manager instance object

Song through the API to get the url of the later, in the small program music broadcast need through wx. GetBackgroundAudioManager () to obtain small program only audio manager object, songs and return the API url assigned to the SRC attribute of the instance objects.

Defines the onTimeUpdate method on the instance object to get the number of seconds the audio is currently playing in real time. Note that the total audio duration is also obtained in this method, which is invalid in other methods.

initBackgroundAudioManager: function () {
   api.getSongUrl({
     id: this.data.song.id
   }).then(res= > {
     if (res.data.code === 200 && res.data.data[0].url) {
       bam.src = res.data.data[0].url;
       // Assign values to other attributes of the instance
       bam.title = this.data.song.name;
       bam.epname = this.data.song.al.name;
       bam.coverImgUrl = this.data.song.al.picUrl;
       bam.singer = this.data.song.ar[0].name;
       bam.onTimeUpdate(() = > {
         this.setData({
           songDuration: bam.duration,
         });
         if (!this.data.onSlide) {
           this.setData({
             songCurr: bam.currentTime,
           })
         }
       });
       bam.onEnded(() = > {
         this.onSwitchSong({
           detail: {
             flag: true}}); }); }})},Copy the code
  1. The lyrics deal with

The lyric processing is wrapped in songWrapper and consists of two parts: formatting the lyric and handling the lyric beat.

  • Format lyrics

The lyric obtained through API is a long text, and my processing idea is to divide the long text into an array. The element of the array is the lyric object, that is, each lyric is a lyric object, including lid and LRC. Lid represents the number of seconds corresponding to the lyric, and LRC represents the string of the lyric.

formatLyrics: function (lrc) {
    // 1. Use split method to split lyrics into string arrays according to "\n"
  let lrcArr = lrc.split("\n");
    // Notice that the last element of the array is an empty string that needs to be disposed of
  lrcArr.pop();
  let res = [];
    // Encapsulate the lyric object
  for (let lyric of lrcArr) {
    let pos = lyric.indexOf('] ');
      // Lid is formatted, util defines a method to convert to seconds, i.e. 01:53 ==> 113(s)
    let lid = util.formatLyc(lyric.slice(1, pos));
    if(pos ! = = -1) {
      res.push({
        lid,
        lrc: lyric.slice(pos + 1)}}}return res;
},
Copy the code
  • Deal with lyrics jumping

There are two kinds of lyric beats:

  1. Automatically jump to the next line as the music plays
  2. The audio tempo changes and the lyrics need to jump to the corresponding tempo

Because the lyrics are encapsulated in the component, the component receives a songCurr props, which is the current playback time of the audio, to change the lyrics by listening for changes in the songCurr value. But because the component has no way of knowing which of the two conditions the jump of the lyrics belongs to, it also needs the parent component to provide a props notification.

Knowing what kind of beat the lyrics are, how do you make the lyrics roll?

Use the scroll-view container (be sure to give a height) to scroll up by controlling the value of its scroll-top property. (The scroll-into-view property was considered and found to work when the height of the container allows only one child element to be displayed.)

The value of the scrolltop attribute can be determined by the current lyrics index value, that is, “What line is the lyrics corresponding to the current audio playback time”. Since the value of songCurr is almost impossible to be matched with the number of seconds of lyrics, the current lyrics index value can only be obtained by comparison: If the songCurr value is greater than the lid of the lyric object in the next line of the current lyric (this.data.lyrics[this.data.lyricIndex + 1].lid), the current lyric needs to move.

'songCurr': function (val) {
  if (val) {
    // The lyrics jump to a certain line
    if (this.properties.jumpLyc) {
      let lyricIndex = this.data.lyrics.findIndex(lyc= > {
        return this.properties.songCurr < lyc.lid;
      }) - 1;
      this.setData({
        scrollVal: lyricIndex * 34,
        lyricIndex,
      });
      this.triggerEvent('jumpEnd');
    } else if (this.data.lyricIndex < this.data.lyrics.length - 1) {
      // The lyrics move down normally
      if (val >= this.data.lyrics[this.data.lyricIndex + 1].lid) {
        this.setData({
          lyricIndex: this.data.lyricIndex + 1.scrollVal: this.data.lyricIndex * 34
        });
      };
    };
  };
},
Copy the code
  1. Progress bar interactive processing
  • The essence of a progress bar is asliderComponent whose value increases as the audio plays, or the user can drag the progress bar to change itsliderThe value of the. Components to acceptmaxProperties andvalueProperty, in this case, the total song duration and the current playing progress, respectively.

The progress bar also needs to wrap the values of these two properties, because the value presented to the user should be in the form of 01:53, not 113s.

To avoid data redundancy on the page (because the wrapped property values are not used elsewhere on the page), I packaged the progress bar as a component that accepts both the current audio progress value songCurr and the total audio duration songDuration, Define two wrapper data, songCurrShow and songDurationShow, within the component.

/** * Two formats to represent the time: * 1. Audio manager instance: in seconds, no format, type is Number, exact Number (ex: 1.013573, 1.013573 seconds) * 2. Display time: the format is [00:00], type is String (ex: [02:53], indicating 2 minutes 53 seconds) * Convert the first time to the second */
formatSec: (time) = > {
    let min = '00',
      sec = '00.000';
    if (typeof time == 'String') time = Number(time);
    if (time) {
      // Format the minutes
      min = ('0' + Math.floor(time / 60)).slice(-2);
      // Format the seconds
      let secArr = (time % 60).toString().split('. ');
      sec = secArr[0];
      sec = sec.length === 1 ? '0' + sec : sec;
    }
    return `${min}:${sec}`;
},
Copy the code
  • When the user drags the progress bar, due toonTimeUpdateIn the methodsongCurrUpdate in real time, so there will be a progress bar flash back bug. My processing is inonTimeUpdateMethod adds a layer of judgment that does not update in real time when the user starts draggingsongCurrThe value of the.
  if (!this.data.onSlide) {
    this.setData({
      songCurr: bam.currentTime,
    })
  }
Copy the code

Note that the background audio manager is defined and managed on the playsong-.js page, while the drag event is monitored in the progress bar component, so there is a need for the child component to communicate with the parent component.

  1. Switch song
  • The song list

The song list is stored in app.js as a global variable. Every time after searching, clicking the song list, clicking private FM, etc., the song list will change, and the song list stores the id value of the song.

  • The idea of switching songs is to find the index value of the current song in the song list, and then get the ID of the song to play according to the user operation (previous or next), and call the current pageonLoadMethod to refresh.
// Listen for the cut event
onSwitchSong: function (e) {
    // Get the list of songs
    let wsl = app.globalData.waitingSongsList;
    let pos = wsl.findIndex(s= > {
      return s.id === this.data.song.id
    });
    let target = pos + 1;
    // true:next false:prev
    if (e.detail.flag) {
      // If the current song is the last one, loop it
      if (target > wsl.length - 1) {
        target = 0; }}else {
      target = pos - 1;
      // For the first song, skip to the last one
      if (target < 0) {
        target = wsl.length - 1; }}let curPages = getCurrentPages();
    curPages[curPages.length - 1].onLoad({
      ids: wsl[target].id
    });
},
Copy the code

conclusion

The implementation of NetEase cloud music applet is relatively smooth. In this project, I also learned to use Less pre-processing language, and how to use GitHub issue to manage the project process and write notes as much as possible, so that it would be easier to copy.

I also need to improve. Many of the work of packaging components is that I realized after writing a page that “a certain part can be packaged into components”, which adds a lot of work. If we can observe and plan components well from the beginning, the development will be more smooth and the pressure of later reconstruction will be reduced.