嘛,先来吹一波,神三元大佬的云音悦项目采用的技术都是相当新的,很具有学习和借鉴的意义,推荐大家可以去瞧瞧他写的掘金小册React Hooks 与 Immutable 数据流实战,有能力的可以支持下,项目源码是上传了github的,你要不想买也可以直接读源码,耐心点也还是没问题的。
接下来我会对歌手详情页面(github地址、项目在线地址)中涉及的技术进行解析,对于其中用到的你可能不了解的技术进行说明:
其中涉及的技术有:
-
styled-components
-
React Hooks
-
react-transition-group
-
redux
-
immutable
-
better-scroll
接下来我将就其中使用的各个技术栈来进行介绍,带各位入门,更详细的学习可以参照各个技术的官方文档进行深入的学习,官网的地址我都会在下面贴出来。
1. styled-components
神三元在项目中使用组件化,引入style-component
- 不再是index.js + index.css
- 界面类组件更易于复用
styled-components 说白了就是一个可以帮你自动生成类名的工具,它的上手成本非常底,而且贼好用,这个工具生成的类名都是唯一的,所以开发者也不再需要考虑 Css 变量名称污染的问题了,styled-components 的大体使用上和 css 区别不大,下面就针对其中一些需要注意的点进行讲解:
// 安装 styled-components 然后导入使用,安装可以使用 npm i xxxx -S 的格式安装,如:npm i styled-components -S
import styled from 'styled-components';
import style from '../../assets/global-style';
// 创建并且导出一个SongListWrapper,再在要使用这个样式文件的地方导入 SongListWrapper ,然后就可以使用<SongListWrapper></SongListWrapper>来包裹html标签
// 它本质上其实是个<div />,不过它会自动给这个<div /> 生成类名 可在浏览器控制台中查看
export const SongListWrapper = styled.div`
position: absolute;
z-index: 50;
top: 0;
left: 0;
/* 这里的props接收的是从<SongListWrapper ref={songScrollWrapper} play={songsCount}></SongListWrapper> 中的play传递的参数 */
bottom: ${props => props.play ? "60px": 0};
right: 0;
>div{
position: absolute;
left: 0;
width: 100%;
overflow: visible;
}
`
// 对于全局变量的接收:background: ${style["theme-color"]}
// 对于全局方法的使用:${style.noWrap()}
在使用styled-components时,背景图片的设置需要你把背景图片先导入进来在使用变量的方式传递给url,不然图片不会显示:
import TabBarImg from '../assets/images/homeLayout/tabBar.jpg';
//正确使用:background-image: url(${TabBarImg});
//错误使用: background-image: url('../assets/images/homeLayout/tabBar.jpg');
要使用你定义的styled-components文件,可以直接在头部引入,如:
import { ImgWrapper, CollectButton, SongListWrapper, BgLayer } from "./style";
function Test() {
return (
<>
<ImgWrapper></ImgWrapper>
<collectButton><SongListWrapper></SongListWrapper></collectButton>
</>
)
}
如果想要更深入的了解的话可以去看看官方文档。
2. React Hooks
在三元的项目里没有再去使用class形式的react编程风格,改用了最新的React Hooks来进行编写,这也是未来的趋势,如果还在使用class编写的同学们可以考虑改用React Hooks了,Hooks已经相当稳定了,并且能够完成所有的class组件的功能,而且也是官方所推荐的,我在这里就介绍三元歌手详情页面用到的这些:
// 定义ref ,可以通过console.log在控制台打印输出 imageWrapper 查看相关信息
// 使用的话通过给html标签加上ref={imageWrapper}, 如:<div ref={imageWrapper}></div>
// 就可以获取到这个div的相关信息,
const imageWrapper = useRef();
// 与类组件中的 state 差不多,第一个参数 showStatus 是你自己定义的变量名称,
// 第二个参数 setShowStatus 是用来设置这个变量值的函数,如:setShowStatus(false) 将变量的值改为false
// 默认命名规则为 set+第一个参数的名称,你要定义成其它的也没啥问题,不过不推荐
// useState(true) 表示 showStatus 的初始值为 true
const [showStatus, setShowStatus] = useState(true);
// 相当于原来类组件的三个状态 挂载时,更新时,卸载时 (componentDidMount,componentDidUpdate,componentWillUnmount)
useEffect(() => {
// 在页面的内容渲染完毕后调用 (componentDidMount,componentDidUpdate)
console.log(tets);
return () => {
// (componentWillUnmount)
// 在这里处理卸载,如 clearInterval 类似的操作
}
}, [count]) // useEffect的第二参数是依赖,传入count表示仅在 count 更改时更新 effect
// 对函数做缓存,第二参数同样是依赖,仅在依赖改变时更新函数,不然就使用缓存的版本,用来优化性能, [] 表示一直使用缓存中的,不更新函数
const setShowStatusFalse = useCallback(() => {
setShowStatus(false);
}, [])
更多的详细内容可以去React官网查看相关文档,官方文档讲解的挺清楚的,所以我在这里就不过多介绍了。
3. react-transition-group
这个插件类似Vue中<transition />
就是用来实现组件切换的动画效果,我在这里也附上它的官方文档,这个插件只是给你提供了一些状态,具体啥动画还是要你自己写的:
<CSSTransition
in={showStatus}
timeout={300}
classNames="fly"
appear={true}
unmountOnExit
onExited={() => props.history.goBack()}
>
</CSSTransition>
- in: 显示组件,触发进入或退出状态
- timeout: 动画时间
- classNames: 当你自己定义动画效果的时候就使用如:.fly-enter,.fly-enter-active类似这种,fly就是你classNames定义的,后面是这个组件自己默认的。
- appear:说白了就是第一次挂载加不加载动画
- unmountOnExit:在达到退出状态后卸载组件
- onExited:达到退出状态后触发的回调函数
export const Container = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: ${props => props.play > 0 ? "60px": 0};
width: 100%;
z-index: 100;
overflow: hidden;
background: #f2f3f4;
transform-origin: right bottom;
/* 触发动画要用到的类,fly是你在classNames中定义的,-enter,-某某某 则是这个插件默认的各个时期的类名,
至于这些类名会在啥时候触发,你可以去浏览器控制台选择这个元素,然后出发动画,看看什么时候会加上什么类名 */
&.fly-enter, &.fly-appear{
transform: rotateZ(30deg) translate3d(100%, 0, 0);
}
&.fly-enter-active, &.fly-appear-active{
transition: transform .3s;
transform: rotateZ(0deg) translate3d(0, 0, 0);
}
&.fly-exit{
transform: rotateZ(0deg) translate3d(0, 0, 0);
}
&.fly-exit-active{
transition: transform .3s;
transform: rotateZ(30deg) translate3d(100%, 0, 0);
}
`
4. Redux、immutable
redux
神三元在 redux 这块的操作不同于我们过去把所有的 action 和 reducer 统一丢到一个 store 文件夹下,神三元将他们拆分,分别放到各自的组件目录下进行管理,各个组件管理各自的 reudx,其它的倒是和正常的使用没啥区别。如果对于Redux这一块有什么不懂的建议去看视频学习,毕竟这算是 react 的基础,要写的话也不知道要写多少多细才够……,关于 redux 的视频网上有很多,这里推荐可以去B站看看技术胖的视频来学习。
immutable基础知识
Immutable data encourages pure functions (data-in, data-out) and lends itself to much simpler application development and enabling techniques from functional programming such as lazy evaluation. -- 官方文档对其描述
Immutable data 说白了就是创建后就不可变的数据。Immutable 提供了许多持久的不可变数据结构,包括:List, Stack, Map, OrderedMap, Set, OrderedSet 和 Record。通过使用结构共享,这些数据结构在现代JavaScript虚拟机上非常高效,可以将复制或缓存数据的需求降至最低。
来介绍下 immutable 的一些基础操作:
// Map类型数据的初始化操作
var map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('b', 50);
// 取值
map1.get('b'); // 2
map2.get('b'); // 50
// List类型数据的初始化操作
const list1 = List([ 1, 2 ]);
const list2 = list1.push(3, 4, 5);
const list3 = list2.unshift(0);
const list4 = list1.concat(list2, list3);
console.log(list1) // ListI [1, 2]
console.log(list2) // ListI [1, 2, 3, 4, 5]
console.log(list4) // ListI [1, 2, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]
const obj = { 1: "one" };
const map = fromJS(obj); // 将 obj 转化为 Immutable 的数据类型
console.log(map) // MapI {"1" ⇒ "one"}
console.log(map.get("1"), map.get(1)); // "one", undefined
const test = fromJS([1,2,3]);
console.log(test) // ListI [1, 2, 3]
// toObject toArray 可以将 Immutable 类型的数据转换为普通的JS类型数据,不过只能浅拷贝
// toJS 可以将 Immutable 类型的数据转换为普通的JS类型数据,属于深拷贝
const deep = Map({ a: 1, b: 2, c: List([ 3, 4, 5 ]) });
console.log(deep.toObject()); // { a: 1, b: 2, c: List [ 3, 4, 5 ] }
console.log(deep.toArray()); // [ 1, 2, List [ 3, 4, 5 ] ]
console.log(deep.toJS()); // { a: 1, b: 2, c: [ 3, 4, 5 ] }
更多的内容请参考immutable的官方文档
其它
better-scroll的话可以参照着三元项目里封装的scroll,这个插件不难使用,最大的问题就是刚开始使用时需要的参数有点难找……,嘛,多用几次多踩点坑就会了……
剩下的话,我就对这个歌手详情页面剩下的可能有问题的地方进行解释,解释在代码的注释中:
import React, { useState, useEffect, useRef, useCallback } from "react";
import { Container } from "./style";
import Header from "../../baseUI/header/index";
import { ImgWrapper, CollectButton, SongListWrapper, BgLayer } from "./style";
import Scroll from "../../baseUI/scroll/index";
import { HEADER_HEIGHT } from "./../../api/config";
import { getSingerInfo } from "./store/actionCreators";
import { connect } from "react-redux";
import Loading from "./../../baseUI/loading/index";
import { EnterLoading } from "../Singers/style";
import { changeEnterLoading } from "./store/actionCreators";
import { CSSTransition } from "react-transition-group";
import SongsList from "../SongList/";
import MusicNote from "../../baseUI/music-note/index";
function Singer(props) {
const initialHeight = useRef(0);
const [showStatus, setShowStatus] = useState(true);
const OFFSET = 5;
const {
artist: immutableArtist,
songs: immutableSongs,
loading,
songsCount
} = props;
const { getSingerDataDispatch } = props;
// 用toJS()深度转换,将数据还原为普通的js数据
const artist = immutableArtist.toJS();
const songs = immutableSongs.toJS();
const collectButton = useRef();
const imageWrapper = useRef();
const songScrollWrapper = useRef();
const songScroll = useRef();
const header = useRef();
const layer = useRef();
const musicNoteRef = useRef();
useEffect(() => {
const id = props.match.params.id;
// 调用dispatch触发action中的方法
getSingerDataDispatch(id);
let h = imageWrapper.current.offsetHeight;
initialHeight.current = h;
songScrollWrapper.current.style.top = `${h - OFFSET}px`;
//把遮罩先放在下面,以裹住歌曲列表
layer.current.style.top = `${h - OFFSET}px`;
songScroll.current.refresh();
// eslint-disable-next-line
}, []); // 这里写个空数组表示每次依赖都不变,使用类似缓存中的版本,不进行更新,实现了性能上的优化
const handleScroll = pos => {
let height = initialHeight.current;
const newY = pos.y;
const imageDOM = imageWrapper.current;
const buttonDOM = collectButton.current;
const headerDOM = header.current;
const layerDOM = layer.current;
const minScrollY = -(height - OFFSET) + HEADER_HEIGHT;
const percent = Math.abs(newY / height);
//说明: 在歌手页的布局中,歌单列表其实是没有自己的背景的,layerDOM其实是起一个遮罩的作用,给歌单内容提供白色背景
//因此在处理的过程中,随着内容的滚动,遮罩也跟着移动
if (newY > 0) {
//处理往下拉的情况,效果:图片放大,按钮跟着偏移
imageDOM.style["transform"] = `scale(${1 + percent})`;
buttonDOM.style["transform"] = `translate3d(0, ${newY}px, 0)`;
layerDOM.style.top = `${height - OFFSET + newY}px`;
} else if (newY >= minScrollY) {
//往上滑动,但是还没超过Header部分
layerDOM.style.top = `${height - OFFSET - Math.abs(newY)}px`;
layerDOM.style.zIndex = 1;
imageDOM.style.paddingTop = "75%";
imageDOM.style.height = 0;
imageDOM.style.zIndex = -1;
buttonDOM.style["transform"] = `translate3d(0, ${newY}px, 0)`;
buttonDOM.style["opacity"] = `${1 - percent * 2}`;
} else if (newY < minScrollY) {
//往上滑动,但是超过Header部分
layerDOM.style.top = `${HEADER_HEIGHT - OFFSET}px`;
layerDOM.style.zIndex = 1;
//防止溢出的歌单内容遮住Header
headerDOM.style.zIndex = 100;
//此时图片高度与Header一致
imageDOM.style.height = `${HEADER_HEIGHT}px`;
imageDOM.style.paddingTop = 0;
imageDOM.style.zIndex = 99;
}
};
const musicAnimation = (x, y) => {
musicNoteRef.current.startAnimation({ x, y });
};
const setShowStatusFalse = useCallback(() => {
setShowStatus(false);
}, []) // 这里写个空数组表示依赖一直相同,返回该回调函数的 memoized 版本,回调函数不会更新,同样对性能进行了优化
return (
<CSSTransition
in={showStatus}
timeout={300}
classNames="fly"
appear={true}
unmountOnExit
onExited={() => props.history.goBack()}
>
<Container>
<Header
handleClick={setShowStatusFalse}
title={artist.name}
ref={header}
></Header>
<ImgWrapper ref={imageWrapper} bgUrl={artist.picUrl}>
<div className="filter"></div>
</ImgWrapper>
<CollectButton ref={collectButton}>
<i className="iconfont"></i>
<span className="text">收藏</span>
</CollectButton>
<BgLayer ref={layer}></BgLayer>
<SongListWrapper ref={songScrollWrapper} play={songsCount}>
<Scroll onScroll={handleScroll} ref={songScroll}>
<SongsList
songs={songs}
showCollect={false}
usePageSplit={false}
musicAnimation={musicAnimation}
></SongsList>
</Scroll>
</SongListWrapper>
{loading ? (
<EnterLoading style={{ zIndex: 100 }}>
<Loading></Loading>
</EnterLoading>
) : null}
<MusicNote ref={musicNoteRef}></MusicNote>
</Container>
</CSSTransition>
);
}
// 映射Redux全局的state到组件的props上
const mapStateToProps = state => ({
artist: state.getIn(["singerInfo", "artist"]),
songs: state.getIn(["singerInfo", "songsOfArtist"]),
loading: state.getIn(["singerInfo", "loading"]),
songsCount: state.getIn(["player", "playList"]).size
});
// 映射dispatch到props上
const mapDispatchToProps = dispatch => {
return {
getSingerDataDispatch(id) {
dispatch(changeEnterLoading(true));
dispatch(getSingerInfo(id));
}
};
};
// 将ui组件包装成容器组件
// connect 将React组件连接到Redux仓库
export default connect(
mapStateToProps,
mapDispatchToProps
)(React.memo(Singer)); // React.memo 可以来解决 函数组在传入的props不变是会重新弄渲染的问题
那么到这里就结束啦,希望你在阅读完后能够有所收获,我的文章都是学习过程中的总结,感谢阅读。
参考
styled-components 官方文档
React Hooks 官方文档
react-transition-group 官方文档
redux 官方文档
immutable 官方文档
better-scroll 官方文档
神三元 - React Hooks 与 Immutable 数据流实战
云音乐 github 地址、项目在线地址