您现在的位置是:首页 >技术教程 >react18+ts4的项目初始化(带路由配置,ReduxToolkit,axios封装)(附github地址)网站首页技术教程

react18+ts4的项目初始化(带路由配置,ReduxToolkit,axios封装)(附github地址)

阿泽不会飞 2024-06-17 11:25:00
简介react18+ts4的项目初始化(带路由配置,ReduxToolkit,axios封装)(附github地址)

1.项目的初始化配置

(1)创建react+ts项目

create-react-app react_ts_music --template typescript

生成目录:
image.png
修改运行package.json中的配置
image.png
改成:
image.png

(2)项目基本配置

1.craco配置webpack

npm install @craco/craco@alpha -D

创建craco.config.json文件:

const path = require('path')


const resolve = (dir) => path.resolve(__dirname, dir)


module.exports = {
  webpack: {
    alias: {
      '@': resolve('src'),
    }
  }
}

2.配置@指定的src文件下的内容

在tsconfig.json文件中加:
image.png

3.代码规范配置

创建.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
配置prettier工具

可让代码更加好看,风格一致
image.png

npm install prettier -D

创建_prettierrc文件

{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": false,
  "trailingComma": "none",
  "semi": false
}

改运行文件
image.png
image.png

# /build/*
# .local
# .output.js
# /node_modules/**


# **/*.svg
# **/*.sh


# /public/*
配置ESlint工具

npm install eslint -D

初始化:

npx eslint --init

image.png

module.exports = {
    env: {
      browser: true,
      node: true,
      es2021: true
    },
    extends: [
      'eslint:recommended',
      'plugin:react/recommended',
      'plugin:@typescript-eslint/recommended',
    ],
    overrides: [],
    parser: '@typescript-eslint/parser',
    parserOptions: {
      ecmaVersion: 'latest',
      sourceType: 'module'
    },
    plugins: ['react', '@typescript-eslint'],
    rules: {
      '@typescript-eslint/no-var-requires': 'off',
    }
  }
  

2.项目内容配置

(1)目录的划分

image.png

(2)配置less

npm install 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"),
    },
  },
};

(3)路由的配置

npm install react-router-dom

router文件夹建立index.tsx

import React,{lazy} from "react";
import {Navigate} from 'react-router-dom'
import {RouteObject} from 'react-router-dom'
const Discover = lazy(() => import('@/views/discover'))


const routes: RouteObject[]=[
  {
    path: '/',
    element: <Navigate to="/discover" />
  },
  {
    path: '/discover',
    element: <Discover />,
    children: [

    ]
  },
]


export default routes
import React,{Suspense} from "react";
import {useRoutes,Link} from 'react-router-dom'
import routes from './router'
function App() {
  return <div className="App">
    <div className="topHeader">
      <Link to="/discover">discover</Link>
      <Link to="/users">users</Link>
      <Link to="/task">task</Link>
    </div>
    <Suspense fallback="Loading...">
      <div className="main">
        {useRoutes(routes)}
      </div>
    </Suspense>
  </div>;
}

export default App;

import React from "react";
import ReactDOM from "react-dom/client";
import {BrowserRouter} from 'react-router-dom'
import App from "@/App";
import "@/assets/css/base.less"
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(<BrowserRouter><App /></BrowserRouter>);

(4)redux的配置

安装react-redux:react-redux和@reduxjs/toolkit。

npm install react-redux @reduxjs/toolkit -S

安装完相关包以后开始编写基本的 RTK 程序

  • 创建一个store文件夹
  • 创建一个index.ts做为主入口
  • 创建一个festures文件夹用来装所有的store
  • 创建一个counterSlice.js文件,并导出简单的加减方法

创建 Redux State Slice

创建 slice 需要一个字符串名称来标识切片、一个初始 state 以及一个或多个定义了该如何更新 state 的 reducer 函数。slice 创建后 ,我们可以导出 slice 中生成的 Redux action creators 和 reducer 函数。
image.png
store/features/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

// 创建一个Slice
export const counterSlice = createSlice({
  name: 'counter',
  initialState:{
    value: 0,
  },
  reducers: {
    // 定义一个加的方法
    increment:(state,{payload})  => { 
      state.value += parseInt(payload)
    },
    // 定义一个减的方法
    decrement: state => {
      state.value -= 1
    },
  },
})

// 导出加减方法
export const { increment, decrement } = counterSlice.actions

// 暴露reducer
export default counterSlice.reducer

createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。
createSlice(configuration object)
配置对象中的属性:

  • name —— reducer的名字,会作为action中type属性的前缀,不要重复
  • initialState —— state的初始值
  • reducers —— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。

总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。
我们可以看看counterSlice和counterSlice.actions是什么样子
image.png

将 Slice Reducers 添加到 Store 中

下一步,我们需要从计数切片中引入 reducer 函数,并将它添加到我们的 store 中。通过在 reducer 参数中定义一个字段,我们告诉 store 使用这个 slice reducer 函数来处理对该状态的所有更新。
我们以前直接用redux是这样的

const reducer = combineReducers({
  counter:counterReducers
});

const store = createStore(reducer);

store/index.js
切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:

import { configureStore } from '@reduxjs/toolkit'
import { useSelector,useDispatch } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import counterSlice from './features/counterSlice'
 
// configureStore创建一个redux数据
const store = configureStore({
  // 合并多个Slice
  reducer: {
    counter: counterSlice,
  },
})

//app的hook,ts类型定义
export type IRootState=ReturnType<typeof store.getState>
type AppDispatch = typeof store.dispatch
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch
export default store
  • configureStore需要一个对象作为参数,在这个对象中可以通过不同的属性来对store进行设置,比如:reducer属性用来设置store中关联到的reducer,preloadedState用来指定state的初始值等,还有一些值我们会放到后边讲解。
  • reducer属性可以直接传递一个reducer,也可以传递一个对象作为值。如果只传递一个reducer,则意味着store中只有一个reducer。若传递一个对象作为参数,对象的每个属性都可以执行一个reducer,在方法内部它会自动对这些reducer进行合并。

store加到全局

main.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

// redux toolkit
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>,
)

在 React 组件中使用 Redux 状态和操作

现在我们可以使用 React-Redux 钩子让 React 组件与 Redux store 交互。我们可以使用 useSelector 从 store 中读取数据,使用 useDispatch dispatch actions。
App.jsx

import React, { memo ,useRef,createRef} from 'react'
import { increment, decrement } from '@/store/features/counterSlice'
import type { FC, ReactNode } from 'react'
// import { IRootState } from '@/store'
import { useAppSelector ,useAppDispatch} from '@/store'
interface IProps {
  children?: ReactNode
}

const Artist: FC<IProps> = () => {
  const count = useAppSelector((state) => state.counter.value)
  const dispatch = useAppDispatch()
  const InputRef= useRef<HTMLInputElement>(null);
  const InputRef2= createRef();
  return (
    <div>
      <h1>count</h1>
      <div style={{ width: 100, margin: '10px' }}>
        <input ref = { InputRef } type="text"/>
        <button onClick={() => dispatch(increment(InputRef.current?.value))}>+</button>
        <button onClick={() => dispatch(decrement())}>-</button>

        <span>{count}</span>
      </div>
    </div> 
  )
}

export default memo(Artist)

image.png
现在,每当你点击”递增“和“递减”按钮。

  • 会 dispatch 对应的 Redux action 到 store
  • 在计数器切片对应的 reducer 中将看到 action 并更新其状态
  • 组件将从 store 中看到新的状态,并使用新数据重新渲染组件

(5)axios配置

在service文件下:创建requset文件夹:

service/requset:

配置接口的路径:

const BASE_URL = 'http://codercba.com:9002/'
const TIME_OUT = 10000

console.log(process.env.REACT_APP_NAME)
console.log(process.env.REACT_APP_AGE)

export { BASE_URL, TIME_OUT }

拦截器类型:

import type { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface HYRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: HYRequestInterceptors<T>
  showLoading?: boolean
}

请求类的封装:

import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { HYRequestInterceptors, HYRequestConfig } from './type'


const DEAFULT_LOADING = true


class HYRequest {
  instance: AxiosInstance
  interceptors?: HYRequestInterceptors
  showLoading: boolean


  constructor(config: HYRequestConfig) {
    // 创建axios实例
    this.instance = axios.create(config)


    // 保存基本信息
    this.showLoading = config.showLoading ?? DEAFULT_LOADING
    this.interceptors = config.interceptors


    // 使用拦截器
    // 1.从config中取出的拦截器是对应的实例的拦截器
    this.instance.interceptors.request.use(
      // this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )


    // 2.添加所有的实例都有的拦截器
    this.instance.interceptors.request.use(
      (config) => {
        return config
      },
      (err) => {
        return err
      }
    )


    this.instance.interceptors.response.use(
      (res) => {
        const data = res.data
        if (data.returnCode === '-1001') {
          console.log('请求失败~, 错误信息')
        } else {
          return data
        }
      },
      (err) => {
        // 例子: 判断不同的HttpErrorCode显示不同的错误信息
        if (err.response.status === 404) {
          console.log('404的错误~')
        }
        return err
      }
    )
  }


  request<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      // 1.单个请求对请求config的处理
      if (config.interceptors?.requestInterceptor) {
        config = config.interceptors.requestInterceptor(config)
      }


      // 2.判断是否需要显示loading
      if (config.showLoading === false) {
        this.showLoading = config.showLoading
      }


      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 1.单个请求对数据的处理
          if (config.interceptors?.responseInterceptor) {
            res = config.interceptors.responseInterceptor(res)
          }
          // 2.将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING


          // 3.将结果resolve返回出去
          resolve(res)
        })
        .catch((err) => {
          // 将showLoading设置true, 这样不会影响下一个请求
          this.showLoading = DEAFULT_LOADING
          reject(err)
          return err
        })
    })
  }


  get<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'GET' })
  }


  post<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'POST' })
  }


  delete<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'DELETE' })
  }


  patch<T = any>(config: HYRequestConfig<T>): Promise<T> {
    return this.request<T>({ ...config, method: 'PATCH' })
  }
}


export default HYRequest

service/modules对要用的接口进行封装:

import hyRequest from '..'


export function getTopBanner() {
  return hyRequest.get({
    url: '/banner'
  })
}


export function getHotRecommend() {
  return hyRequest.get({
    url: '/personalized'
  })
}


export function getNewAlbum(offset = 0, limit = 10) {
  return hyRequest.get({
    url: '/album/new',
    params: {
      offset,
      limit
    }
  })
}


export function getPlayListDetail(id: number) {
  return hyRequest.get({
    url: '/playlist/detail',
    params: {
      id
    }
  })
}

service/index

请求函数的封装

// service统一出口
import HYRequest from './request'
import { BASE_URL, TIME_OUT } from './request/config'


const hyRequest = new HYRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {
    requestInterceptor: (config) => {
      return config
    },
    requestInterceptorCatch: (err) => {
      return err
    },
    responseInterceptor: (res) => {
      return res
    },
    responseInterceptorCatch: (err) => {
      return err
    }
  }
})


export default hyRequest

路由的请求:

import hyRequest from '@/service';
import React, { memo,useEffect,useState} from 'react'
import type { FC, ReactNode } from 'react'
import {getTopBanner} from '@/service/modules/recommend'
interface IProps {
  children?: ReactNode
}
interface listRoot {
  imageUrl: string
  targetId: number
  adid: any
  targetType: number
  titleColor: string
  typeTitle: string
  url: any
  exclusive: boolean
  monitorImpress: any
  monitorClick: any
  monitorType: any
  monitorImpressList: any
  monitorClickList: any
  monitorBlackList: any
  extMonitor: any
  extMonitorInfo: any
  adSource: any
  adLocation: any
  adDispatchJson: any
  encodeId: string
  program: any
  event: any
  video: any
  song: any
  scm: string
  bannerBizType: string
}

const AxiosTemplate: FC<IProps> = () => {
  const [list , setList]=useState<listRoot[]>([])
  useEffect(() =>{
    (async function() {
      const res= await getTopBanner()
      setList(res.banners)
    })()
  },[])
  return <div>
    <h1>我是axios请求的数据:</h1>
    <ul>
      {
        list.map((item)=>{
          return (
            <li key={item.encodeId}>{item.imageUrl}</li>
          )
        })
      }
    </ul>
  </div>
}

export default memo(AxiosTemplate)

3.github代码地址:

https://github.com/wzz778/react_ts_template

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。