1.创建项目
create-react-app ming_music --template typescript
2.用craco
当react-scripts的版本是5.X的时候,就要用
npm install @craco/craco@alpha -D
要想使用@,第一要配置craco.config.js,还有tsconfig.json,然后package.json的npm run start的react-scripts也要改成craco
const path = require('path')
const CracoLessPlugin = require('craco-less')
const resolve = (dir) => path.resolve(__dirname, dir)
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
// modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true
}
}
}
}
],
webpack: {
alias: {
'@': resolve('src'),
components: resolve('src/components')
}
}
}
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
3.项目搭建规范
3.1. 增加editorconfig
# http://editorconfig.org
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行尾的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false
VSCode需要安装一个插件:EditorConfig for VS Code
3.2. 使用prettier
3.2.1 安装prettier
温馨提示:就算你没有安装这个包,在vscode如果你有安装这个插件,它也会读取配置文件来格式化,装这个插件的好处是在其它软件里面也可以格式化,而且可以增添一个下面的代码来全局格式化代码。
"prettier": "prettier --write ."
npm install prettier -D
3.2.2 使用prettier工具
2.配置.prettierrc文件:
- useTabs:使用tab缩进还是空格缩进,选择false;
- tabWidth:tab是空格的情况下,是几个空格,选择2个;
- printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
- singleQuote:使用单引号还是双引号,选择true,使用单引号;
- trailingComma:在多行输入的尾逗号是否添加,设置为
none,比如对象类型的最后一个属性后面是否加一个,; - semi:语句末尾是否要加分号,默认值true,选择false表示不加;
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 80,
"singleQuote": true,
"trailingComma": "none",
"semi": false
}
3.2.3 创建.prettierignore忽略文件
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
3.2.4 VSCode需要安装prettier的插件
3.2.5 VSCod中的配置
- settings =>format on save => 勾选上
- settings => editor default format => 选择 prettier
3.2.6 测试prettier是否生效
- 测试一:在代码中保存代码;
- 测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
"prettier": "prettier --write ."
3.3. 使用eslint
3.3.1 安装eslint
npm i eslint -D
3.3.2 安装ESLint插件
3.3.3 配置eslint
npx是去找node_modules里面的bin,来执行脚本
npx eslint --init
- 第一个是检查语法错误
- 第二个是检查语法错误并且把问题显示出来,比如红色波浪线(一般第二个)
- 第三个是检查语法错误并且把问题显示出来,而且修复
一般选第一个
然后会生成一个.eslintrc.js文件
手写增加一个node:true,这样就可以支持在node环境
3.3.3 解决eslint和prettier冲突的问题
安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
npm install eslint-plugin-prettier eslint-config-prettier -D
下面是.eslintrc.js文件
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
'plugin:prettier/recommended' //兼容prettier
],
3.3.4 VSCode中eslint的配置(可加可不加,旧版vscode就要加,新版可以不用)
下面是vscode的setting.json,增加下面代码
"eslint.lintTask.enable": true,
"eslint.alwaysShowStatus": true,
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
总结一下:就是红色波浪线是eslint+prettier提供的,报错内容是prettier提供的。要是一直没出现,就可以重启一下。仅仅配置eslint的时候,仅仅在npn run start检测出来,如果想让vscode检测出来,并且加上prettier配置文件里面的限制,报红色的错误,就要做上面的步骤,安装eslint插件等等。后续忘记的话可以再回头看看coderwhy的react+typescript项目。
碰到的问题:一直都是出现莫名格式化,增加下面那个vue { "editor.defaultFormatter": "esbenp.prettier-vscode", "[vue]": { "editor.defaultFormatter": "esbenp.prettier-vscode" } }
4. 目录结构
4.1 初始化css样式
reset.less
body,
html,
h1,
h2,
h3,
h4,
h5,
h6,
ul,
ol,
li,
dl,
dt,
dd,
header,
menu,
section,
p,
input,
td,
th,
ins {
padding: 0;
margin: 0;
}
a {
color: #333;
text-decoration: none;
display: block;
}
img {
vertical-align: top;
}
input, textarea, button, select, a {
outline: none;
border: none;
}
li, ul {
list-style: none;
}
4.2 less并没有生效问题解决
因为我们用craco了,所以要安装craco-less
npm install carco-less@2.1.0-aplha.0
5. 用any的时候会报黄解决方法
rules: {
'@typescript-eslint/no-explicit-any': 'off'
}
6. route配置
import type { RouteObject } from 'react-router-dom'
示例:
const Discover = lazy(() => import('@/views/discover'))
const routes: RouteObject[] = [
{
path: '/',
element: <Navigate to="/discover" />
},
]
7. 写组件的时候会报错原因
比如写一个组件导出,可是有波浪线,问题就是没有导入,原因是本质
一个组件导出可能是方法,也可能是类,在route里面要<>包括住,变成实例
import React from 'react'
8. props的typescript两种方式
用第二种的方法的时候好处是,可以有类型推断,所以写代码的时候推荐第二种方法
旧版的children是不用写的,现在要写了
组件导出的时候最好用memo()包裹
举例:export memo(download)
9. 快速生成代码
网站:snippet-generator.app/?descriptio…
生成片段,放到vscode里面代码片段
10. 懒加载
import React, { lazy } from 'react'
const Discover = lazy(() => import('@/views/discover'))
有children的写法:
{
path: '/discover',
element: <Discover />,
children: [
{
path: '/discover',
element: <Navigate to="/discover/recommend" />
},
{
path: '/discover/recommend',
element: <Recommend />
},
{
path: '/discover/ranking',
element: <Ranking />
},
{
path: '/discover/songs',
element: <Songs />
},
{
path: '/discover/djradio',
element: <Djradio />
},
{
path: '/discover/artist',
element: <Artist />
},
{
path: '/discover/album',
element: <Album />
}
]
},
引入不同:
<Suspense fallback="">
<div className="main">{useRoutes(routes)}</div>
</Suspense>
和
<Suspense fallback="">
<Outlet />
</Suspense>
app.tsx文件
function App() {
return (
<div className="App">
<AppHeader />
<Suspense fallback="">
<div className="main">{useRoutes(routes)}</div>
</Suspense>
<AppFooter />
</div>
)
}
11. redux
npm install @reduxjs/toolkit react-redux
简单的例子
import { configureStore } from '@reduxjs/toolkit'
import {
useSelector,
useDispatch,
TypedUseSelectorHook,
shallowEqual
} from 'react-redux'
import counterReducer from './modules/counter'
const store = configureStore({
reducer: {
counter: counterReducer
}
})
//这部分不懂可以以后回来看,这个项目346.347.348
//- **类型封装**:
//
// - 使用 `typeof` 和 `ReturnType` 获取 Redux Store 的状态类型和 Dispatch 类型。
// - 定义了 `IRootState` 和 `DispatchType`,以便后续严格类型检查。
//- **自定义 Hook**:
// - `useAppSelector`:封装了 `useSelector`,支持状态的类型推导。
// - `useAppDispatch`:封装了 `useDispatch`,明确了 Dispatch 类型。
//- **优化工具**:
// - 使用 `shallowEqualApp` 提供浅比较功能,减少组件不必要的重新渲染。
type GetStateFnType = typeof store.getState
type IRootState = ReturnType<GetStateFnType>
type DispatchType = typeof store.dispatch
// useAppSelector的hook
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector
export const useAppDispatch: () => DispatchType = useDispatch
export const shallowEqualApp = shallowEqual
export default store
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
count: 100,
message: 'Hello Redux',
address: '广州市',
height: 1.88
},
reducers: {
changeMessageAction(state, { payload }) {
state.message = payload
}
}
})
export const { changeMessageAction } = counterSlice.actions
export default counterSlice.reducer
import React, { Suspense } from 'react'
import { useRoutes, Link } from 'react-router-dom'
import { useAppSelector, useAppDispatch, shallowEqualApp } from './store'
import routes from './router'
import { changeMessageAction } from './store/modules/counter'
// import { IRootState } from './store'
// import store from './store'
// type GetStateFnType = typeof store.getState
// type IRootState = ReturnType<GetStateFnType>
function App() {
// const { count, message } = useSelector(
// (state: IRootState) => ({
// count: state.counter.count,
// message: state.counter.message
// }),
// shallowEqual
// )
const { count, message } = useAppSelector(
(state) => ({
count: state.counter.count,
message: state.counter.message
}),
shallowEqualApp
)
/** 事件处理函数 */
const dispatch = useAppDispatch()
function handleChangeMessage() {
dispatch(changeMessageAction('呵呵呵呵呵'))
}
return (
<div className="App">
<div className="nav">
<Link to="/discover">发现音乐</Link>
<Link to="/mine">我的音乐</Link>
<Link to="/focus">关注</Link>
<Link to="/download">下载客户端</Link>
</div>
<h2>当前计数: {count}</h2>
<h2>当前消息: {message}</h2>
<button onClick={handleChangeMessage}>修改message</button>
<Suspense fallback="">
<div className="main">{useRoutes(routes)}</div>
</Suspense>
</div>
)
}
export default App
root.render(
<Provider store={store}>
<HashRouter>
<App />
</HashRouter>
</Provider>
)
12.网络请求axios
import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { HYRequestConfig } from './type'
// 拦截器: 蒙版Loading/token/修改配置
/**
* 两个难点:
* 1.拦截器进行精细控制
* > 全局拦截器
* > 实例拦截器
* > 单次请求拦截器
*
* 2.响应结果的类型处理(泛型)
*/
class HYRequest {
instance: AxiosInstance
// request实例 => axios的实例
constructor(config: HYRequestConfig) {
this.instance = axios.create(config)
// 每个instance实例都添加拦截器
this.instance.interceptors.request.use(
(config) => {
// loading/token
return config
},
(err) => {
return err
}
)
this.instance.interceptors.response.use(
(res) => {
return res.data
},
(err) => {
return err
}
)
// 针对特定的hyRequest实例添加拦截器
this.instance.interceptors.request.use(
config.interceptors?.requestSuccessFn,
config.interceptors?.requestFailureFn
)
this.instance.interceptors.response.use(
config.interceptors?.responseSuccessFn,
config.interceptors?.responseFailureFn
)
}
// 封装网络请求的方法
// T => IHomeData
request<T = any>(config: HYRequestConfig<T>) {
// 单次请求的成功拦截处理
if (config.interceptors?.requestSuccessFn) {
config = config.interceptors.requestSuccessFn(config)
}
// 返回Promise
return new Promise<T>((resolve, reject) => {
this.instance
.request<any, T>(config)
.then((res) => {
// 单词响应的成功拦截处理
if (config.interceptors?.responseSuccessFn) {
res = config.interceptors.responseSuccessFn(res)
}
resolve(res)
})
.catch((err) => {
reject(err)
})
})
}
get<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: 'GET' })
}
post<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: 'POST' })
}
delete<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: 'DELETE' })
}
patch<T = any>(config: HYRequestConfig<T>) {
return this.request({ ...config, method: 'PATCH' })
}
}
export default HYRequest
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
// 针对AxiosRequestConfig配置进行扩展
export interface HYInterceptors<T = AxiosResponse> {
requestSuccessFn?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestFailureFn?: (err: any) => any
responseSuccessFn?: (res: T) => T
responseFailureFn?: (err: any) => any
}
export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: HYInterceptors<T>
}
import { BASE_URL, TIME_OUT } from './config'
import HYRequest from './request'
const hyRequest = new HYRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestSuccessFn: (config) => {
return config
}
}
})
export default hyRequest
class HYRequest {
constructor(config: HYRequestConfig) {
this.instance = axios.create(config)
// 1. 第一个请求拦截器(全局)
this.instance.interceptors.request.use(
(config) => {
console.log("1号请求拦截器")
// 添加token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(err) => {
console.log("1号请求错误处理")
return err
}
)
// 2. 第一个响应拦截器(全局)
this.instance.interceptors.response.use(
(res) => {
console.log("1号响应拦截器")
// 统一处理返回数据格式
return res.data
},
(err) => {
console.log("1号响应错误处理")
return err
}
)
// 3. 第二个请求拦截器(实例特有)
this.instance.interceptors.request.use(
config.interceptors?.requestSuccessFn, // 例如:显示loading
config.interceptors?.requestFailureFn
)
// 4. 第二个响应拦截器(实例特有)
this.instance.interceptors.response.use(
config.interceptors?.responseSuccessFn, // 例如:隐藏loading
config.interceptors?.responseFailureFn
)
}
}
总结:
-
多个拦截器形成了一个处理链
-
请求拦截器:后添加的先执行(从外到内)
-
响应拦截器:先添加的先执行(从内到外)
-
这种设计允许我们:
-
在全局层面处理通用逻辑(如token)
-
在实例层面处理特定逻辑(如loading)
-
在请求层面处理个性化逻辑
这就像快递的处理流程:
-
发货时:先经过市级分拣(后添加的拦截器)→ 再到省级分拣(先添加的拦截器)
-
收货时:先经过省级分拣(先添加的拦截器)→ 再到市级分拣(后添加的拦截器)