# 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 (
) })}
download
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