I. Project Introduction

1. Project introduction

  • The technology stack used by the project

    • CSSuseFlexFor the layout
    • Configure the path alias using:carco
    • Project routing uses:react-routerTo manage the
    • usereact-router-configCentralized path mapping table management
    • usestyled-components+ commoncssWriting style
    • useaxiosSending network Requests
    • Project comprehensive embraceReact Hooks
    • The project component library uses:ant design
    • useimmutableFor the projectreducerIn thestatemanage
    • useredux-immtableThe root directoryreducerIn thestatemanage
    • Projects usingredux-thunkThe middleware
    • usepropTypecheckpropsType and default value
    • usereact-transition-groupAdd transition animation effects
    • Optimization in the project: all functional components are usedmemo, route lazy loading, function anti – shake
  • Project objectives

    • useReactDevelopment of NetEase Cloud music PC website

2. Suitable for people and harvest

  • Suitable for people:

    • Suitable for those who want to know the general process of a project

    • Or they learned React but lacked React project experience

      • (If you don’t know anything about React, check out the React series.)
      • It’s better to knowNode)
  • Harvest:

    • How to design music player components, lyrics analysis, etc.
    • Project directory structure division, large projectstatemanagement
    • General process of the project, how to optimize performance and so on

3. Display of page effects and functions

Recommendation/new album/list

Routing switch

Song reviews

list

player

Song switching (random, sequential, single loop)

Song search

  • New: Keyboard events ↓ & function shake proof

    • ctrl+kSearch box gets focus & wake up search drop – down box
    • escUnfocus & drop down box
    • enterEnter the song search details

List of song search details

  • Press in the search boxenterOk, search the listbasicFunction implementation

4. Project source code and API interface

  • 👉 Github address of the project

  • 👉 API interface documents

If you think the project is good 👏, give a ⭐ to encourage it

5. Project specifications

  • Project specifications: There are some development specifications and code styles in the project (you can also follow your own custom)

    1. The folder name under SRC must be lowercase and multiple words are connected by a hyphen (-). Component files and folders are named hump

    2. JavaScript variable names use small humps, constants use all uppercase letters, components use large humps;

    3. The CSS uses a combination of the common CSS and styled- Component

      • Global use commonCSSPartial adoptionstyled-component
    4. Moving away from class components, unifying functional components, and fully embracing Hooks;

    5. All functional components are wrapped with Memo to avoid unnecessary rendering.

    6. Internal component status, using useState and useReducer. All business data is managed in REdux;

    7. Function components are coded in the following order:

      • Components,stateManagement;
      • reduxthehooksCode;
      • Other componentshooksCode;
      • Other logical code;
      • Return JSX code;
    8. The redux code specification is as follows:

      • Each module has its own independentreducerThrough thecombineReducerTo merge;
      • Asynchronous request code useredux-thunk“And write inactionCreators;
      • reduxApply directly toredux hooksIs not used anymoreconnect;
  • Other specifications are decided and written in the project according to the actual situation;

6. React DevTools icon hidden

  • Before the actual development project: We openNetEase Cloud Music official websiteWhy can’t you see the official website of NetEase Cloudreact devtoolsThe markup of the plug-in (not important)
  • The React DevTools icon is hidden

//index.js on top -- in production
// disable react-dev-tools for this project 
if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === "object") {
	for (let [key, value] of Object.entries(window.__REACT_DEVTOOLS_GLOBAL_HOOK__)) {
		window.__REACT_DEVTOOLS_GLOBAL_HOOK__[key] = typeof value == "function" ? () = >{} : null; }}Copy the code

2. Project initialization

1. Introduction – VSCOde&Chrome plugin (optional)

  • If you have already installed it, you can skip it. The following is optional, but you can also skip it

  • For easier development projects, it is recommended to install the following vscode plug-ins

    • ESLint: a code style checker to help standardize code writing
    • vscode-styled-components: in writingstyled-componentsSyntax highlighting and style components in
    • Path-alias: indicates that an alias path has an intelligent prompt
    • ES7 React/Redux/GraphQL/React-Native snippets: Code snippet
  • Chrome plug-ins

    • Redux DevTools: Easy to debugreduxdata
    • FeHelper: returned to the serverjsonData beautification

2. Project directory division

  • usecreate-react-appScaffold initialization project structure:create-react-app music163_xxx
  • The directory structure can also be divided according to its own custom structure
│─ SRC ├─ Assets Public Resources CSS And Image ├─ CSS Global CSS ├─img ├─ Common Common Constants, Data ├─ Components Common Component ├─ Pages Route Mapping Component ├─ Router ├─ exercises ├─ custom hooks ├─ custom hooksCopy the code

3. Initialize the project

1. CSS reset and public CSS extraction

  • Installation:

yarn add normalize.css

  • In assets/ CSS /reset. CSS

@import "~normalize.css";

And write your own global CSS to override the CSS you don’t need in Normalize

@import "~normalize.css";

/* Style reset */
body, html, h1, h2, h3, h4, h5, h6, ul, ol, li, dl, dt, dd, header, menu, section, p, input, td, th, ins {
  padding: 0;
  margin: 0;
}

ul, ol, li {
  list-style: none;
}

a {
  text-decoration: none;
  color: #Awesome!;
}

a:hover {
  color: #Awesome!;
  text-decoration: underline;
}

i, em {
  font-style: normal;
}

input, textarea, button, select, a {
  outline: none;
  border: none;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

img {
  border: none;
  vertical-align: middle;
}
/ /... More direct projects
Copy the code
  • Export in index.js

import "./assets/css/reset.css";

2. Modify the project configuration

  • Installation:
`yarn add @craco/craco`
`yarn add babel-plugin-import`
`yarn add antd`
Copy the code
  • Create craco.config.js in the root directory
const path = require("path");
//dir is the path to the current file (craco- root path), and __dirname is the path passed to resolve () below, then concatenated
const resolve = (dir) = > path.resolve(__dirname, dir);

module.exports = {
// ANTD styles are loaded on demand
  babel: {
    plugins: [["import",
        {
          libraryName: "antd".libraryDirectory: "es".style: "css",},],],},// Configure the @ alias instead of the SRC file path in the root directory
  webpack: {
    alias: {
      "@": resolve("src"),}}};Copy the code

3. Configure project routes

  • Installation:

Yarn add [email protected]

  • Unified route configuration:

yarn add react-router-config

  • Start writing the unified routing configuration in the Router folder of SRC
import Discover from "@/pages/Discover";
import Friends from "@/pages/Friends";
import Mine from "@/pages/Mine";

const routes = [
  {
    path: "/".exact: true.component: Discover,
  },
  {
    path: "/discover".component: Discover,
  },
  {
    path: "/friends".component: Friends,
  },
  {
    path: "/mine".component: Mine,
  },
];
export default routes;
Copy the code

4. AppHeader components

1. Styled – Components Writes the CSS

  • Installation:

yarn add styled-components

  • New style.js- import:

import styled from "styled-components"

  • Then start writing CSS
  • Note: Header TAB data is placed in a common componentcommonThe folderlocal-data.jsFile, directly import traversal can be
  • Pay attention to routingreact-router-configTo configure the route mapping table

5. AppFooter components

1. Styled – Components Writes the CSS

  • Write the structure and style directly, nothing to say

  • Display summary display components in the app.js file, where the routing component is dynamically displayed between the header and tail
// The imported third-party library
import React, { memo } from "react";
import { renderRoutes } from "react-router-config";

// Import your own tools
import routes from "./router";

// Import components
import AppFooter from "@/components/AppFooter";
import AppHeader from "@/components/AppHeader";

export default memo(function App() {
  return (
    <div>
      <AppHeader />
      {renderRoutes(routes)}
      <AppFooter />
    </div>
  );
});
Copy the code

6. Use of Axios

  • To use back-end data, install AXIos:yarn add axios
  • Encapsulate AXIOS twice in the Service folder, and then export it to the desired file
  • Then create a separate file for each module that needs a request, write the method to initiate the request in it, and export the function

7. Use of redux

  • To use redux management data, install:

yarn add redux react-redux redux-thunk

  • Create reducer. Js and index. Js files in SRC /store
    • The reducer. Js file is used to summarize the reducer files and export them to the store/index.js file
    • The index.js file is used to create the Store and various configurations
  • A Store folder is created in each component to manage the Reducer of each component and export the reducer file in the SRC folder
  • Note: Don’t forget to wrap the app.js file with the provider
import { Provider } from "react-redux";
import store from "./store/index";
export default memo(function App() {
  return (
    <div>
      <Provider store={store}>.</Provider>
    </div>
  );
});
Copy the code

8. Replace Connect with useSelector useDispatch

import React, { memo, useEffect } from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";

import { getTopBannerAction } from "./store/actionCreators";

function Recommend(props) {
  // Use hooks instead of traditional redux to retrieve data and perform operations

  // Returns a reference to the dispatch function in the Redux store. You can use it as needed
  const dispatch = useDispatch();

  // Retrieve data from redux's store object (state).
  const { topBanners } = useSelector(
    (state) = > ({
      topBanners: state.recommend.topBanners,
    }),
    shallowEqual
  );
  // console.log(banners);
  useEffect(() = > {
    dispatch(getTopBannerAction());
  }, [dispatch]);

  return <div>Recommend:{topBanners.length}</div>;
}

export default memo(Recommend);

/ / react - traditional story
// import React, { memo, useEffect } from "react";
// import { connect } from "react-redux";

// import { getTopBannerAction } from "./store/actionCreators";

// function Recommend(props) {
// const { getBanners, topBanners } = props;

// // calls the getintegration method that maps to props
// useEffect(() => {
// getBanners();
// }, [getBanners]);

// return 
      
Recommend:{topBanners.length}
;
// } // // map the state and method below to props // const mapStateToProps = (state) => { // return { // topBanners: state.recommend.topBanners, / /}; // }; // const mapDispatchToProps = (dispatch) => { // return { // getBanners: () => { // dispatch(getTopBannerAction()); / /}, / /}; // }; // export default connect(mapStateToProps, mapDispatchToProps)(memo(Recommend)); Copy the code

9. Data variability — immutable.js

  • Immutable. Js Chinese document

  • In React development, we always emphasize immutability of data:

    • Whether it’s state in a class component or state managed in REdux;
    • In fact, immutability of data is very important throughout JavaScript coding;
  • Problems caused by data variability (Cases) :

    • We didn’t change obj, we just changed obj2, but eventually we changed obj too;
    • The reason is very simple. Objects are reference types, they refer to the same memory space, and both references can be modified arbitrarily.
  • Is there a way to solve the above problems?

    • Copy objects: object. assign or extend operators
   const info = {
        name: "why".age: 20};constobj = { ... info };const obj2 = info;

      info.name = "kobe";

      console.log(obj.name); // why
      console.log(obj2.name); // kobe
Copy the code
  • Is there a problem with a shallow copy of this object?
    • From the point of view of the code, there is no problem, but also solved some potential risks in our actual development;
    • From a performance point of view, there are problems. If the object is too large, copying this way can cause performance problems and waste memory.
  • One might say, isn’t that how it’s done in development?
    • Is it right that it has always been so
  • In order to solve the above problem, the emergence ofImmutableObject concept:
    • Immutable is a mutable object that returns a new object whenever it is modified.
  • But isn’t that a way to waste memory?
    • In order to save memory, a new algorithm emerged: Persistent Data Structure (Persistent Data Structure or consistent Data Structure);
  • Of course, when we hear about persistence, the first thing we think of is data being saved to a local or local database, but that’s not what it means:
    • The use of a data structure to store data;
    • When data is modified, an object is returned, but the new object uses the previous data structure as best it can without wasting memory.
  • How do you do this? Structure sharing.

Returns a new Immutable object. Immutable is a Persistent Data Structure, which ensures that old Data can be used to create new Data without changing it. And to avoid the performance cost of deepCopy copying all nodes once, Immutable uses Structural Sharing, where if a node in the object tree changes, only that node and its affected parent are modified, and the other nodes are shared. Watch the animation below:

Example:

<! DOCTYPE html><html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>
    <script>
      const im = Immutable;

      // Map usage
      const info = {
        name: "kobe".age: 30.friend: {
          name: "james".age: 25,}};const infoIM = im.Map(info);

      const obj = infoIM;
      // A new object is returned after modification. The value of the new object is changed
      const infoIM2 = infoIM.set("name"."why");
      console.log(obj.get("name")); //kobe
      console.log(infoIM.get("name")); //kobe
      console.log(infoIM2.get("name")); //why

      // Use List;
      const names = ["abc"."cba"."nba"];
      // const arr = names;
      // names[0] = "why";
      // console.log(arr); // Changes will occur

      const namesIM = im.List(names);
      const arrIM = namesIM.set(0."why");
      console.log(namesIM.get(0));
      console.log(arrIM.get(0));

      // Note that Map can only perform shallow conversions, converting the outermost object to immutable.js, whereas fromJS can perform deep conversions, regardless of the nested layers
      const zgc = {
        name: "kobe".age: 30.friend: {
          name: "james".age: 25,}};const zgcIM1 = im.Map(zgc);
      const zgcIM2 = im.fromJS(zgc);
      console.log(zgcIM1.get("friend"));
      console.log(zgcIM2.get("friend"));
    </script>
  </body>
</html>
Copy the code

Using ImmutableJS in a project:

yarn add immutable

Introduce IMmutable in the Reducer.js of each module:

/ / ImmutableJS method
import * as actionTypes from "./constants";
import { Map } from "immutable";

const defaultState = Map({
  topBanners: [],});function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_TOP_BANNERS:
      return state.set("topBanners", action.topBanners);
    default:
      returnstate; }}export default reducer;

// Common method
// import * as actionTypes from "./constants";

// const defaultState = {
// topBanners: [],
// };
// function reducer(state = defaultState, action) {
// switch (action.type) {
// case actionTypes.CHANGE_TOP_BANNERS:
// return { ... state, topBanners: action.topBanners };
// default:
// return state;
/ /}
// }
// export default reducer;
Copy the code

yarn add redux-immutable

In the store/reducer. In js:

// import { combineReducers } from "redux";
// Reduce combineReducers was imported from redux-immutable
import { combineReducers } from "redux-immutable";

import { reducer as recommendReducer } from ".. /pages/Discover/c-pages/Recommend/store";
/ / merge
const reducer = combineReducers({
  recommend: recommendReducer,
});

export default reducer;
Copy the code

Read:

 // Retrieve data from redux's store object (state).
  const { topBanners } = useSelector(
    (state) = > ({
      / / topBanners: state. How. TopBanners, / / common way to read data
      
      // topintegration: state.get("recommend"). Get (" topintegration "), //ImmutableJS reads data methods
      topBanners: state.getIn(["recommend"."topBanners"]), / / short
    }),
    shallowEqual
  );
Copy the code

10. Discover-Recommend Component (Discover Music -Recommend module)

1. Build the file structure and subdirectories

  • Discover component TAB data is placed in the common componentcommonThe folderlocal-data.jsFile, directly import traversal build file structure
  • Create style.js and start writing CSS
  • Configure the child routing mapping table and import it through props
  • Create a new directory under the Discover folder and place its children in it

2. Complete the rotation diagram component -TopBanner

  • Create a new directory under Discover/ C-Pages /Recommend and put its children in it
  • usingantdIn the libraryCarouselComplete the wheel – cast diagram and rewrite its dot style

3. Encapsulation of the ThemeHeaderRec header module

  • To use this function, you only need to import the module and pass in the title and keywords, which are received by props
  • In addition, the type verification and default parameters are set to prevent errors when the keywords are undefined

4. Hot recommended module HotRecommend is complete

  • Components are encapsulated in components/SongsCover, and two methods are encapsulated in utils/ rec-format.js

  • Redux retrieves and reads the data and iterates through the SongsCover component

5. NewAlbum new disk module is completed

  • Components are encapsulated in components/AlbumCover, using methods encapsulated in utils/ rec-format.js
  • Redux retrieves and reads data and iterates through the AlbumCover component

6. RecommendRanking ranking module is completed

  • Encapsulates components in components/TopRanking, using methods encapsulated in utils/ rec-format.js

  • Redux gets and reads the data and uses the TopRanking component three times

  • The whole background is just a big background picture

7. Other modules on this page are complete

  • The user login module only completes the UI
  • The entry singer module requests data from the back end
  • The popular anchor module is data written dead in common/local-data.js
  • In popular singer module and the host module with the same packaging components/ThemeHeaderSmall head

11. Implementation of cloud music playback toolbar

  • Create a Player/AppPlayerBar folder under Pages
  • Create a new Player/ Store folder under Pages
  • The Slider is the Slider component of antD component library, whose original style is overwritten by Sprite
  • It’s for music<audio/>The label
  • Play and pause change dynamically based on the state

12. Song display page

  • Set up a display page in Pages /Player/index.tsx

  • Receive the search parameter to get the current song ID

  yarn add url-parse
  import { qs } from "url-parse"; / / want to introduce
  const { search } = props.location;
  const { id } = qs.parse(search);
Copy the code

13. Logic for playing songs

  • Add songs to playlists:

    • When the user clicks on a song, it gets the current song details (IDS)
    • If the song already exists in the playlist, get the index of the song in the playlist, modify the index of the currently playing song, modify the currently playing song, and then the music player will play the song.
    • If the current song does not exist, request the song data, add the song to the end of the playlist, get the last index, modify the index of the current playing song, modify the current playing song, and then the music player will play the song.
  • Record the current playback order:

 sequence: 0.// 0 loop 1 random 2 singles
 // Click the button to switch the playback sequence
 const changeSequence = () = > {
    let Sequence = sequence + 1;
    if (Sequence > 2) {
      Sequence = 0;
    }
    dispatch(changeSequenceAction(Sequence));
  };
Copy the code
  • There are two ways to switch the playing order of songs (the second way is used in the project) :
    • Single cycle
      1. Create a playlist and fetch the song from here. The playlist will only fetch the current song from the original list
      2. The index of the current song is unchanged
    • Random broadcast
      1. Create a playlist from which songs are fetched when they are played. The playlist’s data is copied out of order from the original list
      2. With the random number
    • Order of play
      1. I’m going to create a playlist, and I’m going to fetch the song from here, and the playlist data is the same as the original playlist data
      2. Direct current song index +1
  • Switching logic for song playback

// Click the left and right buttons to switch (< -- >) to play the song
export const changeCurrentIndexAndSongAction = (tag) = > {
  return (dispatch, getState) = > {
    const playList = getState().getIn(["player"."playList"]);
    const sequence = getState().getIn(["player"."sequence"]);
    let currentSongIndex = getState().getIn(["player"."currentSongIndex"]);
    // Get the new index
    switch (sequence) {
      case 1: // Random play
        // Get a random index when playing
        let randomIndex = getRandomNumber(playList.length);
        // If the index value is equal to the current index, get a new index
        while (randomIndex === currentSongIndex) {
          randomIndex = getRandomNumber(playList.length);
        }
        currentSongIndex = randomIndex;
        break;
      default:
        // Other cases, i.e. single or looping (in both cases, clicking the left/right toggle button will switch songs)
        currentSongIndex = currentSongIndex + tag;
        if (currentSongIndex >= playList.length) {
          currentSongIndex = 0;
        }
        if (currentSongIndex < 0) {
          currentSongIndex = playList.length - 1; }}const currentSong = playList[currentSongIndex];
    dispatch(changeCurrentSongAction(currentSong));
    dispatch(changeCurrentSongIndexAction(currentSongIndex));
  };
};

Copy the code
  • Request and analysis of song lyrics
    • Get lyrics from the back end
    • Parsing lyric function

[00:31. 160] if the scene appears a piano = = > {time: 16280, the content: ‘あ ん な に love し た jun が い な い “}

const parseExp = / \ [(\ d {2}) : (\ d {2}) \. (\ d {2, 3}) \] /;

export function parseLyric(lyricString) {
  const lineStrings = lyricString.split("\n");

  const lyrics = [];
  for (let line of lineStrings) {
    if (line) {
      const result = parseExp.exec(line);
      // console.log(result);
      // If no match is found this time, the next match is skipped
      if(! result)continue;
      const time1 = result[1] * 60 * 1000;
      const time2 = result[2] * 1000;
      const time3 = result[3].length === 3 ? result[3] * 1 : result[3] * 10;
      const time = time1 + time2 + time3;
      // replace method: replace the previous value with the latter value, trim: remove Spaces
      const content = line.replace(parseExp, "").trim();
      const lineObj = { time: time, content: content }; lyrics.push(lineObj); }}return lyrics;
}
Copy the code
  • Find and display the lyrics according to the current time
 // Get the current playing lyrics
    let i = 0;
    // If the current playing time is less than the current item lyrics, break out of the loop,
    // The current record of I minus 1 is the current lyrics
    for (i; i < lyricList.length; i++) {
      let lyricItem = lyricList[i];
      if (currentTime < lyricItem.time) {
        break; }}if(currentLyricIndex ! == i -1) {
      dispatch(changeCurrentLyricIndexAction(i - 1));
      const content = lyricList[i - 1]? .content; message.open({key: "lyric".content: content,
        duration: 0.className: "lyric-class"}); }Copy the code

14. Lazy route loading

/ / normal
// import Discover from "@/pages/Discover";

// import Recommend from "@/pages/Discover/c-pages/Recommend";
// import Ranking from "@/pages/Discover/c-pages/Ranking";
// import Songs from "@/pages/Discover/c-pages/Songs";
// import Djradio from "@/pages/Discover/c-pages/Djradio";
// import Artist from "@/pages/Discover/c-pages/Artist";
// import Album from "@/pages/Discover/c-pages/Album";

// import Friends from "@/pages/Friends";
// import Mine from "@/pages/Mine";
// import NotFound from "@/components/NotFound";
// import Song from "@/pages/Player";

/ / lazy loading
const Discover = React.lazy(() = > import("@/pages/Discover"));
const Recommend = React.lazy((_) = >
  import("@/pages/Discover/c-pages/Recommend"));const Ranking = React.lazy((_) = > import("@/pages/Discover/c-pages/Ranking"));
const Songs = React.lazy((_) = > import("@/pages/Discover/c-pages/Songs"));
const Djradio = React.lazy((_) = > import("@/pages/Discover/c-pages/Djradio"));
const Artist = React.lazy((_) = > import("@/pages/Discover/c-pages/Artist"));
const Album = React.lazy((_) = > import("@/pages/Discover/c-pages/Album"));

const Friends = React.lazy((_) = > import("@/pages/Friends"));
const Mine = React.lazy((_) = > import("@/pages/Mine"));
const NotFound = React.lazy((_) = > import("@/components/NotFound"));
const Song = React.lazy((_) = > import("@/pages/Player"));
Copy the code

At the same time use Suspense to wrap routing components in App components, fallback is a component that is called at load time

import React, { memo, Suspense } from "react";

 <Suspense fallback={<div>. loading</div>}>
          {renderRoutes(routes)}
 </Suspense>
Copy the code

14. Popover lyrics rotation page

Complete the module in Player/AppPlayPanel

Note: This module only completed the lyrics rotation and song display, the other items (delete, favorites, you can finish by yourself)

yarn add classnames

  • PlayHeader head
  • PlayList PlayList
  • LyricPanel lyrics in rotation

Use ref to get the lyric scroll module on the right to call this method to achieve lyric scroll

// Implement lyrics scroll
export function scrollTo(element, to, duration) {
  if (duration <= 0) return;
  var difference = to - element.scrollTop;
  var perTick = (difference / duration) * 10;
  setTimeout(function () {
    element.scrollTop = element.scrollTop + perTick;
    if (element.scrollTop === to) return;
    scrollTo(element, to, duration - 10);
  }, 10);
}
Copy the code

Element is the element from ref, to is the total height of the current lyric. heght 32px for each lyric. set duration=300, if duration>0 and the scrolling height of the current element is not equal to the total height of the current lyric. call this method recursively