使用 create-react-app 搭建项目ts+less+antd+redux+router+eslint+prettier+axios

使用 create-react-app 搭建项目

当前市面上有很多前端框架或者模板、如:umidvaantd-design-procreate-react-app 等一些框架或者模板。
create-react-app 是 react 官方提供的,相对来说比较干净一些。
此项目是在 create-react-app 的基础上进行搭架、项目采用 ts 语法

项目整体上会添加上以下功能:

1、antd 组件库
2、redux 状态管理工具
3、router 路由工具、路由配置
4、eslint 代码检测工具
5、prettier 代码格式化工具
6、less css 预编辑处理
7、接口请求处理 axios
8、一些常用组件
9、工具类
10、本地跨域处理
11、配置别名@

完整项目代码 传送门

1 create-react-app 创建基础项目

1.1 全局安装 create-react-app

npm install -g create-react-app
# or
yarn add -g create-react-app

1.2 初始化项目

#npx create-react-app 项目名称 --template typescript
npx create-react-app test-project --template typescript

项目安装成功:

在这里插入图片描述

项目结构:

在这里插入图片描述

运行项目:

npm start

运行成功:

在这里插入图片描述

1.3 释放配置文件

npm run eject

运行后项目结构:

在这里插入图片描述

2 配置别名 @

1、找到 config/webpack.config.js 文件

// 找到 resolve 下的 alias 配置项(大约在312行),添加以下配置:
alias: {
    // Support React Native Web
    // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
    "react-native": "react-native-web",
    // Allows for better profiling with ReactDevTools
    ...(isEnvProductionProfile && {
        "react-dom$": "react-dom/profiling",
        "scheduler/tracing": "scheduler/tracing-profiling",
    }),
    ...(modules.webpackAliases || {}),
    // 文件路径别名
    "@": path.resolve(__dirname, "../src"),  // 添加行
    "@": paths.appSrc, // 添加行
},

2、在项目根目录下找到 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",
    "baseUrl": "src", //新增此配置
    "paths":{ //新增此配置
      "@/*":["*"]
    }
  },
  "include": [
    "src"
  ]
}

别名配置成功:

在这里插入图片描述

3 项目引入 less

3.1 安装

npm install -S less less-loader

3.2 配置

在项目中找到 src/react-app-env.d.ts 文件添加一下代码:

declare module "*.less" {
  const less: any;
  export default less;
}

在项目中找到 config/webpack.config.js 文件添加一下代码:

// 在73行添加以下代码
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;

// 在509行添加以下代码
{
   test: lessRegex,
   exclude: lessModuleRegex,
   use: getStyleLoaders(
     {
       importLoaders: 2,
       // modules: true,如果仅打开cssModule  那么原类名 将会没有前缀,无法与自己的样式类名关联,所以下边做法可取
       sourceMap: isEnvProduction
       ? shouldUseSourceMap
       : isEnvDevelopment,
       modules: {
         mode: 'icss',
       },
     },
     'less-loader'
   ),
   sideEffects: true,
},
{
   test: lessModuleRegex,
       sourceMap: isEnvProduction
         ? shouldUseSourceMap
         : isEnvDevelopment,
       modules: {
         mode: 'local',
         getLocalIdent: getCSSModuleLocalIdent,

到此已完成 less 预编译器的安装了

4 项目引入 ant-design

npm install antd --save
# or
yarn add antd

4.1 组件中使用

import React from "react";
import "@/App.less";
import { Button } from "antd";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Button type="primary">Button</Button>
      </header>
    </div>
  );
}

export default App;

在这里插入图片描述

4.2 配置主题色

在你想要的位置创建一个主题文件、方便整体配置:
我的配置位置是 /src/config/style/themeConfig.ts

export const themeConfig = {
	token: {
		colorInfo: '#f4335e',
		colorPrimary: '#f4335e',
		colorBgLayout: '#F2F4FA',
		layoutHeaderColor: 'white',
		colorTextHeading: 'rgba(0,0.85)',
		borderRadius: 2,
		borderRadiusLG: 4,
	},
	//colorTextLightSolid
	components: {
		Tooltip: {
			fontSize: 12,
			colorBgDefault: 'white',
			colorTextLightSolid: 'block',
		},
};

在项目初始入口(/src/App.tsx)添加主题配置:

import React from "react";
import "@/App.less";
import Home from "./pages/home";
import dayjs from "dayjs";
import zhCN from "antd/locale/zh_CN";
import { ConfigProvider } from "antd";
import { themeConfig } from "@/config/style/themeConfig";

dayjs.locale("zh-cn");

function App() {
  return (
    <ConfigProvider locale={zhCN} theme={themeConfig}>
      <Home />
    </ConfigProvider>
  );
}

export default App;

效果图:

在这里插入图片描述

5 添加路由、添加路由守护

npm i react-router-dom
# or
yarn add react-router-dom

5.1 首先创建一些页面

添加一下页面

在这里插入图片描述

5.2 添加路由

我创建的位置在这里 /src/config/routes/index.ts

import { lazy, Suspense } from "react";
import { useRoutes, Navigate } from "react-router-dom";
import { UserOutlined, HomeOutlined } from "@ant-design/icons";

//layout
import Loading from "@/components/loading";

import Login from "@/pages/login/index";
import NotFoundPage from "@/pages/404";
const Home = lazy(() => import("@/pages/home"));
const User = lazy(() => import("@/pages/user"));

// 上层加载
const lazyComponent = (element: JSX.Element) => {
  return <Suspense fallback={<Loading />}>{element}</Suspense>;
};

const baseRoutes: any = [
  {
    path: "/login",
    auth: false, // 是否需要登录
    children: [
      {
        path: "/login",
        auth: false, // 是否需要登录
        element: <>{lazyComponent(<Login />)}</>,
      },
    ],
  },
];

const layoutRoutes: any = [
  { path: "/", element: <Navigate to="/home" /> },
  {
    path: "/",
    children: [
      {
        path: "/home",
        name: "首页",
        auth: true, // 是否需要登录
        icon: <HomeOutlined className="menu-icon" />, // 菜单栏图标
        isMenu: true, // 是否菜单栏显示
        element: <>{lazyComponent(<Home />)}</>,
      {
        path: "/user",
        name: "个人中心", // 是否需要登录
        icon: <UserOutlined className="menu-icon" />,
        isMenu: true, // 是否菜单栏显示
        element: <>{lazyComponent(<User />)}</>,
      { path: "*", element: <Navigate to="/404" /> },
      {
        path: "/404",
        element: (
          <>
            <NotFoundPage />
          </>
        ),
];

export const routes: any = [
  ...baseRoutes,
  ...layoutRoutes,
  { path: "*",
  {
    path: "/404",
    element: (
      <>
        <NotFoundPage />
      </>
    ),
];

function Router() {
  return useRoutes(routes);
}

//根据路径获取路由
const checkAuth = (routers: any, path: String) => {
  for (const data of routers) {
    if (data.path == path) return data;
    if (data.children) {
      const res: any = checkAuth(data.children, path);
      if (res) return res;
    }
  }
  return null;
};

const checkRouterAuth = (path: string) => {
  let auth = null;
  auth = checkAuth(routes, path);
  return auth;
};

export default Router;
export { checkRouterAuth };

5.3 添加路由守卫

我创建的位置在这里 /src/config/routes/RouterBeforeEach.ts

import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { Outlet } from "react-router-dom";

import { checkRouterAuth } from "./index";

const RouterBeforeEach = () => {
  const navigate = useNavigate();
  const location: any = useLocation();
  const [auth, setAuth] = useState(false);

  useEffect(() => {
    const obj = checkRouterAuth(location.pathname);
    const blLogin = ""; // token
    // 判断是否有权限
    if (obj && obj.auth && !blLogin) {
      setAuth(false);
      navigate("/login", { replace: true });
    } else {
      setAuth(true);
    }
  }, [location, navigate]);

  return auth ? <Outlet /> : null;
};

export default RouterBeforeEach;

5.4 页面引入

添加路由

import "@/App.less";
import dayjs from "dayjs";
import zhCN from "antd/locale/zh_CN";
import { ConfigProvider } from "antd";
import { themeConfig } from "@/config/style/themeConfig";
import { BrowserRouter } from "react-router-dom";

import Router from "@/config/routes";

dayjs.locale("zh-cn");

function App() {
  return (
    <ConfigProvider locale={zhCN} theme={themeConfig}>
      <BrowserRouter>
        {/* The rest of your app goes here */}
        <Router />
      </BrowserRouter>
    </ConfigProvider>
  );
}

export default App;

页面使用:

import { Button } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import "./index.less";

const Home: React.FC = () => {
  const navigate = useNavigate();
  const jumpPage = () => {
    navigate("/user");
  };
  return (
    <div className="home">
      <h1>{"首页"}</h1>
      <Button type="primary" className="button" onClick={() => jumpPage()}>
        {"个人中心"}
      </Button>
    </div>
  );
};

export default Home;

效果:

在这里插入图片描述

6 状态管理工具 redux

6.1 安装

npm install react-redux redux-persist @reduxjs/toolkit
# or
yarn add react-redux redux-persist @reduxjs/toolkit

6.2 代码实现

配置: /store/index.ts

import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist";
import storageSession from "redux-persist/lib/storage/session"; // defaults to sessionStorage for web
import userReducer from "./features/userInfoSlice";

// 持久化配置
const persistConfig = {
  key: "__NJOY__",
  storage: storageSession,
};

const persistedReducer = persistReducer(
  persistConfig,
  combineReducers({
    // 合并切片
    userInfo: userReducer,
  })
);

// 创建store
export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      // 忽略序列化检查
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
});

export const persistor = persistStore(store);

// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

配置: /store/hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";

import type { RootState, AppDispatch } from "./index";

// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

实例: /src/store/features/userInfoSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface userState {
  token: string;
  info: {
    name?: string;
    phone?: string;
    nickname?: string;
    avatar?: string;
  };
  menus: any;
}

const initialState: userState = {
  token: "",
  info: {},
  menus: {},
};

export const userInfoSlice = createSlice({
  name: "userInfo",
  initialState,
  reducers: {
    setUserToken: (state, action: PayloadAction<string>) => {
      state.token = action.payload;
    },
    setUserInfo: (state, action) => {
      state.info = action.payload;
    },
    setMenus: (state, action) => {
      state.menus = action.payload;
    },
});
// 每个 case reducer 函数会生成对应的 Action creators
export const { setUserToken, setUserInfo, setMenus } = userInfoSlice.actions;

export default userInfoSlice.reducer;

6.3 全局引入

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import App from "./App";
import reportWebVitals from "./reportWebVitals"; // 设置语言
import Loading from "@/components/loading";
import { store, persistor } from "@/store";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={<Loading />} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app,pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

6.4 代码中使用

写入页面:

import { Button } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import "./index.less";
import { store } from "@/store";
import { setUserInfo } from "@/store/features/userInfoSlice";

const Login: React.FC = () => {
  const navigate = useNavigate();
  const jumpPage = () => {
    // 写入数据
    store.dispatch(
      setUserInfo({
        name: "wolf.ma",
        phone: "133****5960",
        nickname: "wolf",
        avatar: "wolf.png",
      })
    );
    navigate("/home");
  };

  return (
    <div className="home">
      <h1>{"登录"}</h1>
      <Button type="primary" className="button" onClick={() => jumpPage()}>
        {"登录"}
      </Button>
    </div>
  );
};

export default Login;

读取页面:

import { Button } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import "./index.less";
import { useAppSelector } from "@/store/hooks";

const Home: React.FC = () => {
  // 读取数据
  const userInfo = useAppSelector((state) => state.userInfo.info);
  const navigate = useNavigate();
  console.log("userInfo", userInfo);
  const jumpPage = () => {
    navigate("/user");
  };

  return (
    <div className="home">
      <h1>{"首页"}</h1>
      <h1>{userInfo.name}</h1>
      <Button type="primary" className="button" onClick={() => jumpPage()}>
        {"个人中心"}
      </Button>
    </div>
  );
};

export default Home;

7 axios 请求处理

npm install axios
# or
yarn add axios

7.1 封装请求

import { notification } from "antd";
import axios, { AxiosRequestHeaders, AxiosResponse, AxiosError } from "axios";

// 创建 axios 实例   withCredentials: true,
const service = axios.create({
  // API 请求的默认前缀
  baseURL: process.env.REACT_APP_API_URL,  // 接口原地址
  timeout: 1000 * 60 * 3, // 请求超时时间
  responseType: "json",
});

// 异常拦截处理器
const errorHandler = (error: AxiosError) => {
  if (error && error.message && error.message.includes("timeout")) {
    notification.error({
      message: "请求超时",
      description: "请重试",
      duration: 0,
    });
  }
  if ((error as any).data) {
    const data = (error as any).data;
    if (data.code === 403) {
      notification.error({
        message: "请求错误",
        description: data.message,
      });
    }
  }
  return Promise.reject(error);
};

// 请求拦截
service.interceptors.request.use((config: any) => {
  if (!navigator.onLine) {
    notification.error({
      message: "网络断开",
      description: "请检查网络",
    });
  }
  // const token = ls.get(ACCESS_TOKEN);
  // const cid = ls.get(COMPANY_ID);
  const token = "6ce8bb8456bb819cf6627a57dc90fb93";
  const cid = "100028";
  if (token) (config.headers as AxiosRequestHeaders)["X-Token"] = token;
  if (cid) (config.headers as AxiosRequestHeaders)["X-Cid"] = cid;
  return config;
}, errorHandler);

// 响应拦截
let hasExist = false;

service.interceptors.response.use((res: AxiosResponse<any>) => {
  // 身份已失效,请重新登录
  if (res.data.code === 4001) {
    if (!hasExist) hasExist = true;
    hasExist = true;
    notification.error({
      message: "提示",
      description: "身份已失效,请重新登录",
    });
  } else {
    hasExist = false;
  }
  return res.data;
}, errorHandler);

// 通用get
const $get = (url: string, params?: object, _object = {}): Promise<any> => {
  return service.get(url, { params, ..._object });
};
// 通用post
const $post = (url: string, _object = {}): Promise<any> => {
  return service.post(url, params, _object);
};

export { $get, $post };

export { service as axios };

在项目根目录添加 .env.env.test.env.development 文件

// 本地跨域处理
// 测试
BASE_URL = https://test-api-beidou.netjoy.com
// 生产
# BASE_URL = https://api-beidou.netjoy.com

// 跨域 key值
REACT_APP_API_URL = /api

7.2 本地跨域配置

src 文件下添加 setupProxy.js 文件

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
	app.use(
		createProxyMiddleware('/api', {
			target: process.env.BASE_URL,
			changeOrigin: true,
			pathRewrite: {
				'^/api': '/api',
			},
		})
	);
};

7.3 页面中使用

api 接口填写 api/index.ts

import { $post } from "@/utils/axios";

// 用户登录
export async function login(parameter: any): Promise<any> {
  return $post("/user/login", parameter);
}

接口使用

import { Button } from "antd";
import React from "react";
import { useNavigate } from "react-router-dom";
import "./index.less";
import { store } from "@/store";
import { setUserInfo } from "@/store/features/userInfoSlice";
import { login } from "@/config/api"; // 引入接口

const Login: React.FC = () => {
  const navigate = useNavigate();
  const jumpPage = async () => {
    const res: any = await login({}); // 调用接口
    console.log("res", res);
    // 写入数据
    store.dispatch(
      setUserInfo({
        name: "wolf.ma",
        phone: "13381765960",
      })
    );
    navigate("/home");
  };
  return (
    <div className="home">
      <h1>{"登录"}</h1>
      <Button type="primary" className="button" onClick={() => jumpPage()}>
        {"登录"}
      </Button>
    </div>
  );
};
export default Login;

8 layout 布局和组件

8.1 基础布局

空白页面

BasicLayout.tsx

import { Layout } from "antd";
import type { FC } from "react";

import "./BasicLayout.less";
import RouterBeforeEach from "@/config/routes/RouterBeforeEach";
const { Content } = Layout;

const BasicLayout: FC = () => {
  // const currentYear = new Date().getFullYear();

  return (
    <Layout className="basiclayout">
      <Content className="content">
        <RouterBeforeEach />
      </Content>
      {/* <Footer className="footer">©{currentYear} 乐推网络科技有限公司</Footer> */}
    </Layout>
  );
};

export default BasicLayout;

BasicLayout.less

.basiclayout {
	height: 100%;
	overflow: hidden;

	.content {
		flex: none;
		min-height: 100%;
	}

	.footer {
		font-size: 14px;
		color: rgb(0 0 0 / 45%);
		text-align: center;
	}
}

8.2 主要布局

含有 header,menu,Breadcrumb 等基础组件

MainLayout.tsx

import { MenuUnfoldOutlined, MenuFoldOutlined } from "@ant-design/icons";
import { Layout } from "antd";
import classNames from "classnames";
import React, { useState } from "react";
import HeadTop from "@/components/Header";
import SideMenu from "@/components/Menu";
import NjoyBreadcrumb from "@/components/NjoyBreadcrumb";
import RouterBeforeEach from "@/config/routes/RouterBeforeEach";
import { themeConfig } from "@/config/style/themeConfig";
import "./MainLayout.less";

const { Header, Content, Footer, Sider } = Layout;

const MainLayout: React.FC = () => {
  const currentYear = new Date().getFullYear();
  const [collapsed, setCollapsed] = useState(false);

  // 修改布局
  const toggleCollapsed = () => {
    setCollapsed(!collapsed);
  };

  return (
    <Layout className={"mainLayout"}>
      <Header className={"header"}>
        <HeadTop />
      </Header>
      <Layout>
        <Sider
          width={208}
          collapsedWidth={50}
          className={classNames("sider", { padding0: collapsed })}
          collapsed={collapsed}
        >
          <SideMenu collapsed={collapsed} />
          <div className="collapsed" onClick={toggleCollapsed}>
            {collapsed ? (
              <MenuUnfoldOutlined
                style={{ color: themeConfig.token.colorPrimary }}
              />
            ) : (
              <MenuFoldOutlined
                style={{ color: themeConfig.token.colorPrimary }}
              />
            )}
          </div>
        </Sider>
        <Layout className={"layoutContent"}>
          <NjoyBreadcrumb />
          <Content className={"content"}>
            <RouterBeforeEach />
          </Content>
          <Footer className={"footer"}>
            ©{currentYear} 乐推网络科技有限公司
          </Footer>
        </Layout>
      </Layout>
    </Layout>
  );
};

export default MainLayout;

MainLayout.less

.mainLayout {
	height: 100%;
	overflow: hidden;

	.header {
		padding-inline: 24px;
		background: #fff;
		border-bottom: 1px solid rgb(0 21 41 / 6%);
		max-height: 56px;
		min-height: 56px;
	}

	.sider {
		padding: 24px 8px 0;
		margin: 24px 0 0 24px;
		background: #fff;
		position: relative;
		border-radius: 8px;
		// box-shadow: 0 2px 4px rgba(0, 0, .1);

		&.padding0 {
			padding: 0;
		}

		.collapsed {
			position: absolute;
			left: 0;
			bottom: 0;
			padding: 10px 10px 20px;
			width: 100%;
			text-align: right;
			font-size: 15px;
			background-color: transparent;
			// border-top: 1px solid #eee;
			cursor: pointer;
		}
	}

	.layoutContent {
		padding: 24px 24px 0 24px;
		overflow: auto;
	}

	.content {
		flex: 1;
	}

	.footer {
		font-size: 14px;
		color: rgb(0 0 0 / 45%);
		text-align: center;
	}
}

8.3 基础组件

Menu

menu.tsx

import { Menu, MenuProps } from "antd";
import { useEffect, useState } from "react";
import type { FC } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { setMenus } from "@/store/features/userInfoSlice";
import { store } from "@/store";
import { routes } from "@/config/routes";
import "./index.less";

type MenuItem = Required<MenuProps>["items"][number];

type menuProps = {
  collapsed?: boolean;
};

const SideMenu: FC<menuProps> = (props) => {
  const navigateTo = useNavigate();
  const currentRoute = useLocation();
  const { collapsed = false }: any = props;
  const [items, setItems] = useState<MenuItem[]>([]);
  /**
   * 处理菜单数据 用于显示菜单栏
   * 最多只有三层 第一层不处理
   */
  useEffect(() => {
    let tempItems: any = [];
    if (routes && routes.length > 0) {
      routes.forEach((el: any) => {
        // 第一层数据
        if (el.children) {
          // 是否有子项
          el.children.forEach((it: any) => {
            let tempObject: any = {};
            // 第二层
            if (it.isMenu) {
              // 是否菜单
              tempObject = {
                label: it.name,
                key: it.path,
                icon: it.icon,
              };
            }
            if (it.children) {
              tempObject.children = [];

              // 是否有子项
              it.children.forEach((item: any) => {
                // 第三层
                if (item.isMenu) {
                  // 是否菜单
                  tempObject.children.push({
                    label: item.name,
                    key: item.path,
                    icon: item.icon,
                  });
                }
              });
            }
            if (tempObject.label) {
              tempItems.push(tempObject);
            }
          });
        }
      });
    }
    setItems(tempItems);
  }, [routes]);

  /**
   * 处理菜单数据 用于显示面包屑
   */
  useEffect(() => {
    let menus: any[] = [];
    if (routes && routes.length > 0) {
      routes.forEach((el: any) => {
        // 第一层数据
        if (!el.children) {
          menus[el.path] = [el];
        } else {
          el.children.forEach((it: any) => {
            // 第二层数据
            menus[it.path] = [it];
            if (it.children) {
              it.children.forEach((item: any) => {
                menus[item.path] = [it, item];
              });
            }
          });
        }
      });
    }
    store.dispatch(setMenus(menus));
  }, [routes]);

  // 菜单点击
  const menuClick = (e: { key: string }) => {
    navigateTo(e.key);
  };

  //拿着currentRoute.pathname跟items数组的每一项的children的key值进行对比,如果找到了相等,
  //就要他上一级的key,这个key给到openKeys数组的元素,作为初始值
  let firstOpenKey = "";
  function findKey(obj: { key: string }) {
    return obj.key === currentRoute.pathname;
  }
  // 对比的是多个children
  function findFirstOpenKey() {
    for (let i = 0; i < items.length; i++) {
      let itemT: any = items[i];
      if (
        itemT!["children"] &&
        itemT!["children"].length > 0 &&
        itemT!["children"].find(findKey)
      ) {
        firstOpenKey = itemT!.key as string;
        break;
      }
    }
  }
  //设置展开项的初始值
  const [openKeys, setOpenKeys] = useState([firstOpenKey]);
  const handleOpenChange = (keys: string[]) => {
    setOpenKeys([keys[keys.length - 1]]);
  };

  useEffect(() => {
    findFirstOpenKey();
    setOpenKeys([firstOpenKey]);
  }, [currentRoute.pathname, items]);

  return (
    <Menu
      className="sider-menu"
      selectedKeys={[currentRoute.pathname]}
      mode="inline"
      theme="light"
      items={items}
      onClick={menuClick}
      onOpenChange={handleOpenChange}
      openKeys={openKeys}
    />
  );
};
export default SideMenu;

menu.less

.sider-menu {
	width: 100%;
	margin-inline: 0;
	font-weight: 500;
	border-inline-end: 0 solid rgba(5, 5, 0.06) !important;

	.ant-menu-submenu-title {
		// padding-left: 0 !important;
	}

	.ant-menu-vertical {
		border-inline-end: 0 !important;
	}

	.ant-menu.ant-menu-inline,.ant-menu-sub.ant-menu-inline {
		background: transparent !important;
	}

	.ant-menu-inline {
		background: transparent;
	}

	.ant-menu-vertical {
		border-inline-end: 0 solid rgba(5, 0.06);
	}
}

header

header.tsx

import type { FC } from "react";
import logo from "@/assets/logo.png";
import "./index.less";
// 组件
import AvatarDropdown from "@/components/AvatarDropdown";

const Header: FC = () => {
  return (
    <div className="headerContainer">
      <img src={logo} className="appLogo" alt="logo" />
      <div className="rightContent">
        <AvatarDropdown />
      </div>
    </div>
  );
};
export default Header;

header.less

.headerContainer {
	display: flex;
	align-items: center;
	justify-content: space-between;
	width: 100%;
	height: 100%;
	.rightContent {
		display: flex;
		div.language {
			margin-right: 20px;
			span {
				color: rgba(0, 0.45);
				cursor: pointer;
				strong {
					font-weight: 500;
					color: rgba(0, 0.85);
				}
			}
		}
	}
	.appLogo {
		height: 24px;
		margin-left: 20px;
	}
}
breadcrumb

breadcrumb.tsx

import React, { useEffect, useState } from "react";
import { Breadcrumb } from "antd";
import { useLocation, NavLink } from "react-router-dom";
import { useAppSelector } from "@/store/hooks";
import "./index.less";

const NjoyBreadcrumb: React.FC = () => {
  const menus = useAppSelector((state) => state.userInfo.menus);
  const history = useLocation();
  const [breadcrumbMeuns, setBreadcrumbMeuns] = useState<any>();

  /**
   * 处理历史信息
   */
  useEffect(() => {
    const tempArray = menus[history.pathname];
    setBreadcrumbMeuns(tempArray);
  }, [history, menus]);

  return (
    <div className="n-breadcrumb">
      <div className="breadcrumb">
        <Breadcrumb>
          <Breadcrumb.Item>
            <NavLink className={"last-color"} to="/home">
              首页
            </NavLink>
          </Breadcrumb.Item>
          {breadcrumbMeuns &&
            breadcrumbMeuns.length > 0 &&
            breadcrumbMeuns.map((item: any, index: number) => {
              if (item.path === "/home") return;
              return (
                <Breadcrumb.Item>
                  <NavLink
                    className={
                      breadcrumbMeuns.length - 1 === index
                        ? "last-color-active"
                        : "last-color"
                    }
                    to={item.path}
                  >
                    {item.name}
                  </NavLink>
                </Breadcrumb.Item>
              );
            })}
        </Breadcrumb>
      </div>
    </div>
  );
};

export default NjoyBreadcrumb;

breadcrumb.less

.n-breadcrumb {
	width: 100%;
	height: 40px;
	background-color: #fff;

	.breadcrumb {
		height: 100%;
		border-bottom: 1px solid rgb(0 21 41 / 6%);
		display: flex;
		justify-content: flex-start;
		align-items: center;
		padding-left: 16px;

		.last-color {
			color: #888;
			background-color: transparent;
		}
		.last-color-active {
			color: #333;
			background-color: transparent;
			font-weight: 500;
		}
		.last-color:hover,.last-color-active:hover {
			color: #000;
			background-color: transparent;
		}
	}
}

8.4 布局的使用方法

在路由菜单 /config/routes/index.tsx 中添加

import { lazy, HomeOutlined } from "@ant-design/icons";

// 引入 layout
import BasicLayout from "@/layouts/BasicLayout";
import MainLayout from "@/layouts/MainLayout";
import Loading from "@/components/loading";

import Login from "@/pages/login/index";
import NotFoundPage from "@/pages/404";
const Home = lazy(() => import("@/pages/home"));
const User = lazy(() => import("@/pages/user"));

// 上层加载
const lazyComponent = (element: JSX.Element) => {
  return <Suspense fallback={<Loading />}>{element}</Suspense>;
};

const baseRoutes: any = [
  {
    path: "/login", // 是否需要登录
    element: <BasicLayout />, // 添加布局
    children: [
      {
        path: "/login",
    element: <MainLayout />, path: String) => {
  for (const data of routers) {
    if (data.path === path) return data;
    if (data.children) {
      const res: any = checkAuth(data.children, path);
  return auth;
};

export default Router;
export { checkRouterAuth };

布局演示:

在这里插入图片描述

9 eslint 代码检测工具

在原有的基础上安装以下包

yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-react

9.1 eslint 配置

在项目根目录添加 配置文件:.eslintrc.js、忽略文件.eslintignore 文件
.eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
  parser: "@typescript-eslint/parser",
  extends: [
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/jsx-runtime",
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 13,
    sourceType: "module",
  plugins: ["import", "react", "react-hooks", "@typescript-eslint"],
  settings: {
    react: {
      version: "detect",
  rules: {
    //  ----------------------------    代码执行方式 start ↓    -----------------------------
    // // 禁用 'semi' 规则
    // semi: "error",
    // // 使用 '@typescript-eslint/no-extra-semi' 规则 (强制单行结束必须要有分号结尾)
    // "@typescript-eslint/no-extra-semi": "off",
    // 对已声明未使用的变量报错
    "@typescript-eslint/no-unused-vars": "error",
    "no-unused-vars": "off",
    //  不允许if 和 else if判断相同
    "no-dupe-else-if": "error",
    //  强制变量必须小驼峰命名规则
    camelcase: "off",
    //  强制必须使用完全比较
    eqeqeq: "error",
    //  禁止else语句中只包含 if语句,应该修改为 else if
    "no-lonely-if": "error",
    //  允许ts指定类型为any
    "@typescript-eslint/no-explicit-any": "off",
    //  允许对非null进行断言
    "@typescript-eslint/no-non-null-assertion": "off",
    //  允许定义空接口
    "@typescript-eslint/no-empty-interface": "off",
    // 允许使用  // @ts-ignore
    "@typescript-eslint/ban-ts-comment": "off",
    //  禁止在函数中进行无意义的返回
    "no-useless-return": "error",
    //  对于字符串拼接,限制只能使用字符串模板的方式 `hello ${name}`
    "prefer-template": "error",
    //  限制模块导入不可重复
    "no-duplicate-imports": "error",
    //  允许使用require引入模块
    "@typescript-eslint/no-var-requires": 0,
    //  忽略提示react弃用方法
    "react/no-deprecated": "off",
    //  暂时先关闭未使用setState更新的错误报警,后面统一处理
    "react/no-direct-mutation-state": "off",
    // 重新配置 react-hooks 相关内容
    "react-hooks/rules-of-hooks": "error",
    //  ----------------------------    代码执行方式 end ↑    -----------------------------

    //  ----------------------------    代码外观 start ↓    -----------------------------
    //  配置import模块进行分组
    "import/order": [
      "error",
      {
        groups: [
          ["builtin", "external"],
          "internal",
          "parent",
          "sibling",
          "index",
        ],
        "newlines-between": "always",
        pathGroups: [
          {
            pattern: "../**",
            group: "parent",
            position: "after",
          },
          {
            pattern: "./*.scss",
            group: "sibling",
        alphabetize: {
          order: "asc",
          caseInsensitive: true,
        },
    //  ----------------------------    代码外观 end ↑    -----------------------------
  },
};

.eslintignore

build/*.js
src/assets
src/components
public
china.js
scripts
config

package.jsonscripts 文件中添加以下代码:

package.json

	"scripts": {
		"start": "nj dev",
		"build:development": "nj build development",
		"build:test": "nj build test",
		"build:pre": "nj build preview",
		"build": "nj build",
		"lint:eslint": "eslint . --ext .ts,.tsx,.js,jsx --fix"
	},

9.2 进行代码检查

npm run lint:eslint

第一次运行结果

在这里插入图片描述

代码修复后,再次运行

在这里插入图片描述

10 prettier 代码格式化工具

# eslint-plugin-prettier 用于解决eslint 和 prettier 的冲突
npm install -D eslint-config-prettier eslint-plugin-prettier prettier

10.1 prettier 配置

在项目根目录添加 配置文件:.prettierrc.js、忽略文件:.prettierignore

.prettierrc.js

module.exports = {
	endOfLine: 'auto',
	// 书写宽度
	printWidth: 100,
	// 缩进字节数
	tabWidth: 4,
	// 语句末尾打印分号
	semi: true,
	// 使用单引号
	singleQuote: true,
	// 尾随逗号
	trailingComma: 'es5',
	// 在方法的花括号前面加空格
	spaceBeforeFunctionParen: true,
	// 用键位tab缩进
	useTabs: true,
	// 标签换行不完整问题
	htmlWhitespaceSensitivity: 'ignore',
	// 在唯一的箭头函数参数周围包含括号
	arrowParens: 'always',
	endOfLine: 'auto',
};

.prettierignore

.idea
build
node_modules
src/assets
# src/components
public
.gitignore

package.jsonscripts 文件中添加以下代码:

package.json

  "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js",
    "lint:fotmet": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"",
    "lint:eslint": "eslint . --ext .ts,jsx --fix"
  },

10.2 运行代码格式化

npm run  lint:fotmet

运行代码格式化

在这里插入图片描述

10.3 处理与 eslint 的冲突

当 ESLint 的规则和 Prettier 的规则相冲突时,就会发现一个尴尬的问题,用其中一种来格式化代码,另一种就会报错。prettier 官方提供了一款工具 eslint-config-prettier 来解决这个问题。本质上这个工具其实就是禁用掉了一些不必要的以及和 Prettier 相冲突的 ESLint 规则。

.eslintrc.js 中添加代码处理两者之间的冲突

.eslintrc.js

// 1、在 extends 中添加
extends: [
  'plugin:react/recommended',
  'plugin:react-hooks/recommended',
  'plugin:@typescript-eslint/recommended',
  'plugin:prettier/recommended',  // 添加此行代码
  'plugin:react/jsx-runtime',
],

// 2、在 plugins 中添加
plugins: ['import', 'react', 'react-hooks', 'prettier', '@typescript-eslint'],

// 3、在 rules 中添加
rules: {
  'prettier/prettier': [
			'error',
			{
				endOfLine: 'auto',
				semi: true,
				singleQuote: true,
				tabWidth: 4,
				trailingComma: 'es5',
		],
}

到此项目已完成。
本人小白,求指点

相关文章

文章浏览阅读774次,点赞24次,收藏16次。typescript项目中我...
文章浏览阅读784次。react router redux antd eslint pretti...
文章浏览阅读3.9k次,点赞5次,收藏11次。需要删除.security...
文章浏览阅读1.2k次,点赞23次,收藏24次。Centos 8 安装es_...
文章浏览阅读3.2k次。设置完之后,数据会⾃动同步到其他节点...
文章浏览阅读1.9k次,点赞2次,收藏7次。针对多数据源写入的...