# react18-ts-zm-music
**Repository Path**: wzm_love_coding/react18-ts-zm-music
## Basic Information
- **Project Name**: react18-ts-zm-music
- **Description**: 这是一个React18+TS的音乐项目
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-07-24
- **Last Updated**: 2023-07-31
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 创建项目
`create-react-app project-name --template typescript`
## 项目配置
`npm i @craco/craco@alpha -D `
- 配置别名@需要在craco.config.js和tsconfig.json两个文件中配置
```json
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
```
craco.config.js
```
const path = require("path");
const resolve = (dir) => path.resolve(__dirname, dir);
module.exports = {
webpack: {
alias: {
"@": resolve("src"),
},
},
};
```
修改package.json
```
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
```
## 项目规范
- `npm i prettier -D`
.prettierrc文件中
```
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",
"semi": false
}
```
package.json中
```
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"prettier": "prettier --write ."
},
```
.prettierignore 忽略文件配置
```
/build/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
```
配置好prettier之后,执行命令 `npm run prettier` 就可以格式化所有代码,不需要ctrl + s 保存进行格式化了
- eslint配置
- 安装 `npm i eslint -D `
- `npx eslint --init`
> prettier和eslint保持一致
>
> `npm i eslint-plugin-prettier eslint-config-prettier -D`
.eslintrc.js文件中
```
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:prettier/recommended'
],
```
## 新建项目目录结构
## 样式重置
- `npm i normalize.css`
## less配置
- `npm i craco-less@2.1.0-alpha.0`
craco.config.js中
```
const path = require('path')
const CracoLessPlugin = require('craco-less')
const resolve = (dir) => path.resolve(__dirname, dir)
module.exports = {
plugins: [{ plugin: CracoLessPlugin }],
webpack: {
alias: {
'@': resolve('src')
}
}
}
```
## 配置路由
- `npm i react-router-dom`
配置单独路由文件:
```
import { RouteObject } from 'react-router-dom'
import Discover from '@/views/discover'
const routes: RouteObject[] = [
{
path: '/discover',
element:
}
]
export default routes
```
使用HashRouter
```
```
useRoutes将单独配置的routes使用
```
import { useRoutes } from 'react-router-dom'
import routes from './router'
function App() {
return
{useRoutes(routes)}
}
export default App
```
## props类型约束
- 直接对props进行类型约束
```
interface IProps {
name: string
age: number
height?: number
}
const Download = (props: IProps) => {
return Download- props.name{props.name}
}
export default Download
```
```
import Download from '../download'
const Discover = () => {
return (
<>
Discover
>
)
}
export default Discover
```
- React.FC, children,ReactNode
```
import Download from '../download'
const Discover = () => {
return (
<>
Discover
I am Children
me too
>
)
}
export default Discover
```
```
import React,{memo} from 'react'
import type { FC,ReactNode } from 'react'
interface IProps {
children?: ReactNode
name: string
age: number
height?: number
}
const Download: FC = (props) => {
return (
name:{props.name}
age:{props.age}
{props.children}
)
}
export default memo(Download)
```
## 配置用户代码片段
设置 -> 用户代码片段 - > typescriptreact.json文件中
```
"react typescript": {
"prefix": "tsreact",
"body": [
"import React, { memo } from 'react'",
"import type { FC, ReactNode } from 'react'",
"",
"interface IProps {",
" children?: ReactNode",
"}",
"",
"const ${1:Home}: FC = () => {",
" return ${1:Home}
",
"}",
"",
"export default memo(${1:Home})",
""
],
"description": "react typescript"
}
```
## 使用模板生成页面代码结构,并配置其它路由
```
import { Navigate } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'
import Discover from '@/views/discover'
import Mine from '@/views/mine'
import Focus from '@/views/focus'
import Download from '@/views/download'
const routes: RouteObject[] = [
{
path: '/',
element:
},
{
path: '/discover',
element:
},
{
path: '/mine',
element:
},
{
path: '/focus',
element:
},
{
path: '/download',
element:
}
]
export default routes
```
## 路由懒加载
```
import React, { lazy } from 'react'
// import Discover from '@/views/discover'
// import Mine from '@/views/mine'
// import Focus from '@/views/focus'
// import Download from '@/views/download'
const Discover = lazy(() => import('@/views/discover'))
const Mine = lazy(() => import('@/views/mine'))
const Focus = lazy(() => import('@/views/focus'))
const Download = lazy(() => import('@/views/download'))
```
懒加载的组件可能没加载完成,需要设置Suspense
```
import React, { Suspense } from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './router'
function App() {
return (
发现音乐
我的音乐
关注
下载客户端
{/* */}
{useRoutes(routes)}
)
}
export default App
```
## discover子页面路由配置
```
{
path: '/discover',
element: ,
children: [
{
path: '/discover',
// 重定向
element:
},
{
path: '/discover/recommend',
element:
},
{
path: '/discover/ranking',
element:
},
{
path: '/discover/songs',
element:
},
{
path: '/discover/djradio',
element:
},
{
path: '/discover/artist',
element:
},
{
path: '/discover/album',
element:
}
]
},
```
```
import React, { memo, Suspense } from 'react'
import type { FC, ReactNode } from 'react'
import { Outlet, Link } from 'react-router-dom'
interface IProps {
children?: ReactNode
}
const Discover: FC = () => {
return (
推荐
排行榜
歌单
主播电台
歌手
新碟上架
{/* 设置了就没有闪动 */}
)
}
export default memo(Discover)
```
## redux集成
- `npm i @reduxjs/toolkit react-redux`
## state类型配置
- 官网 `https://react-redux.js.org/using-react-redux/usage-with-typescript`
- 创建store,定义state类型,useSelector, useDispatch的ts类型
```js
import { configureStore } from '@reduxjs/toolkit'
import { TypedUseSelectorHook, useSelector, useDispatch, shallowEqual } from 'react-redux'
import countReducer from './modules/counter'
const store = configureStore({
reducer: {
counter: countReducer
}
})
type RootState = ReturnType
export const useAppSelector: TypedUseSelectorHook = useSelector
type AppDispatch = typeof store.dispatch
export const useAppDispatch: () => AppDispatch = useDispatch
export const shallowEqualApp = shallowEqual
export default store
```
- 在index.tsx中给App提供store
```js
import ReactDOM from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import 'normalize.css'
import { Provider } from 'react-redux'
import './assets/css/index.less'
import App from '@/App'
import store from './store'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
)
```
- 创建对应模块
```js
import { createSlice } from '@reduxjs/toolkit'
const countSlice = createSlice({
name: 'counter',
initialState: {
count: 999,
msg: 'zm'
},
reducers: {
changeMsgAction(state, { payload }) {
state.msg = payload
}
}
})
export const { changeMsgAction } = countSlice.actions
export default countSlice.reducer
```
- 在组件中使用state,就有了很好的类型提示
```js
import React, { Suspense } from 'react'
import { useRoutes, Link } from 'react-router-dom'
import routes from './router'
import { changeMsgAction } from './store/modules/counter'
import { useAppSelector, useAppDispatch, shallowEqualApp } from './store'
function App() {
const { count, msg } = useAppSelector(
(state) => ({
count: state.counter.count,
msg: state.counter.msg
}),
shallowEqualApp
)
const dispatch = useAppDispatch()
function handleChangeMsg() {
dispatch(changeMsgAction('1899'))
}
return (
count:{count}-msg:{msg}
发现音乐
我的音乐
关注
下载客户端
{/* */}
{useRoutes(routes)}
)
}
export default App
```
## 封装网络请求-axios,useState()定义数据类型
- 安装依赖 `npm i axios`
- 封装网络请求
- useState()定义数据类型
- 工具 json to typescript,地址 `https://transform.tools/json-to-typescript`
- 工具2:`https://tooltt.com/json2typescript/`
- 测试网络请求接口
```js
import zmRequest from '@/service'
import React, { memo, useEffect, useState } from 'react'
import type { FC, ReactNode } from 'react'
interface IProps {
children?: ReactNode
}
interface IBannerData {
imageUrl: string
targetId: number
targetType: number
titleColor: string
typeTitle: string
url?: any
exclusive: boolean
encodeId: string
scm: string
bannerBizType: string
}
const Recommend: FC = () => {
const [banners, setBanners] = useState([])
// 测试网络请求
useEffect(() => {
zmRequest
.get({
url: '/banner'
})
.then((res) => {
setBanners(res.banners)
})
}, [])
return (
Recommend Page
{banners.map((item, index) => {
return
{item.imageUrl}
})}
)
}
export default memo(Recommend)
```
## 生产环境和开发环境区分
```js
// 1.手动切换
export const BASE_URL = 'http://codercba.com:9002'
// export const BASE_URL = 'http://codercba.prod:9002'
export const TIME_OUT = 10000
// 2.依赖当前环境:development/production
// let BASE_URL = ''
// if (process.env.NODE_ENV === 'development') {
// BASE_URL = 'http://codercba.dev:9002'
// } else {
// BASE_URL = 'http://codercba.prod:9002'
// }
// export { BASE_URL }
// 3.从定义的环境变量的配置文件中,加载变量
// console.log(process.env.REACT_APP_BASE_URL)
```
- .env.development配置文件
`REACT_APP_BASE_URL=http://codercba.dev.9002`
- .env.production配置文件
`REACT_APP_BASE_URL=http://codercba.prod:9002`
## ts类型知识补充
- 类组件与ts
```js
import { PureComponent, ReactNode } from 'react'
interface IProps {
msg: string
count?: number
}
interface IState {
name: string
age: number
}
export class Demo02 extends PureComponent {
state = {
name: 'zm',
age: 21
}
render(): ReactNode {
return (
{this.state.name}-{this.state.age}
props:{this.props.msg}
)
}
}
```
## CSS的编写方案-styled-components
- 安装依赖 `npm i styled-components -D`
- 同时安装类型声明 `npm i @types/styled-components -D`
## styled-components混入,theme
```js
const theme = {
color: {
primary: '#c20c0c',
secondary: ''
},
size: {},
mixin: {
wrapv1: `
width:1100px;
margin:0 auto;
`
}
}
export default theme
```
```js
import { ThemeProvider } from 'styled-components'
root.render(
)
```
- 组件样式中使用
```js
import styled from 'styled-components'
export const HeaderWrapper = styled.div`
.content {
height: 100px;
${(props) => props.theme.mixin.wrapv1}
}
`
```
方式2:添加类(css -> common.less)
```
.wrap-v1 {
width: 1100px;
margin: 0 auto;
}
```
组件中使用:
```
发现音乐
我的音乐
关注
下载客户端
```
## app-header样式编写和antd集成
- `npm i antd`
- 最新的antd("antd":"^5.7.3"),安装好后,直接在组件中使用即可
- icon图标需单独安装 `npm i --save @ant-design/icons`
## 发现音乐页面nav-bar样式,代码结构调整
## useRef()类型写法
```js
import React, { memo, useRef } from 'react'
import type { ElementRef, FC, ReactNode } from 'react'
import { Carousel } from 'antd'
import { LeftOutlined, RightOutlined } from '@ant-design/icons'
import { BannerControl, BannerLeft, BannerRight, BannerWrapper } from './style'
import { useAppSelector } from '@/store'
import { shallowEqual } from 'react-redux'
import download from '@/assets/img/download.png'
interface IProps {
children?: ReactNode
}
const TopBanner: FC = () => {
const bannerRef = useRef>(null)
const { banners } = useAppSelector(
(state) => ({
banners: state.recommend.banners
}),
shallowEqual
)
function handlePrevClick() {
bannerRef.current?.prev()
}
function handleNextClick() {
bannerRef.current?.next()
}
return (
{banners.map((item) => {
return (
)
})}
handlePrevClick()}>
handleNextClick()}>
)
}
export default memo(TopBanner)
```
## 轮播图组件
## 动态样式
- `npm i classnames`
```
{banners.map((item, index) => {
return (
)
})}
```
## recom-header热门推荐头部组件封装
## song-menu-item组件封装
## 新碟上架,及new-album-item封装
```js
{/* 双重遍历, albums.slice(item * 5, (item + 1) * 5) 每页展示5条 */}
{[0, 1].map((item) => {
return (
{albums.slice(item * 5, (item + 1) * 5).map((album) => {
return
{album.name}
})}
)
})}
```
## 榜单
- 引入图片方式
```css
import styled from 'styled-components'
export const RankingWrapper = styled.div`
margin-top: 43px;
.content {
margin-top: 22px;
height: 472px;
background: url(${require('@/assets/img/recommend-top-bg.png')});
}
`
```
## Promise.all保证顺序
```js
const rankingIds = [19723756, 3779629, 2884035]
export const fetchPlaylistDetailAction = createAsyncThunk('rankdata', (arg, { dispatch }) => {
// 方式一
// rankingIds.forEach((id) => {
// getPlaylistDetail(id).then((res) => {
// switch (id) {
// case 19723756:
// console.log('飙升榜', res.playlist)
// dispatch(changeUpRankingAction(res.playlist))
// break
// case 3779629:
// console.log('新歌榜', res.playlist)
// dispatch(changeNewRankingAction(res.playlist))
// break
// case 2884035:
// console.log('原创榜', res.playlist)
// dispatch(changeOriginRankingAction(res.playlist))
// break
// }
// })
// })
// 方式二
/**
* 将三个结果都拿到,统一放到一个数组中管理
* 1.获取到所有的结果后,进行dispatch操作
* 2.获取到的结果一定是有正确顺序的
*/
const promise: Promise[] = []
rankingIds.forEach((id) => {
promise.push(getPlaylistDetail(id))
})
Promise.all(promise).then((res) => {
// res => [{},{},{}]
// map过滤数组
const playlists = res.map((item) => item.playlist)
dispatch(changeAllRankingDataActions(playlists))
})
})
```
## 首页样式调整
## 音乐播放
- views -> player