作者:神三元
来源:https://juejin.im/post/5d1fe6be51882579db031a6d
在线体验地址:http://47.105.150.105:8010/
移动端和PC端的chrome浏览器食用更佳 : )
源码附在最后, 注意查收。
一、技术栈简介
前端部分:
react v16.8
全家桶(react,react-router) : 用于构建用户界面的 MVVM 框架redux
: 著名JavaScript状态管理容器redux-thunk
: 处理异步逻辑的redux中间件immutable
: Facebook历时三年开发出的进行持久性数据结构处理的库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](https://i-blog.csdnimg.cn/blog_migrate/7426fe60d38350ce58f23f96a9b24d14.jpeg)
由于传视频比较麻烦,但是图片又比较单调,无法体现这个webApp的动感,因此以下采用gif.
1、推荐部分
首页推荐:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/efb2c3bad15f72dea48d7a3e2c5dba0b.gif)
推荐歌单详情:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/a71505a5d2af0ae13c13f19b15d8cfd3.gif)
空中切入切出效果,另外还有随着滑动会产生和标题跑马灯效果。
在歌单中歌曲数量过多的情况下,做了分页处理,随着滚动不断进行上拉加载,防止大量DOM加载导致的页面卡顿。
2、歌手部分
歌手列表:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/18d6f97da4f54c04099dd3966328b84e.gif)
这里做了异步加载的处理,上拉到底进行新数据的获取,下拉则进行数据的重新加载。
歌手详情:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/5989826bbb7887dd1862671512b60015.gif)
3、排行榜
榜单页:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/ee6549facca308fcc33f8575e1710d3f.gif)
榜单详情:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/572e6f561512bac1b19288caa582af8c.gif)
4、播放器
播放器内核:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/0351804c2de321190032581fac8b8893.gif)
播放列表:
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/4d0bfcf340e059cfdca84b2359ef2106.gif)
会有移动端app一样的反弹效果。
5、搜索部分
![640?wx_fmt=gif](https://i-blog.csdnimg.cn/blog_migrate/6204deb29e12f44d6a621ae049ad1e2f.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](https://i-blog.csdnimg.cn/blog_migrate/ff7481a0ed7356594b535824918aaff8.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%吧。关于未来的规划,我是这么安排的:
完成收藏、播放历史功能
完成登录功能和评论模块
实现MV模块
同时撰写《手摸手,一起用React实现网易云音乐webApp》系列拆解文章
未来更多功能待补充…
由于还有其他的项目需要忙,所以做这个开源项目需要占掉我很大部分的空余时间,但我觉得这是值得的,毕竟是对自己的一次锻炼和挑战。而且做这个项目的意义对我来说,并不仅仅在于完成这些功能,而是凝结着自己对于技术的思考,对之前各种想法的一次亲身实践。说句实在话,当项目在一个地方被卡住的时候,内心基本上是崩溃的,但是挺过去之后,发现自己又学会了不少东西,满满的成就感,这是我独立做开源项目比较深的感触。
最后,我要好好感谢那些帮助过我的人和项目,让我有底气开始做这个项目,克服一个个难关。
感谢黄轶前辈vue音乐实战课程,让我学到了非常多的原生JS技能和组件封装技巧。
感谢DellLeereact从入门到简书项目实战让我入门React,让我养成了React工程化的编码习惯。
感谢React开源项目mango-music,虽然我现在的项目和它在开发理念和编码风格上截然不同,但还是有部分的动画效果还是借鉴了这个开源项目,让我大开眼界, 非常感谢,请大家也不忘去给这个项目点star,虽然没用到hooks,但是还是值得一学的。
最后说明,这个项目绝不是一时的demo,我是会长期来维护,希望大家能踊跃提pr,提issue,将这个项目打造的更加完美,能够帮助到更多的人学习到react除了官方demo之外的实际应用,避开更多的坑。
其实做这个项目出来效果虽然是还算自然,但是开发的过程是相当曲折的,我在后面也会做持续的分享,把我的开发过程遇到的挑战一五一十地分享给各位。最后,别忘了给这个项目点一个star哦,谢谢支持。
[github源码地址]:https://github.com/sanyuan0704/cloud-music