接着上一篇来继续写了,这篇博客主要完成下面这部分
一.首先先完成这个轮播图了,那肯定需要请求数据了,所以我们先把网络请求部分先写好
在React里面我们也是通过axios来发送网络请求的,先安装
yarn add axios
安装好之后,我们在之前创建的services文件夹里面创建三个js文件,分别叫request.js,recommend.js,config.js
request.js就放我们封装的网络请求
import axios from 'axios';
import { BASE_URL, TIMEOUT } from "./config";
const instance = axios.create({
baseURL: BASE_URL,
timeout: TIMEOUT
});
instance.interceptors.request.use(config => {
// 1.发送网络请求时, 在界面的中间位置显示Loading的组件
// 2.某一些请求要求用户必须携带token, 如果没有携带, 那么直接跳转到登录页面
// 3.params/data序列化的操作
return config;
}, err => {
});
instance.interceptors.response.use(res => {
return res.data;
}, err => {
if (err && err.response) {
switch (err.response.status) {
case 400:
console.log("请求错误");
break;
case 401:
console.log("未授权访问");
break;
default:
console.log("其他错误信息");
}
}
return err;
});
export default instance;
config.js放我们的请求地址
const devBaseURL = "http://123.207.32.32:9001";
const proBaseURL = "https://production.org";
export const BASE_URL = process.env.NODE_ENV === 'development' ? devBaseURL: proBaseURL;
export const TIMEOUT = 5000;
说明一下,我们开发项目肯定大多数时候有两个环境,一个开发环境一个正式环境,这就方便之后我们更改网络请求地址
recommend.js里面放请求的数据接口
import request from './request';
//轮播图数据
export function getTopBanners() {
return request({
url: "/banner"
})
}
二.我们这个项目是把网络请求的数据统一的放在redux里面进行存储的哈,这样我感觉更能考验我们的redux能力,因为redux毕竟比较难嘛
在我们的store文件夹里面创建一个index.js,一个reducer.js,另外我们是用redux-thunk来作为中间件的,喜欢用saga的也可以用saga
index.js
import { createStore, applyMiddleware, compose } from 'redux';
//applyMiddleware:中间件
import thunk from 'redux-thunk';
import reducer from './reducer';
//打开谷歌调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(
//合并多个中间件
applyMiddleware(thunk)
));
export default store;
reducer.js
// combineReducers:合成reducer,因为可能有多个reducer
// 类似:immutable,深拷贝
import { combineReducers } from 'redux-immutable';
//怕命名冲突,as是重命名
import { reducer as recommendReducer } from '../pages/discover/c-pages/recommend/store';
const cReducer = combineReducers({
recommend: recommendReducer //防止命名冲突
});
export default cReducer;
这个里面需要安装一个redux-immutable,最大的目的我觉得就是提升性能吧
yarn add redux-immutable
上面那个引入recommend/store,是我在我们的recommend文件夹里面又创建了一个小的store,可以形象称为子redux,我们在子redux里面再定义数据,然后将它导入到我们的主redux里面,让我们的项目更加清晰,正规化
别忘了去我们的APP.js中去配置一下
import { Provider } from 'react-redux';
import store from './store';
export default memo(function App() {
return (
<Provider store={store}>
<HashRouter>
<LSHAppHeader/>
{renderRoutes(routes)}
<LSHAppFooter/>
</HashRouter>
</Provider>
)
})
三.开始在recommend文件中里面那个store进行一些配置,在store里面创建四个js文件
constants.js,定义我们的常量
export const CHANGE_TOP_BANNERS = "recommend/CHANGE_TOP_BANNERS"; //轮播图常量
actionCreators.js
import * as actionTypes from './constants';
import {
getTopBanners //从我们的网络请求里面导入
} from '@/services/recommend';
const changeTopBannerAction = (res) => ({
type: actionTypes.CHANGE_TOP_BANNERS,
topBanners: res.banners
});
export const getTopBannerAction = () => {
return dispatch => {
getTopBanners().then(res => {
dispatch(changeTopBannerAction(res));
console.log(res)
})
}
};
reducer.js
import { Map } from 'immutable';
//导入所有常量
import * as actionTypes from './constants';
const defaultState = Map({
topBanners: [],
});
function reducer(state = defaultState, action) {
switch (action.type) {
case actionTypes.CHANGE_TOP_BANNERS:
return state.set("topBanners", action.topBanners);
default:
return state;
}
}
export default reducer;
为什么非要用immutable呢?因为react有一个原则就是数据的不可变性,其他的可以去搜索一下哈哈
index.js,导出我们的reducer
import reducer from './reducer';
export {
reducer
}
至此,我们的网络请求和存储数据全部完成了,接下来可以画页面了
四.轮播图页面
首先在recommend文件夹下面创建一个c-cpns文件夹,c-cpns文件夹里面创建一个top-banner文件夹,然后里面创建一个index.js,一个style.js
index.js
我们先一步一步来,分为三块,注意用到了redux里面的hooks,不熟练的最好去稍微学习一下下哈
第一块,为导入配置
import React, { memo, useEffect, useRef, useCallback, useState } from 'react';
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
//useSelector获取我们的state数据,shallowEqual为一个比较,主要为了优化性能
import { getTopBannerAction } from '../../store/actionCreators';
//从我们的recommend中的store导入
import { Carousel } from 'antd';
import {
BannerWrapper,
BannerLeft,
BannerRight,
BannerControl
} from './style';
第二块,为逻辑代码
export default memo(function HYTopBanner() {
// state
const [currentIndex, setCurrentIndex] = useState(0);
// 组件和redux关联: 获取数据和进行操作
const { topBanners } = useSelector(state => ({
// topBanners: state.get("recommend").get("topBanners")
//上面一种写法的语法糖
topBanners: state.getIn(["recommend", "topBanners"])
}), shallowEqual);
const dispatch = useDispatch();
// 其他hooks
const bannerRef = useRef();
useEffect(() => {
dispatch(getTopBannerAction());
}, [dispatch]);
//背景图的切换
const bannerChange = useCallback((from, to) => {
setCurrentIndex(to);
}, []);
// 其他业务逻辑
const bgImage = topBanners[currentIndex] && (topBanners[currentIndex].imageUrl + "?imageView&blur=40x20")
})
首先获取我们的state,然后通过useDispatch来获取我们store里面的dispatch,之后在useEffect发送网络请求
第三块
return (
<BannerWrapper bgImage={bgImage}>
<div className="banner wrap-v2">
<BannerLeft>
<Carousel effect="fade" autoplay ref={bannerRef} beforeChange={bannerChange}>
{
topBanners.map((item, index) => {
return (
<div className="banner-item" key={item.imageUrl}>
<img className="image" src={item.imageUrl} alt={item.typeTitle} />
</div>
)
})
}
</Carousel>
</BannerLeft>
<BannerRight></BannerRight>
<BannerControl>
<button className="btn left" onClick={e => bannerRef.current.prev()}></button>
<button className="btn right" onClick={e => bannerRef.current.next()}></button>
</BannerControl>
</div>
</BannerWrapper>
)
由于我们用的antd的那个走马灯轮播,useRef是为了我们调用方法的时候,useCallback是因为,我们变换图片的同时,它的背景图也跟着发生了变化,对了,这个地方还用到了给css样式传参,通过props
style.js
import styled from 'styled-components';
export const BannerWrapper = styled.div`
background: url(${props => props.bgImage}) center center/6000px;
.banner {
height: 270px;
background-color: red;
display: flex;
position: relative;
}
`
export const BannerLeft = styled.div`
width: 730px;
.banner-item {
overflow: hidden;
height: 270px;
.image {
width: 100%;
}
}
`
export const BannerRight = styled.a.attrs({
href: "https://music.163.com/#/download",
target: "_blank"
})`
width: 254px;
height: 270px;
background: url(${require("@/assets/img/download.png")});
`
export const BannerControl = styled.div`
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
.btn {
position: absolute;
width: 37px;
height: 63px;
background-image: url(${require("@/assets/img/banner_sprite.png")});
background-color: transparent;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, .1);
}
}
.left {
left: -68px;
background-position: 0 -360px;
}
.right {
right: -68px;
background-position: 0 -508px;
}
`
至此我们的轮播图就完成啦哈哈
下面我们来做这个页面
第一步是干啥??肯定是先在网络请求里面封装接口啊
来到我们services文件夹里面的recommend.js
export function getHotRecommends(limit) {
return request({
url: "/personalized",
params: {
limit
}
})
}
这里有个limit参数,设为几,它就显示几条数据,这个地方很明显要设为8
第二步呢?那肯定是去我们recommend里面的store去存储数据喽,对吧,思路没错应该
先去constants.js里面定义常量
export const CHANGE_HOT_RECOMMEND = "recommend/CHANGE_HOT_RECOMMEND";
然后去actionCreators.js
import * as actionTypes from './constants';
import {
getHotRecommends
} from '@/services/recommend';
const changeHotRecommendAction = (res) => ({
type: actionTypes.CHANGE_HOT_RECOMMEND,
hotRecommends: res.result
})
export const getHotRecommendAction = (limit) => {
return dispatch => {
getHotRecommends(limit).then(res => {
dispatch(changeHotRecommendAction(res));
})
}
}
之后去reducer.js中
import { Map } from 'immutable';
//导入所有常量
import * as actionTypes from './constants';
const defaultState = Map({
hotRecommends: [],
});
function reducer(state = defaultState, action) {
switch (action.type) {
case actionTypes.CHANGE_HOT_RECOMMEND:
return state.set("hotRecommends", action.hotRecommends);
default:
return state;
}
}
export default reducer;
好了,这个页面的数据存储完毕,可以开始撸页面了
在c-cpns文件夹里面创建一个hot-recommend文件夹,然后里面创建一个index.js,一个style.js
还是分三步来好吧
第一步,导入配置
import React, { memo, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { HOT_RECOMMEND_LIMIT } from '@/common/contants';
import HYThemeHeaderRCM from '@/components/theme-header-rcm';
import HYSongsCover from '@/components/songs-cover';
import {
HotRecommendWrapper
} from './style';
import { getHotRecommendAction } from '../../store/actionCreators';
HOT_RECOMMEND_LIMIT这个是我之前定义的那个limit,由于比较喜欢拆分,哈哈
export const HOT_RECOMMEND_LIMIT = 8;
HYThemeHeaderRCM,HYSongsCover这两个我觉得是可以复用的,就给它们放在了components文件夹里面,传数据肯定是在父组件里面获取,然后通过props传递,这个毋庸置疑
components中创建一个theme-header-rcm文件夹,里面创建一个index.js,一个style.js
index.js
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { HeaderWrapper } from './style';
const HYThemeHeaderRCM = memo(function(props) {
const { title, keywords } = props;
return (
<HeaderWrapper className="sprite_02">
<div className="left">
<h3 className="title">{title}</h3>
<div className="keyword">
{
keywords.map((item, index) => {
return (
<div className="item" key={item}>
<a href="todo">{item}</a>
<span className="divider">|</span>
</div>
)
})
}
</div>
</div>
<div className="right">
<a href="todo">更多</a>
<i className="icon sprite_02"></i>
</div>
</HeaderWrapper>
)
})
HYThemeHeaderRCM.propTypes = {
title: PropTypes.string.isRequired,
keywords: PropTypes.array
}
HYThemeHeaderRCM.defaultProps = {
keywords: []
}
export default HYThemeHeaderRCM;
PropTypes进行类型的检测等,我觉得最好还是带上吧,开发规范
style.js
import styled from 'styled-components';
export const HeaderWrapper = styled.div`
height: 33px;
border-bottom: 2px solid #C10D0C;
padding: 0 10px 4px 34px;
background-position: -225px -156px;
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: center;
.title {
font-size: 20px;
font-family: "Microsoft Yahei", Arial, Helvetica, sans-serif;
margin-right: 20px;
}
.keyword {
display: flex;
.item {
.divider {
margin: 0 15px;
color: #ccc;
}
}
}
}
.right {
display: flex;
align-items: center;
.icon {
display: inline-block;
width: 12px;
height: 12px;
margin-left: 4px;
background-position: 0 -240px;
}
}
`
然后是另外一个
components中创建一个songs-cover文件夹,里面创建一个index.js,一个style.js
index.js
import React, { memo } from 'react';
import { getCount, getSizeImage } from "@/utils/format-utils";
import { SongsCoverWrapper } from './style';
export default memo(function HYSongsCover(props) {
const { info } = props;
return (
<SongsCoverWrapper>
<div className="cover-top">
<img src={getSizeImage(info.picUrl, 140)} alt="" />
<div className="cover sprite_covor">
<div className="info sprite_covor">
<span>
<i className="sprite_icon erji"></i>
{getCount(info.playCount)}
</span>
<i className="sprite_icon play"></i>
</div>
</div>
</div>
<div className="cover-bottom text-nowrap">
{info.name}
</div>
<div className="cover-source text-nowrap">
by {info.copywriter || info.creator.nickname}
</div>
</SongsCoverWrapper>
)
})
getCount, getSizeImage ,这两个方法说明一下,先说getCount
就是相当于一个过滤器,当我们数字过长,比如让它除以1万,然后它后面就跟上个万,依次类推
然后getSizeImage
就是你们做项目的时候,有一些图片它非常的大,然后加载的时候特别的慢,网易云就是在图片的后面拼接了一个参数param,参数就传递宽高,这样我们就可以在请求图片的同时,将它的宽高进行锁定,这样当我们项目加载这个图片的时候,就加载我们定义的宽高的图片,可以很好的节省性能和流量,类似这样
"https://p1.music.126.net/fFua795_abR_N2gXAl--LQ==/109951165222245155.jpg?param=140x140"
好吧,去到我们的utils文件里面创建一个format-utils.js
export function 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 + "亿";
}
}
export function getSizeImage(imgUrl, size) {
return `${imgUrl}?param=${size}x${size}`;
}
style.js
import styled from "styled-components";
export const SongsCoverWrapper = styled.div`
width: 140px;
margin: 20px ${props => (props.right || 0)} 20px 0;
.cover-top {
position: relative;
&>img {
width: 140px;
height: 140px;
}
.cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-position: 0 0;
.info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-position: 0 -537px;
color: #ccc;
height: 27px;
.erji {
margin-right: 5px;
display: inline-block;
width: 14px;
height: 11px;
background-position: 0 -24px;
}
.play {
display: inline-block;
width: 16px;
height: 17px;
background-position: 0 0;
}
}
}
}
.cover-bottom {
font-size: 14px;
color: #000;
margin-top: 5px;
}
.cover-source {
color: #666;
}
`
好了,至此两个复用型组件定义完了,回到我们的hot-recommend中
开始第二步,逻辑代码,隔太远了,我把上面的也复制下来吧…
import React, { memo, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { HOT_RECOMMEND_LIMIT } from '@/common/contants';
import HYThemeHeaderRCM from '@/components/theme-header-rcm';
import HYSongsCover from '@/components/songs-cover';
import {
HotRecommendWrapper
} from './style';
import { getHotRecommendAction } from '../../store/actionCreators';
export default memo(function HYHotRecommend() {
// state
// redux hooks
//shallowEqual,为了浅层比较
const { hotRecommends } = useSelector(state => ({
hotRecommends: state.getIn(["recommend", "hotRecommends"])
}), shallowEqual);
const dispatch = useDispatch();
// other hooks
useEffect(() => {
dispatch(getHotRecommendAction(HOT_RECOMMEND_LIMIT));
}, [dispatch]);
该解释的,我前面都解释过了,跳过了哈
return (
<HotRecommendWrapper>
<HYThemeHeaderRCM title="热门推荐" keywords={["华语", "流行", "民谣", "摇滚", "电子"]} />
<div className="recommend-list">
{
hotRecommends.map((item, index) => {
return <HYSongsCover key={item.id} info={item}/>
})
}
</div>
</HotRecommendWrapper>
)
style.js
import styled from "styled-components";
export const HotRecommendWrapper = styled.div`
.recommend-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
`
好了,完事,这两个页面完成,可能大家都觉得全是静态页面,别慌,后面会写播放,歌词等等一些功能,下一篇就写这两个页面了哈