react hooks+redux+immutable.js打造網易雲音樂精美webApp

你們好,我是神三元。css

上次出了服務端渲染的文章,得到了不少大佬的點贊評論,很是開心。以後一段時間有人問我爲何忽然在掘金消失了,其實也並無,這個社區仍是常常在關注的,不過,更重要的是,這段時間淡出你們的視野,我決定開始沉澱一些技術上的東西,把以前學到的一些技術棧和對前端工程的一些思考作一個覆盤和整合,因而,我開始了很早就有的想法,作一個音樂webApp吧。雖然如今已經有很多這樣的webApp出現,可是實際上絕大部分vue的版本,即便有react的相似項目也中止維護了,hooks特性沒來得及更新,整個開發的方式也不是我所喜歡的。不過請你放心,開始這樣一個開源項目絕對不是把現有的項目拿來套套模板,我會長期來維護,並且,若是你能讀完這篇文章或者閱讀源碼的話你能看到徹底不同的開發思路和開發方式,學到不少硬幹貨。html

好,如今正式來介紹這個項目。前端

在線體驗地址vue

(目前掘金打開有一些樣式問題,你們先用瀏覽器打開看吧,謝謝理解)react

移動端和PC端的chrome瀏覽器食用更佳 : )ios

源碼附在最後, 注意查收。git

1、技術棧簡介

前端部分:github

  • react v16.8全家桶(react,react-router) : 用於構建用戶界面的 MVVM 框架
  • redux: 著名JavaScript狀態管理容器
  • redux-thunk: 處理異步邏輯的redux中間件
  • immutable: Facebook歷時三年開發出的進行持久性數據結構處理的庫 (它和memo、Redux搭配就是神器,memo包裹函數組件跟PureComponent是同樣的效果,在組件更新前進行數據的淺層比較,具體請參考這篇文章當 PureComponent 趕上 ImmutableJS)
  • react-lazyload: react懶加載庫
  • better-scroll: 提高移動端滑動體驗的知名庫
  • styled-components: 處理樣式,體現css in js的前端工程化神器(詳情請移步我以前的文章styled-components:前端組件拆分新思路)
  • axios: 用來請求後端api的數據

後端部分:web

  • 採用github上婦孺皆知的網易雲音樂NodeJS版api接口NeteaseCloudMusicApi,提供音樂數據。

其它:ajax

  • create-react-app: React腳手架,快速搭建項目
  • eslint: 知名代碼風格檢查工具
  • iconfont: 阿里巴巴圖標庫
  • fastclick: 解決移動端點擊延遲300ms的問題

2、項目規範

在介紹項目功能以前,我有必要強調一個這個項目工程的開發規範和我我的的編碼風格,提早告知一下,我這麼作也是有本身充分的理由的,讓項目可讀性和可維護性儘量高,但願後面看到一些奇葩的操做不要感到奇怪。

一、class組件再也不用,全面擁抱hooks,統一用函數組件。

二、組件內部狀態用hooks處理,凡是業務數據所有放在redux中管理。

三、ajax請求以及後續數據處理的具體代碼所有放在actionCreator中,由redux-thunk進行處理,儘量精簡組件代碼。

四、每個容器組件都有本身獨立的reducer,而後再全局的store下經過redux的combineReducer方法合併。

五、JS變量名(包括函數名)採用小駝峯的方式,組件名或者styled-components導出的樣式容器名都採用大駝峯,常量名全部字母大寫。

六、普通CSS類名所有用英語小寫,單詞間用下劃線鏈接,CSS動畫鉤子類名中單詞用-鏈接。

七、凡是props中有數據的,所有在組件最前面提早解構賦值,而且,得到的屬性名和方法名要分開聲明,從父組件得到的props和經過react-redux中映射得到的props也要分開聲明。

八、useEffect統一寫在最前面,而且緊跟着props解構賦值代碼後面。

九、凡是負責返回JSX的函數,統一彙集在函數最後面,中間不要穿插事件處理函數和其餘邏輯。

十、mapDispatchToProps返回的函數中,函數名格式爲xxxDispatch,以避免和現有action名衝突。

3、項目總體架構及演示演示

說明:本項目參考網易雲音樂安卓端app界面開發,基礎輪子組件沒有藉助任何UI框架,算是對本身的一個挑戰,在這個過程也學到了很多設計經驗。

因爲傳視頻比較麻煩,可是圖片又比較單調,沒法體現這個webApp的動感,所以如下采用gif.

一、推薦部分

首頁推薦:

推薦歌單詳情:
空中切入切出效果,另外還有隨着滑動會產生和標題跑馬燈效果。 在歌單中歌曲數量過多的狀況下,作了分頁處理,隨着滾動不斷進行上拉加載,防止大量DOM加載致使的頁面卡頓。

二、歌手部分

歌手列表:

這裏作了異步加載的處理,上拉到底進行新數據的獲取,下拉則進行數據的從新加載。

歌手詳情:

三、排行榜

榜單頁:

榜單詳情:

四、播放器

播放器內核:

播放列表:

會有移動端app同樣的反彈效果。

五、搜索部分

4、項目部分模塊分享

一、利用better-scroll打造超級好用的scroll基礎組件

import React, { forwardRef, useState,useEffect, useRef, useImperativeHandle } from "react"
import PropTypes from "prop-types"
import BScroll from "better-scroll"
import styled from 'styled-components';
import { debounce } from "../../api/utils";

const ScrollContainer = styled.div` width: 100%; height: 100%; overflow: hidden; `

const Scroll = forwardRef((props, ref) => {
  const [bScroll, setBScroll] = useState();

  const scrollContaninerRef = useRef();

  const { direction, click, refresh, pullUpLoading, pullDownLoading, bounceTop, bounceBottom } = props;

  const { pullUp, pullDown, onScroll } = props;

  useEffect(() => {
    const scroll = new BScroll(scrollContaninerRef.current, {
      scrollX: direction === "horizental",
      scrollY: direction === "vertical",
      probeType: 3,
      click: click,
      bounce:{
        top: bounceTop,
        bottom: bounceBottom
      }
    });
    setBScroll(scroll);
    if(pullUp) {
      scroll.on('scrollEnd', () => {
        //判斷是否滑動到了底部
        if(scroll.y <= scroll.maxScrollY + 100){
          pullUp();
        }
      });
    }
    if(pullDown) {
      scroll.on('touchEnd', (pos) => {
        //判斷用戶的下拉動做
        if(pos.y > 50) {
          debounce(pullDown, 0)();
        }
      });
    }

    if(onScroll) {
      scroll.on('scroll', (scroll) => {
        onScroll(scroll);
      })
    }

    if(refresh) {
      scroll.refresh();
    }
    return () => {
      scroll.off('scroll');
      setBScroll(null);
    }
    // eslint-disable-next-line
  }, []);

  useEffect(() => {
    if(refresh && bScroll){
      bScroll.refresh();
    }
  })

  useImperativeHandle(ref, () => ({
    refresh() {
      if(bScroll) {
        bScroll.refresh();
        bScroll.scrollTo(0, 0);
      }
    }
  }));

  const PullUpdisplayStyle = pullUpLoading ? { display: "" } : { display: "none" };
  const PullDowndisplayStyle = pullDownLoading ? { display: "" } : { display: "none" };
  return (
    <ScrollContainer ref={scrollContaninerRef}> {props.children} {/* 滑到底部加載動畫 */} <PullUpLoading style={ PullUpdisplayStyle }></PullUpLoading> {/* 頂部下拉刷新動畫 */} <PullDownLoading style={ PullDowndisplayStyle }></PullDownLoading> </ScrollContainer>
  );
})

Scroll.defaultProps = {
  direction: "vertical",
  click: true,
  refresh: true,
  onScroll: null,
  pullUpLoading: false,
  pullDownLoading: false,
  pullUp: () => {},
  pullDown: () => {},
  bounceTop: true,
  bounceBottom: true
};

Scroll.propTypes = {
  direction: PropTypes.oneOf(['vertical', 'horizental']),
  refresh: PropTypes.bool,
  onScroll: PropTypes.func,
  pullUp: PropTypes.func,
  pullDown: PropTypes.func,
  pullUpLoading: PropTypes.bool,
  pullDownLoading: PropTypes.bool,
  bounceTop: PropTypes.bool,//是否支持向上吸頂
  bounceBottom: PropTypes.bool//是否支持向上吸頂
};




export default React.memo(Scroll);
複製代碼

二、富有動感的loading組件

import React from 'react';
import styled, {keyframes} from 'styled-components';
import style from '../../assets/global-style'

const dance = keyframes` 0%, 40%, 100%{ transform: scaleY(0.4); transform-origin: center 100%; } 20%{ transform: scaleY(1); } `
const Loading = styled.div` height: 10px; width: 100%; margin: auto; text-align: center; font-size: 10px; >div{ display: inline-block; background-color: ${style["theme-color"]}; height: 100%; width: 1px; margin-right:2px; animation: ${dance} 1s infinite; } >div:nth-child(2) { animation-delay: -0.4s; } >div:nth-child(3) { animation-delay: -0.6s; } >div:nth-child(4) { animation-delay: -0.5s; } >div:nth-child(5) { animation-delay: -0.2s; } `

function LoadingV2() {
  return (
    <Loading> <div></div> <div></div> <div></div> <div></div> <div></div> <span>拼命加載中...</span> </Loading>
  );
}
 
export default LoadingV2;
複製代碼

三、模塊懶加載及代碼分割(CodeSpliting)

react官方已經提供了相應的方案, 用react自帶的lazy和Suspense便可完成。 操做以下:

import React, {lazy, Suspense} from 'react';
const HomeComponent = lazy(() => import("../application/Home/"));
const Home = (props) => {
  return (
    <Suspense fallback={null}> <HomeComponent {...props}></HomeComponent> </Suspense>
  )
};
......
export default [
  {
    path: "/",
    component: Home,
    routes: [
      {
        path: "/",
        exact: true,
        render:  ()=> (
          <Redirect to={"/recommend"}/> ) }, { path: "/recommend/", extra: true, key: 'home', component: Recommend, routes:[{ path: '/recommend/:id', component: Album, }] } ...... ] }, ]; 複製代碼

5、將來規劃和展望

目前這個項目的核心已經完成,可是仍是有不少擴展的餘地,如今的模塊至關於只是完成了60%吧。關於將來的規劃,我是這麼安排的:

  • 月底完成收藏、播放歷史功能
  • 10月份以前完成登陸功能和評論模塊
  • 10月中旬以前實現MV模塊
  • 同時撰寫《手摸手,一塊兒用React實現網易雲音樂webApp》系列拆解文章
  • 將來更多功能待補充...

因爲還有其餘的項目須要忙,因此作這個開源項目須要佔掉我很大部分的空餘時間,但我以爲這是值得的,畢竟是對本身的一次鍛鍊和挑戰。並且作這個項目的意義對我來講,並不只僅在於完成這些功能,而是凝結着本身對於技術的思考,對以前各類想法的一次親身實踐。說句實在話,當項目在一個地方被卡住的時候,心裏基本上是崩潰的,可是挺過去以後,發現本身又學會了很多東西,滿滿的成就感,這是我獨立作開源項目比較深的感觸。

最後,我要好好感謝那些幫助過個人人和項目,讓我有底氣開始作這個項目,克服一個個難關。

感謝黃軼前輩vue音樂實戰課程,讓我學到了很是多的原生JS技能和組件封裝技巧。

感謝DellLeereact從入門到簡書項目實戰讓我入門React,讓我養成了React工程化的編碼習慣。

感謝React開源項目mango-music,雖然我如今的項目和它在開發理念和編碼風格上大相徑庭,但仍是有部分的動畫效果仍是借鑑了這個開源項目,讓我大開眼界, 很是感謝,請你們也不忘去給這個項目點star,雖然沒用到hooks,可是仍是值得一學的。

最後說明,這個項目毫不是一時的demo,我是會長期來維護,但願你們能踊躍提pr,提issue,將這個項目打造的更加完美,可以幫助到更多的人學習到react除了官方demo以外的實際應用,避開更多的坑。

其實作這個項目出來效果雖然是還算天然,可是開發的過程是至關曲折的,我在後面也會作持續的分享,把個人開發過程遇到的挑戰一五一十地分享給各位。最後,別忘了給這個項目點一個star哦,謝謝支持。

github源碼地址

最後放上個人微信公衆號:前端三元同窗

系列拆解文章將在公衆號連載,敬請關注!
相關文章
相關標籤/搜索