Visual LabINTERACTIVE LEARNING
框架原理精校教程

React + TypeScript 工程实践

从项目创建、路由和状态管理,到 API 与组件类型,串起一套可维护的工程骨架。

ReactTypeScript工程化
专题预计 28 分钟查看源文 ↗

创建项目

  1. create-react-app 创建项目 默认webpack配置
# 创建项目
create-react-app my-app --template typescript
  1. vite创建项目
 pnpm create vite my-app --template react-ts

项目配置

  1. Tsconfig 使用默认配置

编辑器给予更好的代码提示,需要在默认配置上添加paths的配置

{
  "compilerOptions": {
   // ...
    "paths": {
      "@/*": [
        "src/*"
      ],
      //...
    },
  },
//  ...
}
  1. prettier

代码格式化工具,统一代码格式

  1. eslint

代码风格校验,统一风格

  1. editorconfig

统一不同的编辑器的设置,如缩进等

  1. husky

Git hook,代码提交前的会自动触发的钩子,可以用于prettier和eslint的格式化校验

  1. craco 配置webpack

使用craco配置webpack,注意craco配置的层级问题,有外层和选项plugin和webpack选项下的plugin,配置方式略有差异

路由

  1. 安装react-router-dom
npm install react-router-dom
# 当前版本为v6
  1. routes.tsx 路由的配置文件
//routes.tsx
import React, { lazy } from "react";
import { Navigate } from 'react-router-dom'
import type { RouteObject } from "react-router-dom";

//懒加载
const  Home =lazy(()=>import("@/views/home")) ;

const routes: RouteObject[] = [
  {
    path: "/",
    element: <Navigate to="/home"/>,
  },
  {
    path: "/home",
    element: <Home />,
  },
];

export default routes;
  1. index.tsx 顶层使用hashRouter组件或者Router组件
import React from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom'

import App from './App'
const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(
      <HashRouter>
         <App />
      </HashRouter>
);
  1. app.tsx app组件中,应用配置的路由,使用useRoutes
import React, { Suspense } from 'react'
import type { FC ,ReactNode} from 'react'
import { useRoutes } from 'react-router-dom'

import Footer from "@/views/footer";
import Header from '@/views/header';
import  routes from '@/routers'

interface IProps { 
  children?: ReactNode

}

const App: FC<IProps> = (props) => { 
  return (
    <div>
      <Header></Header>
      <Suspense fallback="">  
      // 使用路由懒加载,则需要使用 Suspense组件
        <div>
            {useRoutes(routes)}
      </div>
      </Suspense>
      <Footer></Footer>
    </div>
  )
}

export default App

redux 配置

  1. 安装 @reducjs/toolkit & react-redux(redux会在安装react-redux的时候一起安装)
npm install react-redux @reduxjs/toolkit 
#@reducjs/toolkit & react-redux
  1. 创建store
import configureStore from '@reduxjs/toolkit';
import homeReducer from './modules/home'
import {
  TypedUseSelectorHook,
  useSelector,
  useDispatch,
  shallowEqual,
} from "react-redux";

const store = sonfigureStore({
  reducer:{
    home:homeReducer ,
   }
})

//为useSelector注入类型,让后面使用时返回的数据带上类型
type RootState = ReturnType<typeof store.getState>;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppDispatch = useDispatch;
export const appShallowEqual = shallowEqual;

export default store
  1. 创建reducer
//homeReducer.js
import {createSlice,createAsyncThunk} from '@reduxjs/toolkit'

//异步请求
export const homeAsyncAction = createAsyncThunk('homeAsync',async (params,{dispatch})=>{
//发起异步请求
//获取结果后可以返回结果,也可以直接dispatch某个action
})

const homeReducer = createSlice({
   name:'home',
   initialState:{
      counter:0
   },
   reducers:{
     changeCounter(state,{payload}){
        state.counter = payload
     }
   },
   extraReducers: {},//可选
})

export const {changeCounter }  = homeReducer.actions
export default homeReducer.reducer
  1. 注入全局
index.tsx //注入全局

import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'

import App from './App'
import store from './store'
const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(
//注入到全局
    <Provider store={store}>
         <App />
    </Provider>
);
  1. 组件使用
import { useAppSelector, useAppDispatch, appShallowEqual } from '@/store'
import {changeCounter} from '@/store/modules/home'
import React, { memo } from 'react'
import type { FC ,ReactNode} from 'react'

interface IProps { 
  children?: ReactNode
}
const Home: FC<IProps> = (props) => { 

  const { counter} = useAppSelector(state => ({
    counter:state.home.counter
  }),appShallowEqual)
  const dispatch = useAppDispatch()
  return (
    <div>Home
      <button onClick={e=>dispatch(changeCounter(counter+1) )}>+1</button>
      <div>{ counter}</div>

    </div>
  )
}

export default memo(Home)
  1. 异步处理

API封装

对axios进行二次封装,实现定制化
  • 自定义全局拦截器、实例拦截器、单次请求拦截器,针对不同的需求可以实现不同的效果,也可以快速扩展
import axios from "axios";
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";

// 自己扩展的拦截器
interface generalIntercepters<T = AxiosResponse> {
  requestSuccFn?: (config: AxiosRequestConfig) => AxiosRequestConfig;
  requestErrFn?: (err: any) => any;
  responseSuccFn?: (res: T) => T;
  responseErrFn?: (err: any) => any;
}

//  自己扩展的请求配置
interface GeneralRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: generalIntercepters<T>;
}

class GeneralRequest {
  instance: AxiosInstance;

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

    // 全局拦截器,可以做统一操作
    this.instance.interceptors.request.use(
      (config) => {
        console.log("全局请求拦截器");
        return config;
      },
      (err) => {}
    );

    //全局响应拦截
    this.instance.interceptors.response.use(
      (res) => {
        return res.data;
      },
      (err) => err
    );

    // 实例拦截器
    this.instance.interceptors.request.use(
      config.interceptors?.requestSuccFn,
      config.interceptors?.requestErrFn
    );
    this.instance.interceptors.response.use(
      config.interceptors?.responseSuccFn,
      config.interceptors?.responseErrFn
    );
  }

  //请求
  request<T = any>(config: GeneralRequestConfig<T>) {
    //   单独的请求拦截
    if (config.interceptors?.requestSuccFn) {
      config = config.interceptors.requestSuccFn(config);
    }
    return new Promise<T>((resolve, reject) => {
      this.instance
        .request<any, T>(config)
        .then((res) => {
          // 单独的响应拦截
          if (config.interceptors?.responseSuccFn) {
            res = config.interceptors.responseSuccFn(res);
          }
          console.log("请求响应");
          resolve(res);
        })
        .catch((err) => reject(err));
    });
  }

  get<T = any>(config: GeneralRequestConfig<T>) {
    return this.request<T>({ ...config, method: "GET" });
  }
  post<T = any>(config: GeneralRequestConfig<T>) {
    return this.request<T>({ ...config, method: "POST" });
  }
  put<T = any>(config: GeneralRequestConfig<T>) {
    return this.request<T>({ ...config, method: "PUT" });
  }
  delete<T = any>(config: GeneralRequestConfig<T>) {
    return this.request<T>({ ...config, method: "DELETE" });
  }
}

export default GeneralRequest;

组件使用ts

函数式组件
import React,{memo} from 'react'
import type {FC,ReactNode} from 'react'

//定义传入的props类型
interface IProps {
  children?:ReactNode
  name:string
}
//使用Fc泛型
const Home:FC<IProps> = (props)=>{
   return <div></div>
}

export default memo(Home)
类组件
import React, { PureComponent } from 'react'
import type {ReactNode} from 'react'

//定义
interface IProps {
    children?: ReactNode
}

//定义组件内的状态数据
interface IState{
    name: string
}

export default class songs extends PureComponent<IProps, IState> {
    state: IState = {
        name: 'songs'
    }

  render() {
    return (
      <div>songs</div>
    )
  }
}

ref绑定时的类型

import type {ElementRef} from 'react'
import {useRef} from 'react'

const eleRef = useRef<ElementRef<typeof Swiper >>(null)