• 该项目是我跟着神三元(抖音架构组)做的一款网易云音乐的 WebApp,原电子书链接
  • 主要技术栈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)图标文件

  • srcassets 目录下,创建一个 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
  • 类似的编写 RecommendSingersRank 组件。
  • 现在启动项目,可以看到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
  • srcApp.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-componentsless 一样,允许我们使用变量。

  • 我们导入了全局样式 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">&#xe65c;</span><span className="title">WebApp</span><span className="iconfont search">&#xe62b;</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>
  • 使用 <NavLink></NavLink> 组件,会变成 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;}}}`;
  • 我们给三个 <NavLink></NavLink> 都写了 activeClassName="selected",这样当我们选中哪一个路由时,哪一个路由就有了 selected 的样式。

启动项目,查看效果:

三、引入Redux

Redux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。 | Redux 中文官网

1. 安装对应的依赖

redux redux-thunk redux-immutable react-redux immutable
  • reudx-thunk 是中间件,类似的还有 saga,这里我们使用的是 thunk

  • immutableFacebook 开发一个 持久化数据结构,它是一经创建变不可修改的数据,普遍运用于 redux 中。

2. 合并各组件的reducer

  • srcstore 目录下,创建 index.jsreducer.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-reduxProvider 方法传递store。

store能被所有被 <Provider store={store}></Provider> 包裹的组件使用。

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://p1.music.126.net/ZYLJ2oZn74yUz5x8NBGkVA==/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 } = propsreturn (<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} = propsreturn (<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} = propsreturn (<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: "https://p1.music.126.net/fhmefjUfMD-8qtj3JKeHbA==/18999560928537533.jpg",playCount: 17171122,name: "[洗澡时听的歌] 浴室里听歌吹泡泡o○o○o○"}
});<List recommendList={recommendList}></List>
  • JSX:
import React from 'react';function RecommendList(props) {const { recommendList } = propsreturn (<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">&#xe885;</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原生被父组件获取到。

2. 配置scroll

滚动的方向

  • vertical, horizental

滚动的位置

  • probeType: 3:不仅在屏幕滑动的过程中,而且在 momentum 滚动动画运行过程中实时派发 scroll 事件。

点击事件

  • click: true,默认是阻止的。

回弹

  • bounce: { top: bounceTop, bottom: bounceBottom},滑动超过顶部和底部边界时回弹效果
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()回弹

useEffect(() => {if(!bScroll || !pullUp) return;bScroll.on('scrollEnd', () => {//判断是否滑动到了底部if(bScroll.y <= bScroll.maxScrollY + 100){pullUp();}});return () => {bScroll.off('scrollEnd');}
}, [pullUp, bScroll]);

5. 下拉刷新

下拉超过顶部边界时,触发pullDown()回弹。

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加绝对定位

  • 我们滑动时,滑动的不是 <Scroll> 这个组件,而是它里面的内容。
  • 前面给了 <Scroll> 组件一个高度和宽度,都是相对于父容器 Content100%
  • 那是因为我们没有给我们的 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, // 带cookieheaders: {"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.jsconst mapStateToProps = (state) => ({// 不要再这里将数据toJS,不然每次diff比对props的时候都是不一样的引用,还是导致不必要的重渲染, 属于滥用immutablebannerList: 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 来获取数据。

(4)action

提供用于修改仓库数据的方法

// 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 placeholder={<img width="100%" height="100%" src={require('./music.png')} alt="music"/>}><img src={item.picUrl + "?param=300x300"} width="100%" height="100%" alt="music"/>
</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 (<LoadingWrapper><div></div><div></div></LoadingWrapper>);
}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));// 修改enterLoadingdispatch(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 组件就彻底完成了。

React云音悦WebApp相关推荐

  1. 【免费活动】解析腾讯云音视频通信三大核心网络技术实战与创新

    随着互联网的发展越来越成熟,移动终端成为我们人手必备的生活用品,云计算的普及与高速发展,4G.5G网络的瓜熟蒂落,我们真正的进入了全真互联网时代.2020年,一场突如其来的疫情,很多传统行业不得不将线 ...

  2. 解析腾讯云音视频通信三大核心网络技术实战与创新

    随着互联网的发展越来越成熟,移动终端成为我们人手必备的生活用品,云计算的普及与高速发展,4G.5G网络的瓜熟蒂落,我们真正的进入了全真互联网时代.2020年,一场突如其来的疫情,很多传统行业不得不将线 ...

  3. 【线上分享】云原生时代,华为云音视频质量监控与优化实践

    云时代,视频直播.实时音视频通信等在线音视频服务面临各种复杂的网络环境和流量爆发式的增长,对音视频质量监控和成本优化提出新的严峻挑战. 12月3日 19:30,我们邀请到了华为云音视频大数据研发负责人 ...

  4. 一站式体验腾讯云音视频及融合通信技术

    对于一款音视频产品,从底层编解码.到传输网络.到平台架构.再到用户终端,无一不决定产品"生死",与此同时,伴随用户数量的提升和对观看体验的不断提高,如何融合AI技术.优化算法.利用 ...

  5. 【新知实验室】手把手实现腾讯云音视频应用

    腾讯云音视频是什么? 腾讯云音视频(TRTC)提供一站式视频解决方案,包括点播直播.实时视频通话.短视频等视频服务,广泛应用于在线视频.电商.游戏直播.在线教育等场景.实时音视频基于腾讯21年来在网络 ...

  6. 【新知实验室】腾讯云音视频应用

    前几天和同事了解了一下腾讯云音视频,并且根据文档亲自使用了一下,感觉还是非常不错的,在这里和大家分享一下. 到底什么是腾讯云音视频 腾讯云音视频是腾讯提供的一站式视频解决方案,其中包括了点播.直播.实 ...

  7. 腾讯云音视频及融合通信技术

    随着直播.游戏.电商.VR等场景的普及,基于音视频的实时娱乐社交.3D虚拟直播.AI视频招聘.元宇宙等新场景也纷纷涌现,下面一起走进音视频的世界. 腾讯云音视频产品,从底层编解码.到传输网络.到平台架 ...

  8. 【友云音】友云音部署常见问题

    注册过程中常见问题 申请租户,超过半小时仍未通过审核怎么办?或者,申请租户需要紧急通过审核,怎么办? 请联系友云音工作人员 Agent下载与配置过程中常见问题 Agent下载后,windows中双击启 ...

  9. 开源项目【LikeCloudMusic 云音】仿网易云音乐

    LikeCloudMusic 云音 仿网易云音乐v3.7.5,Material Design风格,基于MVP,使用RxBus作为事件总线通信库 效果图 目前功能 扫描本地歌曲 存储歌曲及歌单 后台播放 ...

最新文章

  1. 系统学习NLP(三)--NLP入门综述
  2. SAP-ABAP SmartForms之变量显示小技巧
  3. C++ Primer 第10章 习题 10.18
  4. Yolov4 cfg参数解读
  5. 电脑屏幕倒过来该怎么办?
  6. [考试反思]0819NOIP模拟测试26:荒芜
  7. 企业巧妙运用飞秋提高工作效率
  8. 某储云商城系统源码V1.782 绿色版
  9. 代数学笔记4: Galois基本定理
  10. spring-cloud熔断和负载均衡
  11. 第12周Python学习周记
  12. 实战:配置内网DNS实现内部域名解析
  13. 代理(Proxy)和背靠背用户代理(B2BUA)
  14. linux系统date命令(时间戳与日期相互转换)
  15. Linux网络参数DD,linux tcpdump命令参数及用法详解--linux下抓包网络分析
  16. Androidadb驱动实现原理
  17. 算法小讲堂之B树和B+树(浅谈)|考研笔记
  18. 传奇手游漏洞获取gm权限_如何获取传奇私服gm权限
  19. Yarn框架和工作流程简介
  20. 蓝牙(七)L2CAP层协议解析

热门文章

  1. 【BeetSQL入门学习】
  2. 【paper吐槽】【SelfSupervised Learning】Self-Supervised Image Restoration with Blurry and Noisy Pairs
  3. 51单片机波形发生器产生各种波形的原理
  4. USB过压过流保护芯片,可调限流4A,6V过压关闭
  5. localstorage在苹果手机浏览器无效
  6. jupyter能debug了,使用vscode的jupyter插件进行debug
  7. android 手机强制关机代码,安卓手机强制重启方法
  8. 震惊!网瘾少年在冒险岛的逆袭之路
  9. python写的2048游戏,源代码,pygame
  10. 揭开物联网的神秘面纱--物联网小灯