react+typescript+eslint+prettier+redux+axios搭建

350 阅读8分钟

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

image.png

image.png

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

image.png

  • 第一个是检查语法错误
  • 第二个是检查语法错误并且把问题显示出来,比如红色波浪线(一般第二个)
  • 第三个是检查语法错误并且把问题显示出来,而且修复

image.png 一般选第一个

image.png

然后会生成一个.eslintrc.js文件

image.png 手写增加一个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. 目录结构

image.png

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

image.png

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'

image.png

8. props的typescript两种方式

image.png

image.png

用第二种的方法的时候好处是,可以有类型推断,所以写代码的时候推荐第二种方法

image.png

旧版的children是不用写的,现在要写了

image.png

组件导出的时候最好用memo()包裹

举例:export memo(download)

image.png

9. 快速生成代码

网站:snippet-generator.app/?descriptio…

image.png

生成片段,放到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)

  • 在请求层面处理个性化逻辑

这就像快递的处理流程:

  • 发货时:先经过市级分拣(后添加的拦截器)→ 再到省级分拣(先添加的拦截器)

  • 收货时:先经过省级分拣(先添加的拦截器)→ 再到市级分拣(后添加的拦截器)