react hooks + redux + immutable.js
后端部分:采用 github 上开源的 NodeJS 版 api 接口 NeteaseCloudMusicApi,提供音乐数据。本项目使用新版本的依赖进行构建(如:react-router v6
),新版本依赖的使用可以参考本文,具体知识点的学习可以查看小册链接。参考我的github仓库代码:CloudMusic
目前项目完成了首页展示(其他页面类似)。
一、全局样式与路由配置 1.项目搭建 过程查看文档:creat-react-app 2. 全局样式(1)下载依赖
这个项目利用的是css in js
,所以我们先安装:styled-components
yarn add styled-components
它的作用就是让我们能够使用 js
来书写 css
样式。如果是用 vsCode
写这个项目的话,可以搜索 vscode-styled-components
这个插件来辅助我们书写代码。
(2)全局样式
import { createGlobalStyle } from 'styled-components';
export const GlobalStyle = createGlobalStyle `
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
html, body{
background: #f2f3f4;;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a{
text-decoration: none;
color: #fff;
}
`
这段代码是用来定义全局样式的。所有 styled-components
的代码都需要通过引入 styled-components
插件后,通过 export const GlobalStyle = createGlobalStyle
的方式使用。在这里面我们可以书写 CSS
代码,代码的格式类似于 Less
。
(3)图标文件
在src
的 assets
目录下,创建一个 iconfont
文件夹,里面放我们我们的图标文件。可以通过我的项目源码获取到。把 iconfont.css
文件改成 iconfont.js
文件在 iconfont.js
里面引入 styled-components
保留 @font-face
和 .iconfont
的内容,其他的删掉。
(4)导入
function App() {
return (
<div className="App">
<GlobalStyle></GlobalStyle>
<IconStyle></IconStyle>
<div>组件</div>
</div>
);
}
export default App;
3. 路由配置
(1)下载依赖
react-router
react-router-dom
yarn add react-router react-router-dom
我们利用 react-router-dom
来写路由。这里使用的是的react-router-dom v6
,不需要下载react-router-config
新版本的react-router-dom
与老版本还是有比较多的区别的,可以查看官网进行了解。
(2)书写路由文件
第一个路由指定'/'
是进入的主界面,同时二级路由显示的是 Recommend
组件也就是推荐组件的内容。exact
是精确匹配的意思,当通过 '/'
进入主界面的时候,会进行重定向 *** 作。
import React from "react";
import { useRoutes, Navigate } from 'react-router-dom';
import Home from '../application/Home';
import Recommend from '../application/Recommend';
import Singers from '../application/Singers';
import Rank from '../application/Rank';
function Routes() {
const routes = useRoutes([
{
path: '/',
element: <Home />,
children: [
{ path: '/', exact: true, element: <Navigate to='/recommend'/> },
{ path: '/recommend', element: <Recommend /> },
{ path: '/rank', element: <Rank /> },
{ path: '/singers', element: <Singers /> }
]
},
])
return routes
}
export default Routes;
(3)导入路由文件
import { GlobalStyle } from './style';
import { IconStyle } from './assets/iconfont/iconfont';
import { HashRouter } from 'react-router-dom';
import Routes from './routes';
function App() {
return (
<HashRouter>
<GlobalStyle></GlobalStyle>
<IconStyle></IconStyle>
<Routes/>
</HashRouter>
);
}
export default App;
(4)编写组件
在application
文件夹下新建 Home
文件夹,然后新建 index.js
文件
import React from 'react';
import { Outlet } from 'react-router';
function Home() {
return (
<div>Home</div>
)
}
export default React.memo(Home);
React.memo
是用来控制组件渲染的,类似于 PureComponent
类似的编写 Recommend
、Singers
、Rank
组件。现在启动项目,可以看到Home,但是还需要看到recommend。也就是说,现在只渲染了一级路由。Home组件需要修改一下。
import React from 'react';
import { Outlet } from 'react-router';
function Home() {
return (
<div>
Home
<Outlet /> // 用于渲染二级路由
</div>
)
}
export default React.memo(Home);
到这里,我们就可以看到渲染出来的内容了,项目也就基本搭建好了。
二、头部和Tab栏 1. 全局样式(1)下载依赖
使用flexible.js
yarn add lib-flexible
在 src
的 App.js
中导入 flexible
:
import 'lib-flexible'
flexible.js
来做适配,因为 flexible.js
是把设备划分为 `10份
(2)书写全局样式
assets
目录下新建 global-style.js
文件
// 这里定义的全局的通用属性
// 扩大可点击区域
const extendClick = () => {
return`
position: relative;
&:before {
content: '';
position: absolute;
top: -.266667rem;
bottom: -.266667rem;
left: -.266667rem;
right: -.266667rem;
}
`
}
// 一行文字溢出部分用 …… 代替
const noWrap = () => {
return`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`
}
export default {
'theme-color': '#d44439', // 主题颜色——网易云红
'theme-color-shadow': 'rgba(212, 68, 57, .5)', // 主题颜色——暗色
'font-color-light': '#f1f1f1', // 字体颜色——高亮灰白色
'font-color-desc': '#2E3030', // 字体颜色——黑灰色
'font-color-desc-v2': '#bba8a8', // 字体颜色——带红色的深灰色
'font-size-ss': '10px', // 字体大小——极小
'font-size-s': '12px', // 字体大小——小
'font-size-m': '14px', // 字体大小——正常
'font-size-l': '16px', // 字体大小——大
'font-size-ll': '18px', // 字体大小——极大
'border-color': '#e4e4e4', // 边框颜色——白灰色
'background-color': '#f2f3f4', // 背景颜色——银灰色
'background-color-shadow': 'rgba(0, 0, 0, 0.3)', // 背景颜色——深灰黑色
'hightlight-background-color': '#fff', // 背景颜色——白色
extendClick,
noWrap
}
这里使用rem布局,可以使用 cssrem
这个插件来帮助我们进行计算,只要记得把 Cssrem: Root Font Size
的大小设置为 75
就好了。具体的原因请自行百度。值得注意的是我们这里的字体大小并没有使用 rem
,依然使用的是 px
。但是我们使用的图标需要使用 rem
,这里可能不会被注意到.因为我们不希望字体随屏幕变化,但是图标我们希望它是自适应的。
2. 顶部栏开发
(1)书写顶部栏样式
使用 styled-components
来做组件
Home文件夹下新建style.js
文件
import styled from 'styled-components';
import style from '../../assets/global-style';
export const Top = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 5px 10px;
background: ${style["theme-color"]};
&>span {
line-height: 1.066667rem;
color: #f1f1f1;
font-size: 20px;
&.iconfont {
font-size: .666667rem;
}
}
`
styled-components
和 less
一样,允许我们使用变量。
我们导入了全局样式 global-style
,并使用里面预先设定好的颜色:theme-color
。
这里是用 js
语法写的。所以变量的使用方法是:${style["theme-color"]}
,这里是对象解构的写法。
使用样式组件
import React from 'react';
import { Outlet } from 'react-router';
import { Top, Tab, TabItem } from './style';
import { NavLink } from 'react-router-dom';
function Home() {
return (
<div>
<Top>
<span className="iconfont menu"></span>
<span className="title">WebApp</span>
<span className="iconfont search"></span>
</Top>
<Outlet />
</div>
)
}
export default React.memo(Home);
现在就启动服务,就可以看见效果了。
3. Tab栏开发(1)书写Tab栏样式
export const Tab = styled.div`
height: 1.066667rem;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: ${style['theme-color']};
`;
export const TabItem = styled.div`
height: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
现在的 Tag
组件
<Tab>
<TabItem><span>推荐span>TabItem>
<TabItem><span>歌手span>TabItem>
<TabItem><span>排行榜span>TabItem>
Tab>
(2)引入路由
import { NavLink } from 'react-router-dom';
修改tab
<Tab>
<NavLink to="/recommend" activeClassName="selected"><TabItem><span>推荐span>TabItem>NavLink>
<NavLink to="/singers" activeClassName="selected"><TabItem><span>歌手span>TabItem>NavLink>
<NavLink to="/rank" activeClassName="selected"><TabItem><span>排行榜span>TabItem>NavLink>
Tab>
使用
组件,会变成 a
标签,修改原来的Tab的样式:
export const Tab = styled.div`
height: 1.066667rem;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: ${style['theme-color']};
a {
flex: 1;
padding: .053333rem 0;
font-size: .373333rem;
color: #e4e4e4;
&.selected {
span {
padding: .08rem 0;
font-weight: 700;
color: #f1f1f1;
border-bottom: .053333rem solid #f1f1f1;
}
}
}
`;
我们给三个
都写了 activeClassName="selected"
,这样当我们选中哪一个路由时,哪一个路由就有了 selected
的样式。
启动项目,查看效果:
三、引入ReduxRedux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。 | Redux 中文官网
1. 安装对应的依赖redux redux-thunk redux-immutable react-redux immutable
reudx-thunk
是中间件,类似的还有 saga
,这里我们使用的是 thunk
。
immutable
是 Facebook
开发一个 持久化数据结构
,它是一经创建变不可修改的数据,普遍运用于 redux
中。
在 src
的 store
目录下,创建 index.js
和 reducer.js
reducer.js
用来整合其他仓库的 reducer
的数据的,而辅助函数 combineReducers
就用来帮我们完成这件事。
import { combineReducers } from 'redux-immutable';
export default combineReducers({
})
3. 创建store
这里使用了增强函数,在启用谷歌调试工具 redux
的基础上使用了中间件 thunk
。
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
// compose 做的只是让你在写深度嵌套的函数时,避免了代码的向右偏移
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 生成数据
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
))
4. 引入store
使用 react-redux
的 Provider
方法传递store。
store能被所有被
包裹的组件使用。
import { GlobalStyle } from './style';
import { IconStyle } from './assets/iconfont/iconfont';
import { HashRouter } from 'react-router-dom';
import Routes from './routes';
import 'lib-flexible'
import { Provider } from 'react-redux';
import store from './store/index';
function App() {
return (
<Provider store={store}>
<HashRouter>
<GlobalStyle></GlobalStyle>
<IconStyle></IconStyle>
<Routes/>
</HashRouter>
</Provider>
);
}
export default App;
四、轮播图和推荐列表
1. 轮播图组件
先制作轮播图组件:
import React from 'react';
import Slider from '../../components/slider';
function Recommend(props) {
const bannerList = [1, 2, 3, 4].map(item => {
return { imageUrl: "http://www.kaotop.com/file/tupian/20220523/109951164331219056.jpg" }
})
return (
<div>
<Slider bannerList={bannerList}></Slider>
</div>
)
}
export default React.memo(Recommend);
src/components
下新建一个 slilder
目录及其 index.js
文件夹:
import React from 'react';
function Slider(props) {
const { bannerList } = props
return (
<div>
{
bannerList.map((slider, index) => {
return (
<img key={index} src={slider.imageUrl} width="100%" height="100%" alt="推荐" />
);
})
}
</div>
)
}
export default React.memo(Slider)
2. 使用 swiper 插件
这里使用新版本的swiper,和旧版有区别,可以查看官网swiper官网,或者下载旧版。
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay} from 'swiper';
// 引入swiper样式
import 'swiper/css';
import 'swiper/css/pagination';
function Slider(props) {
const {bannerList} = props
return (
<Swiper
modules={[Pagination, Autoplay]}
loop={true} // 开启环路
pagination={{ // 分页器
el: '.swiper-pagination',
type: 'bullets' }}
autoplay={{ // 自动播放
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: false}}>
{
bannerList.map((item, index) => {
return (
<SwiperSlide key={index}>
<img src={item.imageUrl} width="100%" height="100%" alt="推荐"/>
</SwiperSlide>
)
})
}
</Swiper>
)
}
export default React.memo(Slider)
调节 swiper 样式
// style.js
import styled from 'styled-components';
import style from '../../assets/global-style';
// 整体样式
export const SliderContainer = styled.div `
position: relative;
box-sizing: border-box;
width: 100%;
// 背景色
.before{
position: absolute;
top: 0;
height: 60%;
width: 100%;
background: ${style["theme-color"]};
}
// 轮播图样式
.swiper{
position: relative;
width: 98%;
overflow: hidden;
margin:auto;
border-radius: .16rem;
}
// 自定义分页器样式
.swiper-pagination-bullet-active{
background: ${style["theme-color"]};
}
`;
引入样式
import React from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay} from 'swiper';
import 'swiper/css';
import 'swiper/css/pagination';
import {SliderContainer} from './style'
function Slider(props) {
const {bannerList} = props
return (
<SliderContainer>
// 背景色
<div className="before"></div>
<Swiper
modules={[Pagination, Autoplay]}
loop={true} // 开启环路
pagination={{
el: '.swiper-pagination',
type: 'bullets' }}
autoplay={{
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: false}}>
{
bannerList.map((item, index) => {
return (
<SwiperSlide key={index}>
<img src={item.imageUrl} width="100%" height="100%" alt="推荐"/>
</SwiperSlide>
)
})
}
</Swiper>
// 不是必须的,为了自定义分页器样式
<div className="swiper-pagination"></div>
</SliderContainer>
)
}
export default React.memo(Slider)
3. 推荐列表
数据模拟(和轮播图类似):
import List from '../../components/list';
// 推荐列表数据
const recommendList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, index) => {
return {
id: index,
picUrl: "http://www.kaotop.com/file/tupian/20220523/18999560928537533.jpg",
playCount: 17171122,
name: "[洗澡时听的歌] 浴室里听歌吹泡泡o○o○o○"
}
});
<List recommendList={recommendList}></List>
JSX:
import React from 'react';
function RecommendList(props) {
const { recommendList } = props
return (
<div>
{
recommendList.map((item, index) => {
return (
<img key={index} src={item.picUrl} alt={item.name} />
)
})
}
</div>
)
}
export default React.memo(RecommendList)
样式
import { ListWrapper, ListTitle, List, ListItem} from './style';
<ListWrapper>
<ListTitle>
<div className='title'>推荐歌单</div>
<div className='tag'>歌单广场</div>
</ListTitle>
<List>
{
recommendList.map((item, index) => {
return (
<ListItem>
<img key={index} src={item.picUrl} alt={item.name} />
</ListItem>
)
})
}
</List>
</ListWrapper>
样式文件
import styled from 'styled-components';
import style from '../../assets/global-style';
export const ListWrapper = styled.div`
position: relative;
width: 100%;
`
export const ListTitle = styled.div`
overflow: hidden;
line-height: 1.066667rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.title {
font-size: .373333rem;
font-weight: 700;
padding-left: .16rem;
}
.tag {
height: .266667rem;
font-size: .266667rem;
font-weight: 600;
padding: .053333rem .16rem;
margin-right: .16rem;
line-height: .266667rem;
color: #444;
border: .026667rem solid rgb(211, 210, 210);
border-radius: .213333rem;
}
`
export const List = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
`
export const ListItem = styled.div`
box-sizing: border-box;
flex: 33.33%;
padding: 0 .16rem .16rem .16rem;
img {
width: 100%;
}
`
接下来,更改
ListItem
的布局样式
减小请求图片的大小
添加播放次数
修改key的位置
<ListItem key={item.id}>
<div className="img_wrapper">
<div className="decorate"></div>
{/* 加此参数可以减小请求的图片资源大小 */}
<img src={item.picUrl + "?param=300x300"} width="100%" height="100%" alt="music" />
<div className="play_count">
<i className="iconfont play"></i>
<span className="count">{item.playCount}</span>
</div>
</div>
<div className="desc">{item.name}</div>
</ListItem>
样式文件
export const ListItem = styled.div`
position: relative;
width: 32%;
.img_wrapper{
.decorate {
position: absolute;
top: 0;
width: 100%;
height: .933333rem;
border-radius: .08rem;
background: linear-gradient(hsla(0,0%,43%,.4),hsla(0,0%,100%,0));
}
position: relative;
height: 0;
padding-bottom: 100%;
.play_count {
position: absolute;
right: .053333rem;
top: .053333rem;
font-size: .32rem;
line-height: .4rem;
color: ${style["font-color-light"]};
.play{
font-size: .426667rem;
vertical-align: top;
}
}
img {
position: absolute;
width: 100%;
height: 100%;
border-radius: .08rem;
}
}
.desc {
overflow: hidden;
margin-top: .053333rem;
padding: 0 .053333rem;
height: 1.333333rem;
text-align: left;
font-size: .32rem;
line-height: 1.4;
color: ${style["font-color-desc"]};
}
`
修改播放量数据格式
在src/api
下面新建一个 utils.js
文件
export const getCount = (count) => {
if (count < 0) return;
if (count < 10000) {
return count;
} else if (Math.floor(count / 10000) < 10000) {
return Math.floor(count / 1000) / 10 + "万";
} else {
return Math.floor(count / 10000000) / 10 + "亿";
}
}
规范格式
import { getCount } from "../../api/utils";
<span className="count">{getCount(item.playCount)}</span>
五、Scroll 基础组件
better-scroll
之前页面展示没有问题了,但是滚动页面时,是整个页面滚动。现在我希望顶部固定,滑动下面歌曲内容的时候,只有这部分出现滚动,现在实现一下。 1. 引入scroll插件实现:滑动页面、下拉刷新、上拉刷新
(1)下载依赖
yarn add better-scroll@next
(2)大致结构
baseUI
中新建一个 scroll
文件夹及其 index.js
文件,大致结构如下
import React,{ useEffect, useState, useEffect, useRef } from 'react';
import BScroll from "better-scroll";
const Scroll = forwardRef((props, ref) => {
return (
<ScrollContainer ref={scrollContaninerRef}>
{props.children}
</ScrollContainer>
);
})
export default Scroll;
ScrollContainer
是样式组件
forwarRef
可以使得在父组件中可以得到子组件中的 DOM
节点。这里的ScrollContainer是可以作为DOM原生被父组件获取到。
滚动的方向
vertical
, horizental
滚动的位置
probeType: 3
:不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。
点击事件
click: true
,默认是阻止的。
回d
bounce: { top: bounceTop, bottom: bounceBottom}
,滑动超过顶部和底部边界时回d效果
import React,{ useEffect, useState, useEffect, useRef } from 'react';
import BScroll from "better-scroll";
const Scroll = forwardRef((props, ref) => {
// 初始化scroll实例对象,用于配置
const [bScroll, setBScroll] = useState();
// scrollContaninerRef.current指向初始化bs实例需要的DOM元素
const scrollContaninerRef = useRef();
// 创建 better-scroll实例
useEffect(() => {
const scroll = new BScroll(scrollContaninerRef.current, {
scrollX: direction === "horizental",
scrollY: direction === "vertical",
probeType: 3,
click: true,
bounce: {
top: bounceTop,
bottom: bounceBottom
}
});
setBScroll(scroll);
return () => { // 清除函数
setBScroll(null);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ScrollContainer ref={scrollContaninerRef}>
{props.children}
</ScrollContainer>
);
})
export default Scroll;
重新渲染
每次重新渲染都要刷新实例,防止无法滑动useEffect(() => {
if(refresh && bScroll){
bScroll.refresh();
}
});
3. 绑定 scroll 事件
在滑动时触发 onScroll
,传入scroll实例
useEffect(() => {
if(!bScroll || !onScroll) return;
bScroll.on('scroll', (scroll) => {// 绑定
onScroll(scroll);
})
return () => {
bScroll.off('scroll');// 解除绑定
}
}, [onScroll, bScroll]);
4. 上拉刷新
滑动超过底部边界时,触发pullup()回d
useEffect(() => {
if(!bScroll || !pullUp) return;
bScroll.on('scrollEnd', () => {
//判断是否滑动到了底部
if(bScroll.y <= bScroll.maxScrollY + 100){
pullUp();
}
});
return () => {
bScroll.off('scrollEnd');
}
}, [pullUp, bScroll]);
5. 下拉刷新
下拉超过顶部边界时,触发pullDown()回d。
useEffect(() => {
if(!bScroll || !pullDown) return;
bScroll.on('touchEnd', (pos) => {
//判断用户的下拉动作
if(pos.y > 50) {
pullDown();
}
});
return () => {
bScroll.off('touchEnd');
}
}, [pullDown, bScroll]);
6. 暴露方法
暴露给使用scroll组件地方调用的刷新方法使用的方案是 React Hooks
中的 useImperativeHandle
// 一般和forwardRef一起使用,ref已经在forWardRef中默认传入
useImperativeHandle(ref, () => ({
//给外界暴露refresh方法
refresh() {
if(bScroll) {
bScroll.refresh();
bScroll.scrollTo(0, 0);
}
},
//给外界暴露getBScroll方法, 提供bs实例
getBScroll() {
if(bScroll) {
return bScroll;
}
}
}));
7. 设置默认props
scroll组件中默认props
Scroll.defaultProps = {
direction: "vertical",
click: true,
refresh: true,
onScroll:null,
pullUpLoading: false,
pullDownLoading: false,
pullUp: null,
pullDown: null,
bounceTop: true,
bounceBottom: true
};
8. 类型检查
import PropTypes from "prop-types";
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//是否支持向上吸顶
};
给组件添加样式
import styled from 'styled-components';
const ScrollContainer = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
`;
9. Recommend中使用
引入
import Scroll from '../../baseUI/scroll';
JSX代码
用scroll组件包裹前面的内容,滑动的时候,scroll组件没有变化,里面的内容出现滚动效果。<Content>
<Scroll className="list">
<div>
<Slider bannerList={bannerList}></Slider>
<List recommendList={recommendList}></List>
</div>
</Scroll>
</Content>
content 是样式组件
import styled from 'styled-components';
export const Content = styled.div`
position: fixed;
top: 2.4rem;
bottom: 0;
width: 100%;
max-width: 750px;
`
为什么给content加绝对定位
我们滑动时,滑动的不是
这个组件,而是它里面的内容。前面给了
组件一个高度和宽度,都是相对于父容器 Content
的 100%
。那是因为我们没有给我们的 Header
组件加绝对定位,如果不给 Recommend
加绝对定位的话,我们滑动时滑动的会是整个页面。由于 Recommend
脱离了文档流,有固定的宽高,所以我们才能看见滑动的功能。
10. 调整样式
顶部下拉过程中间会有一段空白,这是因为设置了背景色导致的。直接加大高度解决这个问题。
.before{
position: absolute;
top: -8rem;
height: 10.666667rem;
width: 100%;
background: ${style["theme-color"]};
}
六、请求获取数据
前面内容都是模拟数据展示的,现在使用网络请求的方式获取数据。先去 GitHub
上面 clone
这个项目:GitHub网易云音乐接口,然后把它运行在其他端口上,保证不和当前前端服务端口冲突。(可能需要修改端口)
1. axios
安装依赖
yarn add axios
配置 axios
api
文件夹里,在这个文件夹下面创建 config.js
文件
主要两点:
在所有请求前方加上http://localhost:3300
,即让所有数据从这个端口号请求响应拦截,请求失败处理
还有:
请求超时时间带cookie响应数据类型…等import axios from 'axios';
export const baseUrl = 'http://localhost:4000';
// 创建axios的实例
const axiosInstance = axios.create({
baseURL: baseUrl,
timeout: 5000, // 请求超时时间
responseType: "json",
withCredentials: true, // 带cookie
headers: {
"Content-Type": "application/json;charset=utf-8"
}
});
// 响应拦截器【响应拦截器的作用是在接收到响应后进行一些 *** 作】
axiosInstance.interceptors.response.use(
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
res => res.data,
// 服务器状态码不是2开头的的情况
err => {
console.log(err, "网络错误");
}
);
export {
axiosInstance
};
2. 封装不同的网络请求
这里封装了需要的两个接口,到时候直接调用即可。
// request.js
import { axiosInstance } from "./config";
export const getBannerRequest = () => {
return axiosInstance.get('/banner');
}
export const getRecommendListRequest = () => {
return axiosInstance.get('/personalized');
}
3. reudx 开发
在 Recommend
目录下,新建 store
文件夹
(1)常量集合
常量集合,存放不同action的type值
//constants.js
export const CHANGE_BANNER = 'recommend/CHANGE_BANNER';
export const CHANGE_RECOMMEND_LIST = 'recommend/RECOMMEND_LIST';
(2)reducer
该组件的仓库
// reducer
// 获取常量
import * as actionTypes from './constants';
// 导入 immutable 的 frmoJS 方法
import { fromJS } from 'immutable';
// 这里用到fromJS把JS数据结构转化成immutable数据结构
const defaultState = fromJS({
bannerList: [],
recommendList: [],
});
export default (state = defaultState, action) => {
switch(action.type) {
case actionTypes.CHANGE_BANNER:
return state.set('bannerList', action.data);
case actionTypes.CHANGE_RECOMMEND_LIST:
return state.set('recommendList', action.data);
default:
return state;
}
}
导出分仓库
// index.js
// 导入仓库
import reducer from './reducer'
// 导入变量
import * as actionCreators from './actionCreators'
// 导出变量
export { reducer, actionCreators };
合并到主仓库
// src/reducer.js
// 合并 reducer 函数
import { combineReducers } from 'redux-immutable';
// 导入分仓库的 reducer
import { reducer as recommendReducer } from '../application/Recommend/store/index';
// 合并 reducer 函数为一个 obj
export default combineReducers({
recommend: recommendReducer,
})
(3)获取仓库数据
将数据映射Redux全局的state到组件的props上
// Recommend/index.js
const mapStateToProps = (state) => ({
// 不要再这里将数据toJS,不然每次diff比对props的时候都是不一样的引用,还是导致不必要的重渲染, 属于滥用immutable
bannerList: state.getIn(['recommend', 'bannerList']),
recommendList: state.getIn(['recommend','recommendList']),
})
把去掉的 mock
数据,通过prop获取
const { bannerList, recommendList } = props;
// 把 immutable 数据类型转换为对应的 js 数据类型
const bannerListJS = bannerList ? bannerList.toJS() : [];
const recommendListJS = recommendList ? recommendList.toJS() :[];
此时如果 redux
中有数据,我们就已经获取到了,可是现在 redux
中还没有数据,所以接下来我们用 axios
来获取数据。
提供用于修改仓库数据的方法
// actionCreators.js
// 导入常量
import * as actionTypes from './constants';
// 将JS对象转换成immutable对象
import { fromJS } from 'immutable';
// 导入网络请求
import { getBannerRequest, getRecommendListRequest } from '../../../api/request';
export const changeBannerList = (data) => ({
type: actionTypes.CHANGE_BANNER,
data: fromJS(data)
});
export const changeRecommendList = (data) => ({
type: actionTypes.CHANGE_RECOMMEND_LIST,
data: fromJS(data)
});
// 获取轮播图数据
export const getBannerList = () => {
return (dispatch) => {
// 发送请求
getBannerRequest().then(data => {
dispatch(changeBannerList(data.banners));
}).catch(() => {
console.log("轮播图数据传输错误");
})
}
};
// 获取推荐列表
export const getRecommendList = () => {
return (dispatch) => {
getRecommendListRequest().then(data => {
dispatch(changeRecommendList(data.result));
}).catch(() => {
console.log("推荐歌单数据传输错误");
});
}
};
(5)更新仓库数据
将方法映射dispatch到props上
const mapDispatchToProps = (dispatch) => {
return {
getBannerDataDispatch() {
dispatch(actionCreaters.getBannerList());
},
getRecommendListDataDispatch() {
dispatch(actionCreaters.getRecommendList());
},
}
}
在组件初次渲染的时候调用
const { getBannerDataDispatch, getRecommendListDataDispatch } = props;
// 当传空数组([])时,只会在组件 mount 时执行内部方法。
useEffect(() => {
getBannerDataDispatch();
getRecommendListDataDispatch();
}, []);
现在,就能够看到通过请求获取到的数据了。
七、优化 1. 图片懒加载 我们使用一个成熟的图片懒加载库:react-lazyload
来做我们的图片懒加载。
yarn add react-lazyload
在 components/list/index.js
中导入
import LazyLoad from "react-lazyload";
(1)添加占位图片
对 img
标签进行改造:
//img标签外部包裹一层LazyLoad
LazyLoad>
给 img
外面包裹了一层 LazyLoad
,它会有一个占位图片 music.png
,这个png文件在list文件夹下。
图片懒加载的原理:
在大量图片加载的情况下,会造成页面空白甚至卡顿。我们只让视口内的图片显示即可,同时图片未显示的时候给它一张默认的图片进行占位。滑动到占位图片的位置时,会请求真正的图片。(2)滑动加载图片
懒加载库提供了一个滑动加载图片的方法:forceCheck
给 Scroll
组件的 onScroll
传入 forceCheck
import {forceCheck} from 'react-lazyload'
// ...
<Scroll className="list" onScroll={forceCheck}>
2. 进场Loading效果
Ajax请求往往需要一定的时间,在这个时间内,页面会处于没有数据的状态,也就是空白状态。用户点击来的时候看见一片空白的时候心里是非常焦灼的,尤其是Ajax的请求时间长达几秒的时候。而loading效果便能减缓这种焦急的情绪,并且如果loading动画做的漂亮,还能够让人赏心悦目,让用户对App产生好感。
(1)loading效果制作
利用了CSS3的animation-lay特性,让两个圆交错变化,产生一个涟漪的效果。import React from 'react';
import styled,{ keyframes } from 'styled-components';
import style from '../../assets/global-style';
const loading = keyframes`
0%,100% {
transform: scale(0.0);
}
50% {
transform: scale(1.0);
}
`
const LoadingWrapper = styled.div`
>div {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
width: 1.6rem;
height: 1.6rem;
/* 不透明级 */
opacity: 0.6;
border-radius: 50%;
background-color: ${style["theme-color"]};
animation: ${loading} 1.4s infinite ease-in;
}
>div:nth-child(2) {
/* 定义动画何时开始 */
animation-delay: -0.7s;
}
`;
function Loading() {
return (
);
}
export default React.memo(Loading)
在 Recommend
组件中使用
import Loading from '../../baseUI/loading/index';
<Content>
...
<Loading></Loading>
<Content>
现在,就可以看到Loading效果了。但是Loading效果一直存在,挡住了页面。
(2)Loading的控制逻辑
Loading效果一开始时出现的,但是当页面请求获取到数据的时候,需要隐藏该效果。
使用enterLoading来进行控制,这里也使用redux。
// reducer.js
const defaultState = fromJS({
// ...
enterLoading: true
});
// ...
case actionTypes.CHANGE_ENTER_LOADING:
return state.set('enterLoading', action.data);
//constants.js
...
export const CHANGE_ENTER_LOADING = 'recommend/CHANGE_ENTER_LOADING';
当获取到请求数据的时候,将enterLoading修改为false。
//actionCreators.js
//...
// 获取推荐歌单
export const getRecommendList = () => {
return (dispatch) => {
getRecommendListRequest().then(data => {
dispatch(changeRecommendList(data.result));
// 修改enterLoading
dispatch(changeEnterLoading(false));
}).catch(() => {
console.log("推荐歌单数据传输错误");
});
}
};
在 Recommend/index.js
中使用
const { bannerList, recommendList, enterLoading } = props;
// ...
<Content>
// ...
{ enterLoading ? <Loading></Loading> : null }
<Content>
// ...
// 获取store
const mapStateToProps = (state) => ({
...
enterLoading: state.getIn(['recommend', 'enterLoading'])
});
到这里,Loading效果完成。
3. Redux数据缓存 切换到歌手页面,然后切回到推荐页的时候,页面重新发出请求,虽然在页面上没有区别(因为数据没有变化),但是这个请求是没有必要发出的,所以优化一下。很简单,加一个判断,如果如果页面有数据,就不发请求。//Recommend/index.js
useEffect(() => {
// 如果页面有数据,则不发请求,通过immutable数据结构中的长度属性size判断
if(!bannerList.size){
getBannerDataDispatch();
}
if(!recommendList.size){
getRecommendListDataDispatch();
}
// eslint-disable-next-line
}, []);
Recommend
组件就彻底完成了。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)