react hooks+redux+immutable.js仿网易云音乐打造精美webApp

大家好,我是神三元。

今天给大家介绍一下我最近做的开源项目。

github地址:https://github.com/sanyuan0704/cloud-music(进去有在线体验入口哦)

移动端和PC端的chrome浏览器食用更佳 : )

源码附在最后, 注意查收。

一、技术栈简介

前端部分:

  • 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的数据

后端部分:

  • 采用github上妇孺皆知的网易云音乐NodeJS版api接口NeteaseCloudMusicApi,提供音乐数据。

其它:

  • create-react-app: React脚手架,快速搭建项目

  • eslint: 知名代码风格检查工具

  • iconfont: 阿里巴巴图标库

  • fastclick: 解决移动端点击延迟300ms的问题

二、项目规范

在介绍项目功能之前,我有必要强调一个这个项目工程的开发规范和我个人的编码风格,提前告知一下,我这么做也是有自己充分的理由的,让项目可读性和可维护性尽可能高,希望后面看到一些奇葩的操作不要感到奇怪。

1、class组件不再用,全面拥抱hooks,统一用函数组件。

2、组件内部状态用hooks处理,凡是业务数据全部放在redux中管理。

3、ajax请求以及后续数据处理的具体代码全部放在actionCreator中,由redux-thunk进行处理,尽可能精简组件代码。

4、每一个容器组件都有自己独立的reducer,然后再全局的store下通过redux的combineReducer方法合并。

5、JS变量名(包括函数名)采用小驼峰的方式,组件名或者styled-components导出的样式容器名都采用大驼峰,常量名所有字母大写。

6、普通CSS类名全部用英语小写,单词间用下划线连接,CSS动画钩子类名中单词用-连接。

7、凡是props中有数据的,全部在组件最前面提前解构赋值,并且,获得的属性名和方法名要分开声明,从父组件获得的props和通过react-redux中映射获得的props也要分开声明。

8、useEffect统一写在最前面,并且紧跟着props解构赋值代码后面。

9、凡是负责返回JSX的函数,统一聚集在函数最后面,中间不要穿插事件处理函数和其他逻辑。

10、mapDispatchToProps返回的函数中,函数名格式为xxxDispatch,以免和现有action名冲突。

三、项目整体架构及演示演示

说明:本项目参考网易云音乐安卓端app界面开发,基础轮子组件没有借助任何UI框架,算是对自己的一个挑战,在这个过程也学到了不少设计经验。

640?wx_fmt=png

由于传视频比较麻烦,但是图片又比较单调,无法体现这个webApp的动感,因此以下采用gif.

1、推荐部分

首页推荐:

640?wx_fmt=gif

推荐歌单详情:

640?wx_fmt=gif

空中切入切出效果,另外还有随着滑动会产生和标题跑马灯效果。

在歌单中歌曲数量过多的情况下,做了分页处理,随着滚动不断进行上拉加载,防止大量DOM加载导致的页面卡顿。

2、歌手部分

歌手列表:

640?wx_fmt=gif

这里做了异步加载的处理,上拉到底进行新数据的获取,下拉则进行数据的重新加载。

歌手详情:

640?wx_fmt=gif

3、排行榜

榜单页:

640?wx_fmt=gif

榜单详情:

640?wx_fmt=gif

4、播放器

播放器内核:

640?wx_fmt=gif

播放列表:

640?wx_fmt=gif

会有移动端app一样的反弹效果。

5、搜索部分

640?wx_fmt=gif

四、项目部分模块分享

1、利用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(() => {	
    if(bScroll) return;	
    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);
2、富有动感的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;

640?wx_fmt=gif

3、模块懒加载及代码分割(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,	
        }]	
      }	
      ......	
    ]	
  },	
];

五、未来规划和展望

目前这个项目的核心已经完成,但是还是有很多扩展的余地,现在的模块相当于只是完成了60%吧。关于未来的规划,我是这么安排的:

  • 月底完成收藏、播放历史功能

  • 10月份之前完成登录功能和评论模块

  • 10月中旬之前实现MV模块

  • 同时撰写《手摸手,一起用React实现网易云音乐webApp》系列拆解文章

  • 未来更多功能待补充...

由于还有其他的项目需要忙,所以做这个开源项目需要占掉我很大部分的空余时间,但我觉得这是值得的,毕竟是对自己的一次锻炼和挑战。而且做这个项目的意义对我来说,并不仅仅在于完成这些功能,而是凝结着自己对于技术的思考,对之前各种想法的一次亲身实践。说句实在话,当项目在一个地方被卡住的时候,内心基本上是崩溃的,但是挺过去之后,发现自己又学会了不少东西,满满的成就感,这是我独立做开源项目比较深的感触。

最后,我要好好感谢那些帮助过我的人和项目,让我有底气开始做这个项目,克服一个个难关。

感谢黄轶前辈vue音乐实战课程,让我学到了非常多的原生JS技能和组件封装技巧。

感谢DellLee react从入门到简书项目实战让我入门React,让我养成了React工程化的编码习惯。

感谢React开源项目mango-music,虽然我现在的项目和它在开发理念和编码风格上截然不同,但还是有部分的动画效果还是借鉴了这个开源项目,让我大开眼界, 非常感谢,请大家也不忘去给这个项目点star,虽然没用到hooks,但是还是值得一学的。

最后说明,这个项目绝不是一时的demo,我是会长期来维护,希望大家能踊跃提pr,提issue,将这个项目打造的更加完美,能够帮助到更多的人学习到react除了官方demo之外的实际应用,避开更多的坑。

其实做这个项目出来效果虽然是还算自然,但是开发的过程是相当曲折的,我在后面也会做持续的分享,把我的开发过程遇到的挑战一五一十地分享给各位。最后,别忘了给这个项目点一个star哦,谢谢支持。

github源码地址:https://github.com/sanyuan0704/cloud-music

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值