快速搭建react18.2+TS框架(包含redux,sass,@根目录等有用配置)

944 阅读13分钟

创建react+ts的项目:

全局安装 react 脚手架(已经安装的忽略):npm i create-react-app -g

npx create-react-app 项目名 --template typescript

创建成功之后的一级目录

1.jpg

下面简单说一下每个文件夹里面的文件和修改了哪些内容

1:node_modules,不用多说,npm的依赖包。

2:public,不用多说,创建项目之后里面只留下 一个 favicon.ico 和一个入口 index.html 即可。

3:src,业务目录,基本上我们所有的业务代码,图片,css等都会在里面。

-------3.1:src下面的所有目录

2.jpg

--------------3.1.1:src/asstes,我们存放图片或者css以及其他静态资源

3.jpg

--------------3.1.2:src/asstes/styles/base.css,项目的初始化样式,这里我简单写了一点,可自行修改

*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html {
  height: 100%;
  font-size: 14px;
  user-select: none;
}
body{
  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
  height: 100%;
  color: #333;
}
a {
  text-decoration: none;
  color: #333;
  outline: none;
}
i {
  font-style: normal;
}
img {
  max-width: 100%;
  max-height: 100%;
  vertical-align: middle;
}
ul {
  list-style: none;
}

-------3.2:src/components,我们在项目中用到了公共组件,这里我只简单封装了一个鉴权路由组件

1.jpg

//自己封装的 history 路由跳转,在tsx和ts中都能使用。使用 react-router-dom 里面的 useHistory Hooks 只能在tsx函数组件中使用,不能在ts文件中使用,不太方便,所以这里自己封装。
import history from "@/utils/history";

// 自己封装的获取token的函数
import { hasToken } from "@//utils/storage";

import React from "react";

import { Redirect, Route } from "react-router-dom";

type AtahType = {
  path: string; //传入的路由路径,必传
  component: React.FC; //引入react中以及封装好的 组件 的类型声明,必传
};

function AuthRoute({ path, component: Com }: AtahType) {
  return (
    //引入 react-router-dom 中的 Route 组件
    <Route
      // 路由路径
      path={path}
      render={() => {
        // 需要token的页面使用本组件,检查到有token,就正常访问
        if (hasToken()) return <Com />;
        else {
          // 没有token,提示一下。这里随便写的alret,可自行修改其他方式提示(比如antd的轻提示)
          alert("登录失效");
          return (
            // 路由重定向到 登录页面
            <Redirect
              to={{
                pathname: "/Login",
                // 附带 回跳 信息有些业务需求 登录之后回到 上一个页面的时候可以使用比如在个人详情页由于某些原因token失效或者本地删除了token那就会回到登录页登录成功之后会回跳到 个人详情 state: { from: history.location.pathname },
              }}
            />
          );
        }
      }}
    />
  );
}

// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoAuthRoute = React.memo(AuthRoute);

// 默认导出
export default MemoAuthRoute;

-------3.3:src/pages,我们所有的组件

1.jpg

--------------3.3.1:src/pages/Home/index.tsx,简单声明一个组件

import React from "react";

function Home() {
  return <div>Home</div>;
}

// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoHome = React.memo(Home);

// 默认导出
export default MemoHome;

--------------3.3.2:src/pages/Layout/index.tsx,简单声明一个组件(一般会把Layout作为二级路由页面,看各位需求)

import React from "react";

function Layout() {
  return (
    <div>
      <h1>Layout页面</h1>
    </div>
  );
}

// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoLayout = React.memo(Layout);

// 默认导出
export default MemoLayout;

--------------3.3.3:src/pages/Home/Login.tsx,简单声明一个组件,包含了index.module.scss,模块化思想的css,下面会详细介绍

index.module.scss

.Login {
  width: 500px;
  height: 500px;
  background-color: red;

  // 使用 global 来声明 除了第一个盒子 会模块化自动随机生成 calss类名,global里面的类名和元素不会变更。这样在控制台调试样式的时候不会混乱
  :global {
    h1 {
      background-color: aquamarine;
      font-size: 40px;
    }
  }
}

index.tsx

import React from "react";

// 自己封装的 axios 初始基地址,会根据开发环境或者打包环境来自动改变
import { baseURL } from "@/utils/http";

// 导入模块化的css
import styles from "./index.module.scss";

function Login() {
  return (
    // 定义模块化css类名
    <div className={styles.Login}>
      <h1>登录页</h1>
      <h2>全局变量:{baseURL}</h2>
    </div>
  );
}

// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
const MemoLogin = React.memo(Login);

// 默认导出
export default MemoLogin;

-------3.4:src/store,模块化仓库 redux 的所有文件

2.jpg

--------------3.4.1:src/store/index.tsx,仓库的总出口

// 导入 redux
import { applyMiddleware, legacy_createStore as createStore } from 'redux'
// 导入自己封装的  rootReducer 
import rootReducer from './reducer'
// 导入调试工具和 异步的 redux(用来发送异步请求)
// 调试工具需要下载谷歌 扩展程序 我用的是 Redux DevTools 3.0.17
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'

// 创建仓库实例
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))

// 声明 RootState,在使用仓库的时候用来使用
export type RootState = ReturnType<typeof store.getState>

// 声明 AppDispatch,在异步请求的时候来使用
export type AppDispatch = typeof store.dispatch

// 导出仓库实例
export default store

--------------3.4.2:src/store/action/login.ts,登录模块的action

// 导入总仓库和自己声明的 AppDispatch
import store, { AppDispatch } from ".."
// import http from "@/utils/http"

export const addNunAction = () => {

  // 返回一个函数,用来处理异步操作
  return async (dispatch: AppDispatch) => {
    // 可以异步发送请求,这里我简单模拟一个异步操作
    // const res = await http.get(``)
    // console.log('----', res);
    window.setTimeout(() => {
      // 从仓库中获取之前的 num 值
      const { num } = store.getState().loginStore
      // 设置新的 num +1
      dispatch({ type: 'login/addNum', payload: num + 1 })
    }, 100);
  }
}

--------------3.4.3:src/store/reducer/login.ts,登录模块的reducer

// 导入自己封装的 关于 LoginReducer 的ts声明文件
import { LoginType } from "@/types"

// 初始化状态
const initState: LoginType = {
  num: 0,
  active: 0,
}

// 定义 action 类型
type LoginActionType =
  { type: 'login/addNum', payload: number }

// 频道 reducer
export default function loginReducer(state = initState, action: LoginActionType) {
  switch (action.type) {
    case 'login/addNum':
      return { ...state, num: action.payload }
    default:
      return state
  }
}

--------------3.4.4:src/store/reducer/index.ts,reducer模块的总出口文件(用来合并reducer)

// 导入合并reducer的依赖
import { combineReducers } from 'redux'

// 导入 登录 模块的 reducer
import loginReducer from './login'

// 合并 reducer
const rootReducer = combineReducers({
  loginStore: loginReducer,
})

// 默认导出
export default rootReducer

-------3.5:src/types,整个项目的声明文件

3.jpg

--------------3.5.1:src/types/store/login.d.ts,关于仓库/登录模块reducer的声明文件,这里我简单声明了一下,后面根据项目需求自己定义

export type LoginType = {
  num: number;
  active: number;
}

--------------3.5.2:src/types/declaration.d.ts,一些第三方包没有ts声明,scss或者less,以及图片 都需要在这里定义,避免ts类型检查的报错。这里我简单定义了一下,后面根据项目需求自己完善

declare module 'history'
declare module '*.scss';

--------------3.5.3:src/types/index.d.ts,拿到所有定义的类型声明文件,在这里统一获取,并且导出。方便后期使用的时候管理

//获取上面定义的 关于仓库/登录模块reducer的声明文件 并且导出。后面模块多了,也是在这里全部获取并且导出。
export * from './store/login'

-------3.6:src/utils,项目里面的所有工具的封装

4.jpg

--------------3.6.1:src/utils/history.ts,自己封装的 路由 history,在函数组件tsx和ts中都能使用

// createBrowserHistory 和 createHashHistory。
// 这里我们简单说一下 createBrowserHistory地址栏不带#; createHashHistory 带#
// 根据公司要求自己选择,createBrowserHistory在项目上线的时候需要服务器映射等处理。
import { createHashHistory  } from 'history'
const history = createHashHistory()
export default history

--------------3.6.2:src/utils/http.ts,基于 axios 封装的请求实例

// 导入 axios
import axios from "axios";
// 导入自己封装的 history 用来跳转。useHistory 在单纯的ts文件里面无法使用
import history from "./history";
// 导入自己封装的获取token信息的函数,和删除本地存储token信息的函数
import { getTokenInfo, removeTokenInfo } from "./storage";
// 请求基地址,根据开发环境和打包环境来自己设置。一般打包环境都为空
export const baseURL = process.env.NODE_ENV === "development" ? "开发环境" : "";

// 创建 axios 实例
const http = axios.create({
  baseURL,
  // 请求超过5m没有回来即停止请求,并且抛出异常
  timeout: 5000,
});

// 设置一个请求状态开关,判断 请求正在发送 和 所有请求都发送完毕
let axajInd = 0;

// 请求拦截器
http.interceptors.request.use(
  function (config: any) {
    // 开关数量+1,表示现在有一个请求在发送。
    axajInd++;
    //可以在这里添加业务代码,比如:显示 loding状态

    // 检查到有token,为所有请求添加请求体。Bearer字段为 我这边后端的需求,各位看自己 后端需求来修改
    const { token } = getTokenInfo();
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  function (err) {
    return Promise.reject(err);
  }
);

// 设置一个定时器id,防止多个请求同时发送,并且token失效的情况,多次执行 业务 代码
let timeId = -1;

// 响应拦截器
http.interceptors.response.use(
  function (response) {
    // 一个请求发送完毕
    axajInd--;
    if (axajInd === 0) {
      // 所有请求发送完毕,可以在这里添加业务代码,比如:隐藏 loding状态
    }

    // token过期,这里看后台的返回值设置的为多少
    if (response.data.code === 401) {
      // 先清理定时器,在执行。防止多次执行。即页面防抖原理
      clearTimeout(timeId);

      timeId = window.setTimeout(() => {
        // 删除本地token信息
        removeTokenInfo();
        // 这里的提示统一简单用的alert,自己根据需求更改。比如:antd的message
        alert("登录失效!");
        // 回到登录页面
        history.push("/Login");
      }, 200);
    } else if (response.data.code === 0) {
      // 请求成功,状态也成功(根据后端定义的状态值自己修改)
      // 这里暂时不做处理,我一般会根据具体需求在组件中自己定义 message
    } else {
      //这里一般是请求成功,但是状态码有问题,直接返回后端的提示信息。这里的msg是我这边后端的字段,自己根据后端字段来修改
      alert(response.data.msg);
     }

    return response.data;
  },
  async function (err) {
    // 请求错误的时候,直接把 axajInd 归零,防止页面一直处于 loding 状态
    axajInd = 0;
    // 如果在上面定义了发送请求前显示 loding状态,这里需要隐藏 loding

    // 如果因为网络原因,response没有,给提示消息
    if (!err.response) {
      alert("网络繁忙,请稍后重试");
    } else {
      // 网络没问题,后台返回了有数据
      alert("错误");
    }

    return Promise.reject(err);
  }
);

// 导出 axios 实例
export default http;

--------------3.6.3:src/utils/storage.ts,用户信息(token)的本地存储的封装

// ------------------------------------token的本地存储------------------------------------

// 用户 Token 的本地缓存键名,自己定义
const TOKEN_KEY = 'XXXXX'

/**
 * 从本地缓存中获取 Token 信息
 */
export const getTokenInfo = (): any => {
  return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
}

/**
 * 将 Token 信息存入缓存
 * @param {Object} tokenInfo 从后端获取到的 Token 信息
 */
export const setTokenInfo = (tokenInfo: any): void => {
  localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenInfo))
}

/**
 * 删除本地缓存中的 Token 信息
 */
export const removeTokenInfo = (): void => {
  localStorage.removeItem(TOKEN_KEY)
}

/**
 * 判断本地缓存中是否存在 Token 信息
 */
export const hasToken = (): boolean => {
  return Boolean(getTokenInfo().token)
}


-------3.7:src/App.tsx,App总组件,一个项目的总的初始组件

// 导入初始化样式
import "@/assets/styles/base.css";

// 导入 修改仓库的 dispatch,和获取仓库数据的 useSelector
import { useDispatch, useSelector } from "react-redux";

// 导入自己定义的 RootState 声明类型
import { RootState } from "./store";

// 导入自己定义的异步addNunAction
import { addNunAction } from "./store/action/login";

// 关于路由
import React from "react";
import { Router, Route, Switch, Redirect } from "react-router-dom";

// 导入自己封装的 history
import history from "./utils/history";

// 导入自己封装的 鉴权路由
import AuthRoute from "./components/AuthRoute";

// 使用 React.lazy 懒加载页面
const Layout = React.lazy(() => import("./pages/Layout"));
const Login = React.lazy(() => import("./pages/Login"));
const Home = React.lazy(() => import("./pages/Home"));

function App() {
  const dispatch = useDispatch();
  return (
    <div>
      <h1>App总组件</h1>
      {/* 点击这个按钮 控制  dispatch 来异步 修改仓库的 num 变量*/}
      <button onClick={() => dispatch(addNunAction())}>点击+1</button>
      <br />
      <div onClick={() => history.push("/Login")}>去登录页</div>
      <br />
      <div onClick={() => history.push("/Layout")}>去Layout页面</div>
      <br />
      <div onClick={() => history.push("/Home")}>去Home页面</div>
      <hr />
      <Son1 />
      <hr />
      <Son2 />

      {/* 关于路由 */}
      {/* 这里需要把自己封装的 history 给第一级总路由器,不然项目中的路由信息会链接不上*/}
      <Router history={history}>
        {/* 使用了 React.lazy 需要定义一个加载中组件,这里我简单写一个。后面根据需求自己修改更好看的 加载中 组件*/}
        <React.Suspense fallback={<div>加载中...</div>}>
          {/* 使用 Switch 包裹路由,匹配到一个之后就不会往下匹配 */}
          <Switch>
            {/* 使用路由重定向,回到登录页 */}
            <Redirect exact path="/" to="Login" />
            {/* 普通路由,没有鉴权功能 */}
            <Route path="/Login" component={Login} />
            {/* 普通路由,没有鉴权功能 */}
            <Route path="/Layout" component={Layout} />
            {/* 鉴权路由,没有token,访问改路由,会重定向到 登录页面 */}
            <AuthRoute path="/Home" component={Home} />
            {/* 当所有路由匹配不到的时候显示 自己定义的 404 页面,这里我暂时没有定义 */}
            {/* <Route path='*' component={NotFound} /> */}
          </Switch>
        </React.Suspense>
      </Router>
    </div>
  );
}

const Son1 = function Son1() {
  // 从仓库中获取响应式数据 num
  const { num } = useSelector((state: RootState) => state.loginStore);
  return (
    <>
      <h2>子组件1</h2>
      <p>存在仓库的数字:{num}</p>
    </>
  );
};

const Son2 = function Son2() {
  // 从仓库中获取响应式数据 num
  const { num } = useSelector((state: RootState) => state.loginStore);

  return (
    <>
      <h2>子组件2</h2>
      <p>存在仓库的数字:{num}</p>
    </>
  );
};

const MemoApp = React.memo(App);

export default MemoApp;

-------3.8:src/index.tsx,项目的入口,这里一般不要写太复杂的业务逻辑

// 导入 App,总组件
import App from './App'
// 导入仓库
import store from './store/index'
// 导入仓库的入口 函数(可以简单这么理解。react的所有数据都是单向流动,即从父级页面向下流动。所有需要把仓库信息在第一级里面定义)
import { Provider } from 'react-redux'

// 导入 react 里面定义的 转化dom的方法(简单里面为把 dom 转化成react需要的格式)
import { createRoot } from 'react-dom/client';

// 获取 public/index.html里面id为root的根元素
const container = document.getElementById('root') as HTMLElement;

// 转化成react需求的格式
const root = createRoot(container);

root.render(
  // 仓库信息在根组件中定义
  <Provider store={store}>
    <App />
  </Provider>
  );

4:.gitignore,git忽略跟踪的文件,不必多说

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

5:config-overrides.js,配置类似vue里面的@表示src/根目录

const path = require('path')
const { override, addWebpackAlias } = require('customize-cra')

// 添加 @ 别名
const webpackAlias = addWebpackAlias({
  '@': path.resolve(__dirname, 'src'),
})

// 导出要进行覆盖的 webpack 配置
module.exports = override(webpackAlias)

6:package-lock.json,大家都懂。依赖包的信息,大同小异

7:package.json,为了配置类似vue里面的@表示src/根目录,我修改了 scripts 的 调试命令。大家可以复制我的信息

{
  "name": "demo",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.18.3",
    "@types/react": "^18.0.24",
    "@types/react-dom": "^18.0.8",
    "axios": "^1.1.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-redux": "^8.0.4",
    "react-router-dom": "5.3",
    "react-scripts": "5.0.1",
    "redux": "^4.2.0",
    "redux-devtools-extension": "^2.13.9",
    "redux-thunk": "^2.4.1",
    "sass": "^1.55.0",
    "typescript": "^4.8.4",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "dev": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/history": "^5.0.0",
    "@types/react-router-dom": "^5.3.3",
    "customize-cra": "^1.0.0",
    "react-app-rewired": "^2.2.1"
  },
  "homepage": "."
}

8:path.tsconfig.json,也是为了配置类似vue里面的@表示src/根目录

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

9:README.md,git文档信息。有需求自己修改,我反正从来没动过。

10:tsconfig.json,ts的声明,这里面的信息比较杂,这里不深入展开,可以直接复制我的

{
  "extends": "./path.tsconfig.json",
  "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"
  },
  "include": [
    "src"
  ]
}

经过一番“辛苦”的配置,满心欢喜的 npm run dev。然后你就会发现。

1.jpg

这里的原因个人判断是 redux 和 react-redux 版本更替过程中的ts类型声明问题(个人推断,各位大佬有很好的理解或者解决方法也麻烦告诉小弟~)

解决办法1:

在使用 useDispatch 的时候 添加 any 泛型

即:原来是 const dispatch = useDispatch();

修改为:const dispatch = useDispatch<any>();
    
但是这样在每次使用的时候都要定义any,不太友好。
    
你也可以自己定义一个函数,然后调用自己的函数
    
const useUserDispatch = () => useDispatch<any>();
    
即:原来是 const dispatch = useDispatch();

修改为:const dispatch = useUserDispatch();

但个人认为也不太好,毕竟多了一层封装。

解决办法2:修改源代码ts类型声明

1.找到 import { useDispatch, useSelector } from "react-redux";

2.按住键盘的Ctrl+鼠标左键打开

1.jpg

3.找到 AnyAction,继续按住键盘的Ctrl+鼠标左键打开

1.jpg

4.进入 node_modules > redux > index.d.ts 找到(或者你直接搜索下面的代码)

1.jpg

export interface Action<T = any> {
  type: T
}

5.加上一个小小的?,一切都解决了

1.jpg

export interface Action<T = any> {
  type?: T
}

再次 npm run dev ,你就会看到一个很丑的页面。点击按钮 点击+1 就能完成异步的 active 控制仓库的响应式数据。然后根据业务需求来美美的开发吧。

1.jpg

分享 redux 的调试工具

考虑到不能翻墙的小伙伴,我这里准备了一个链接

redux 的调试工具

谷歌-设置-扩展程序-打开开发者模式-解压下载的文件-然后整个拖进去

打开控制台,就可以看到仓库数据的每一步变化。讲道理,个人觉得比vue的调试工具好用得多

1.jpg


完结撒花

各位大佬有不同的想法或者本人写错的地方,欢迎指正。