使用 create-react-app 搭建项目
当前市面上有很多前端框架或者模板、如:umi、dva、antd-design-pro、create-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,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
},
'less-loader'
),
},
到此已完成 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,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: "个人中心",
auth: true, // 是否需要登录
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: "*", element: <Navigate to="/404" /> },
{
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, params?: object, _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",
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;
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, 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, 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, 5, 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, 0, 0.45);
cursor: pointer;
strong {
font-weight: 500;
color: rgba(0, 0, 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, Suspense } from "react";
import { useRoutes, Navigate } from "react-router-dom";
import { UserOutlined, 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",
auth: false, // 是否需要登录
element: <BasicLayout />, // 添加布局
children: [
{
path: "/login",
auth: false, // 是否需要登录
element: <>{lazyComponent(<Login />)}</>,
},
],
},
];
const layoutRoutes: any = [
{ path: "/", element: <Navigate to="/home" /> },
{
path: "/",
element: <MainLayout />,
children: [
{
path: "/home",
name: "首页",
auth: true, // 是否需要登录
icon: <HomeOutlined className="menu-icon" />, // 菜单栏图标
isMenu: true, // 是否菜单栏显示
element: <>{lazyComponent(<Home />)}</>,
},
{
path: "/user",
name: "个人中心",
auth: true, // 是否需要登录
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: "*", element: <Navigate to="/404" /> },
{
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 };
布局演示:
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",
position: "after",
},
],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
// ---------------------------- 代码外观 end ↑ -----------------------------
},
};
.eslintignore
build/*.js
src/assets
src/components
public
china.js
scripts
config
在 package.json
的 scripts
文件中添加以下代码:
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.json
的 scripts
文件中添加以下代码:
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,.tsx,.js,jsx --fix"
},
10.2 运行代码格式化
npm run lint:fotmet
运行代码格式化
10.3 处理与 eslint 的冲突
当 ESLint 的规则和 Prettier 的规则相冲突时,就会发现一个尴尬的问题,用其中一种来格式化代码,另一种就会报错。prettier 官方提供了一款工具 eslint-config-prettier 来解决这个问题。本质上这个工具其实就是禁用掉了一些不必要的以及和 Prettier 相冲突的 ESLint 规则。
在 .eslintrc.js
中添加代码处理两者之间的冲突
.eslintrc.js文章来源:https://www.toymoban.com/news/detail-780433.html
// 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',
},
],
}
到此项目已完成。
本人小白,求指点文章来源地址https://www.toymoban.com/news/detail-780433.html
到了这里,关于使用 create-react-app 搭建项目ts+less+antd+redux+router+eslint+prettier+axios的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!