本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
Antd5最最最吸引我的点
Antd5 官网
又好用,又能美观。
没有Antd5之前,我是选Mui的
比方我要做一个对款式有许多需求的办理后台,或许一些官网主页,我都会选MUI
MUI官网
这个才是我认为最能变美观的组件库,MUI走在了Antd5之前,就搞CSS in JS了,定制主题那叫一个简略,但有一个点便是他不是很好用。。。。
Antd5,几乎让我没理由不选,又好用,又能美观
MUI不怎样好用,组件功用不多,远没有Antd4好用,想用只能自己着手封装,可是能让我纠结MUI和Antd选谁的首要原因,便是MUI真美观,现在Antd5有了这么好用的自定义主题的本领,一下就完全治愈精力内讧,直接Antd5,无脑入就对了。又好用,又能美观。
ok,那么简略的介绍完我选Antd5的理由,接下来,咱们就直奔主题,快速的过一下,运用React18+Antd5+Vite+Ts搭建办理后台的全过程,并在文章结尾处贴出这个项目代码地址,供咱们快速上手实操,体会一下Antd5的痛快。
Antd5调配React18+Vite+Ts开发办理后台全流程
装备vite和tsconfig准备工作
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3120/',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '')
}
}
},
plugins: [react()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
})
装备 “@”,这样我在代码里能够痛快的引进文件了。
装备proxy,我就能够跟后台进行通信,处理开发环境的跨域问题。
tsconfig
装备
{
"include": ["env.d.ts", "src/**/*"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"experimentalDecorators":true,
"composite": true,
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "preserve"
}
}
加入d.ts处理大部分引进文件报错的问题
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: 'development' | 'production' | 'test';
readonly PUBLIC_URL: string;
}
}
declare module '*.avif' {
const src: string;
export default src;
}
declare module '*.bmp' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<
SVGSVGElement
> & { title?: string }>;
const src: string;
export default src;
}
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { readonly [key: string]: string };
export default classes;
}
束缚一下node和npm的最低版别
由于用了vite,咱们需求束缚一下node和npm的版别,由于版别太低,跑不起来的。
开发登陆页
封装字体特效组件,让页面看着浓艳不俗气
import "react";
import { useRef } from "react";
import { useEffect } from "react";
import styles from "./index.module.scss";
let defaultRun: boolean = true;
let infinite: boolean = true;
let frameTime: number = 75;
let endWaitStep = 3;
let prefixString = "";
let runTexts = [""];
let colorTextLength = 5;
let step = 1;
let colors = [
"rgb(110,64,170)",
"rgb(150,61,179)",
"rgb(191,60,175)",
"rgb(228,65,157)",
"rgb(254,75,131)",
"rgb(255,94,99)",
"rgb(255,120,71)",
"rgb(251,150,51)",
"rgb(226,183,47)",
"rgb(198,214,60)",
"rgb(175,240,91)",
"rgb(127,246,88)",
"rgb(82,246,103)",
"rgb(48,239,130)",
"rgb(29,223,163)",
"rgb(26,199,194)",
"rgb(35,171,216)",
"rgb(54,140,225)",
"rgb(76,110,219)",
"rgb(96,84,200)",
];
let inst = {
text: "",
prefix: -(prefixString.length + colorTextLength),
skillI: 0,
skillP: 0,
step: step,
direction: "forward",
delay: endWaitStep,
};
function randomNum(minNum: number, maxNum: number): number {
switch (arguments.length) {
case 1:
return parseInt((Math.random() * minNum + 1).toString(), 10);
case 2:
return parseInt(
(Math.random() * (maxNum - minNum + 1) + minNum).toString(),
10
);
default:
return 0;
}
}
let randomTime: number = randomNum(15, 150);
let destroyed: boolean = false;
let continue2: boolean = false;
let infinite0: boolean = true;
function render(dom: HTMLDivElement, t: string, ut?: string): void {
if (inst.step) {
inst.step--;
} else {
inst.step = step;
if (inst.prefix < prefixString.length) {
inst.prefix >= 0 && (inst.text += prefixString[inst.prefix]);
inst.prefix++;
} else {
switch (inst.direction) {
case "forward":
if (inst.skillP < t.length) {
inst.text += t[inst.skillP];
inst.skillP++;
} else {
if (inst.delay) {
inst.delay--;
} else {
inst.direction = "backward";
inst.delay = endWaitStep;
}
}
break;
case "backward":
if (inst.skillP > 0) {
inst.text = inst.text.slice(0, -1);
inst.skillP--;
} else {
inst.skillI = (inst.skillI + 1) % runTexts.length;
inst.direction = "forward";
}
break;
default:
break;
}
}
}
if (ut != null) {
inst.text = ut.substring(0, inst.skillP);
if (inst.skillP > ut.length) {
inst.skillP = ut.length;
}
}
dom.textContent = inst.text;
let value;
if (inst.prefix < prefixString.length) {
value = Math.min(colorTextLength, colorTextLength + inst.prefix);
} else {
value = Math.min(colorTextLength, t.length - inst.skillP);
}
dom.appendChild(fragment(value));
}
function getNextColor(): string {
return colors[Math.floor(Math.random() * colors.length)];
}
function getNextChar(): string {
return String.fromCharCode(94 * Math.random() + 33);
}
function fragment(value: number): DocumentFragment {
let f = document.createDocumentFragment();
for (let i = 0; value > i; i++) {
let span = document.createElement("span");
span.textContent = getNextChar();
span.style.color = getNextColor();
f.appendChild(span);
}
return f;
}
let flag = false;
export default (props) => {
const { texts } = props;
let container = useRef();
let container2 = useRef();
function init(): void {
setTimeout(() => {
if (destroyed) {
return;
}
container.current && loop();
}, randomTime);
}
function loop(): void {
if (destroyed) {
return;
}
setTimeout(() => {
if (continue2 && container.current != null) {
if (destroyed) {
return;
}
let dom = container.current;
let index = inst.skillI;
let originText = texts[index];
let currentText = runTexts[index];
if (originText != currentText) {
render(dom, currentText, originText);
runTexts[index] = originText;
} else {
render(dom, currentText);
}
}
if (infinite0) {
loop();
} else {
if (inst.skillP < runTexts[0].length) {
loop();
}
}
}, frameTime);
}
useEffect(() => {
{
runTexts = texts;
continue2 = defaultRun;
infinite0 = infinite;
inst.delay = endWaitStep;
if (!infinite0) {
if (runTexts.length > 1) {
console.warn(
"在设置infinite=false的情况下,仅第一个字符串收效,后续字符串不再显现。"
);
}
}
init();
}
}, []);
return (
<div className={styles.content}>
<pre ref={container} className={styles.container} id="container"></pre>
<pre ref={container2}></pre>
</div>
);
};
scss
运用的css Module
技能
.content{
color: black;
height: 100%;
width: 100%;
.container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
white-space: pre-wrap;
word-wrap: break-word;
}
}
登陆卡片,用的Antd5中的Card组件
这没啥可说的,可是作用的确看起来舒服多了
import { Card } from "antd";
import type { ReactNode } from "react";
const { Meta } = Card;
const App: React.FC = (props: { children: ReactNode }) => (
<Card
hoverable
style={{ width: 400 }}
cover={
<img
alt="example"
src="942w_531h_progressive.webp"
/>
}
>
{props.children}
</Card>
);
export default App;
开发主页
首要挑干的说说几点:
- 左侧菜单
- tab标签
- 面包屑
别看就这几个,组合一起,完成各种功用就费力了,接下来详细说说
规划路由结构装备计划
路由数据的结构是树状的,咱们把这个树状结构分离出来,结构数据和内容数据
- 结构数据:描述结构关系,经过id
- 内容数据:依据id去匹配对应的数据内容
结构数据
export const RouteIds = {
hello: "hello",
sys: "sys",
role: "role",
user: "user",
};
export const routesStructData = [
{
id: RouteIds.hello,
},
{
id: RouteIds.sys,
children: [{ id: RouteIds.role }, { id: RouteIds.user }],
},
];
内容数据
import { Login, Center, Page1, Hello, UserPage, RolePage } from "../pages";
export default {
center: {
meta: {
title: "中心",
},
},
hello: {
meta: {
title: "主页",
},
component: Hello,
},
sys: {
meta: {
title: "系统办理",
},
},
user: {
meta: {
title: "用户办理",
},
component: UserPage,
},
role: {
meta: {
title: "人物办理",
},
component: RolePage,
state: { a: 1111 },
},
};
然后二者组成完好路由数据的根本数据,这么做的优点便是能够把重视的事情分开,有些逻辑需求结构,就用结构数据,有些地方需求内容,就经过结构的id进行获取,这样,代码组织的难度更小。
别忘了运用的组件,要用lazy进行懒加载
import { lazy } from "react";
const Center = lazy(() => import("./center"));
const Login = lazy(() => import("./login"));
const Page1 = lazy(() => import("./page1"));
const Hello = lazy(() => import("./hello"));
const UserPage = lazy(() => import("./sys/user"));
const RolePage = lazy(() => import("./sys/role"));
export { Center, Login, Page1, Hello, UserPage, RolePage };
然后规划一个递归算法,生成整个完好的路由结构数据
const processRoute = (children: any[], routesData: any[], prefix: string) => {
routesData.forEach((routeItem, index) => {
const { id } = routeItem;
if (permissions.includes(id)) {
let routeData = routerConfig[id];
// 沿途记载,然后拼接成path
routeData.path = prefix + "/" + id;
routeData.routeId = id;
const { component: Component } = routeData;
if (Component) {
routeData.element = (
<Suspense>
<Component></Component>
</Suspense>
);
}
children!.push(routeData);
if (routeItem.children!?.length > 0) {
routeData.children = [];
processRoute(routeData.children, routeItem.children!, routeData.path);
}
}
});
};
经过算法,咱们尽可能少的装备数据,有些要害数据,完全能够经过这个算法计算出来。
比方路由组件的path,咱们就能够经过分析结构办理,拼接起来
优点是,咱们不必但系调整结构数据,连带的path命名和修正的心智担负。
依据路由数据驱动显现菜单
在组件内部,经过useEffect,呼应路由数据的创立完结
useEffect(() => {
if (routerData.length) {
let result = [];
processRoute(routerData[1].children, result);
setMenuData(result);
}
}, [routerData]);
然后下一步进行,依据路由结构数据,烘托菜单结构
const processRoute = (data, result: any) => {
data.forEach((item) => {
let temp: any = {
key: item.routeId,
icon: createElement(UserOutlined),
label: item.meta.title,
};
result.push(temp);
if (item?.children?.length) {
temp.children = [];
processRoute(item.children, temp.children);
}
});
};
然后将数据经过setMenuData之后,驱动显现菜单
{menuData.length > 0 && (
<Menu
theme="dark"
mode="inline"
selectedKeys={defaultSelectedKeys}
defaultOpenKeys={defaultOpenKeys}
style={{ height: "100%", borderRight: 0 }}
items={menuData}
onClick={({ key }) => {
const path = routerConfig[key]?.path;
if (path) {
navigate(path);
}
}}
/>
)}
Antd5的用法没啥改动,这儿说说用selectedKeys
和openKeys
的原因:
selectedKeys
和defaultOpenKeys
需求让Menu变为可控组件
原因便是,我需求动态呼应路由的改动,就算我直接改写,也能够选中正确的菜单项目,打开正确的折叠项
为啥是defaultOpenKeys,由于不这样,你都点不开折叠,当然你也能够完全设置可控。
那么说到这,咱们需求完成一个监听,react路由改动的功用,你知道怎样规划么?
封装useLocationListen
hook,完成路由改动监听
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default (listener) => {
let location = useLocation();
useEffect(() => {
listener(location);
}, [location]);
};
然后在组件内运用
useLocationListen((location: Location) => {
const { pathname } = location;
let temp = pathname.split("/").filter((item) => {
return item;
});
setDefaultSelectedKeys([temp.at(-1)]);
let temp2 = temp.slice(1, temp.length - 1);
if (temp2.length) {
setDefaultOpenKeys(temp2);
}
// 这个地便利是存储tab标签记载的逻辑
globalStore.addTabHistory(location);
})
然后传入回调函数,就能够完成呼应,然后分析路由,获取Menu组件的打开和选中状况数据,使其完全可控。
也这是由于完成了路由的监听,菜单和下面要介绍的tab标签栏,完美联动,经过监听同一个location对象
记载路由改动,烘托标签栏
每当路由改动,都会经过一个数据存下来,而且还能够夸组件共享,那么就需求mobx这样的状况办理库了,咱们装置一下mobx
yarn add mobx mobx-react
完成一个大局仓库
import { action, makeAutoObservable, toJS } from "mobx";
import type { Location } from "react-router-dom";
// Model the application state.
class Global {
...
permissions: any[] = [];
...
constructor() {
makeAutoObservable(this);
}
init() {
...
this.tabsHistory = {};
...
}
...
addTabHistory = (newItem: Location) => {
let temp = toJS(this.tabsHistory);
temp[newItem.pathname] = newItem;
this.tabsHistory = temp;
};
deleteTabHistory = (pathName: string) => {
let temp = toJS(this.tabsHistory);
if (Object.values(temp).length > 1) {
Reflect.deleteProperty(temp, pathName);
this.tabsHistory = temp;
}
};
...
}
export default new Global();
用法相当的简略,只不过,我完成的对象的特点添加削减的监听,就算我用deep都不好使,所以我用了一个小技巧,便是我先把数据经过tojs转化成一般的数据,让后再修正,最终直接赋值给状况,这样,引用地址改动,就会触发组件的刷线,那么怎样react组件呼应mobx的改写?
react组件需求用到mobx-react供给的hoc,observer
export default observer(() => {
...
useEffect(() => {
let tabsHistory = Object.values(toJS(globalStore.tabsHistory));
setItems(
tabsHistory.map((item) => {
const { pathname } = item;
let routeId = pathname.split("/").at(-1);
const { meta } = routeConfig[routeId];
return { label: meta.title, key: pathname };
})
);
}, [globalStore.tabsHistory]);
...
})
这样,在主页的组件中进行路由大局监听,当路由发生改动就会记载,然后tabs标签组件内部,就会呼应更新,然后烘托数据。
<Tabs
className={styles.content}
type="editable-card"
onChange={onChange}
activeKey={activeKey}
items={items}
hideAdd={true}
onEdit={(e, action) => {
if (action == "remove") {
;
globalStore.deleteTabHistory(e);
}
}}
/>
这便是Antd5中Tabs的运用,可是我需求修正他的默认款式,由于,我仅仅需求tab3供给切换,内部没有什么内容,可是会有多余的margin,我要去覆盖掉,还不能污染大局。
.content {
:global(.ant-tabs-nav) {
margin: initial !important;
}
}
运用css module的:global
来搞就完了。
KeepAlive组件
咱们做的是一个办理后台,常常会有表单填写,不能咱们切换标签了,再回来啥都重置了,那体会可不好,我完成了keepAlive,放置切换重置。
keepAlive的代码,我封装在了R6helper
里。
或许你能够直接在项目完成:
import { useRef, useEffect, useReducer, useMemo, memo } from 'react'
import { useLocation, useOutlet } from 'react-router-dom'
const KeepAlive = (props: any) => {
const outlet = useOutlet()
const { include, keys } = props
const { pathname } = useLocation()
const componentList = useRef(new Map())
const forceUpdate = useReducer((bool: any) => !bool, true)[1] // 强制烘托
const cacheKey = useMemo(
() => pathname + '__' + keys[pathname],
[pathname, keys]
) // eslint-disable-line
const activeKey = useRef<string>('')
useEffect(() => {
componentList.current.forEach(function (value, key) {
const _key = key.split('__')[0]
if (!include.includes(_key) || _key === pathname) {
this.delete(key)
}
}, componentList.current)
activeKey.current = cacheKey
if (!componentList.current.has(activeKey.current)) {
componentList.current.set(activeKey.current, outlet)
}
forceUpdate()
}, [cacheKey, include]) // eslint-disable-line
return (
<div>
{Array.from(componentList.current).map(([key, component]) => (
<div key={key}>
{key === activeKey.current ? (
<div>{component}</div>
) : (
<div style={{ display: 'none' }}>{component}</div>
)}
</div>
))}
</div>
)
}
export default memo(KeepAlive)
然后替换Outlet标签,去掉<Outlet/>
<KeepAlive
include={["/center/sys/user", "/center/sys/role"]}
keys={[]}
></KeepAlive>
封装主题定制Hoc,答应我别再重复定制主题了,封装一下好么。
Antd定制主题真的不要太便利,方法便是经过Antd供给的ConfigProvider
装备theme就行了
import React from 'react';
import { ConfigProvider, Button } from 'antd';
const App: React.FC = () => (
<ConfigProvider
theme={{
token: {
colorPrimary: '#00b96b',
},
}}
>
<Button />
</ConfigProvider>
);
export default App;
类似这样,可是你不能定制一个主题,就写这么一长串吧,那也太不优雅了,这你不赶紧封装一个Hoc
import "react";
import { ConfigProvider, Button } from "antd";
export default (Comp, theme) => {
return (props) => {
return (
<ConfigProvider theme={theme}>
<Comp {...props} />
</ConfigProvider>
);
};
};
然后包装一下组件,然后传入theme数据,大大便利了定制过程。
export default themeProviderHoc(center, {
components: {
Menu: {
colorPrimary: "blue",
},
},
});
Antd5主题,考究了一个token,我不管它为啥这么叫,总之便是能设置款式,而且antd5还供给了,主题定制网页,
导出装备
复制过来就行了,
可是需求提示一下,便是这个装备文件是经过localStorage存储的,有记载,想整理的话,手动整理一下。
权限规划,的确有点难
我目前对权限的认知,便是希望希望经过装备一个权限数据,然后传给后台,后台依据当前用户进行记载,当这个用户登陆的时分,重新获取权限然后依据权限去操控的显现和操作。
我觉得目前了解够用了,究竟一个简略的上手模板项目,并不需求那么复杂的权限规划。
可是我遇到了一个问题,权限操控一个大工作,便是前端的路由的鉴权,这块在React上我花了不少的功夫。
React router你就变吧,咋变你也不好用
我用的React router 6.4.3,咋感觉变了呢~所以我查了一下,加了不少东西啊
我个人观点,我就觉得react的路由不是很好用,比较Vue router体会上我觉得差许多,技能上我没才能说谁规划好,可是我就从运用下来的体会,文档的清晰度,还是Vue router更好的,可能有许多原因是vue文档是中文的,哈哈哈哈,恶作剧的,我觉得我在完成权限操控时分,让我很别扭,比较vue,直接就有api addRoute,react我找半响发现,压根就没有这玩意,233333。
React router 6.4.3
我之前创立路由是经过在最顶层创立一个Router
,然后在内部我再经过useRoutes
创立routes
和一系列route
的,成果这次这个api,把这三个都整合了,变成了一个。
import React from "react";
import { createRoot } from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
Route,
Link,
} from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: (
<div>
<h1>Hello World</h1>
<Link to="about">About Us</Link>
</div>
),
},
{
path: "about",
element: <div>About</div>,
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
经过RouterProvider
供给一个provider
然后再经过createBrowserRouter
创立一个router
。
可是这个RouterProvider
有点意思啊,不能够传children。。。。。,好吧,你赢了。
是简练了许多,我用起来还挺舒服,可是当我想动态设置路由的时分,我就发现,逻辑写起来怎样那么乱呢。
我要动态设置路由
为什么这么规划,首要觉得权限应该操控路由的创立,而不是,路由创立出来,你经过鉴权的方式去防止跳转啥的。
我觉得最好的方式,便是路由依据权限来判别是否存在。
压根就没这个路由权限,就直连续这个路由都不烘托,这不更完全,所以我需求动态创立路由,可是createBrowserRouter
集成在一起了,我需求分开,这样我的逻辑能更清晰。
所以我用了原滋原味的路由装备。
<>
{routerData && (
<Routes>
<Route path="/" element={<Login></Login>}></Route>
<Route
path="/center"
element={<Center></Center>}
children={routerData?.[1]?.children?.map((item) => {
return toRenderRoute(item);
})}
></Route>
</Routes>
)}
</>
有没有中梦回React router v4的感觉,哈哈哈,相同能完成功用,这如同React router说到做到了,他一直发起不给你全部,只给你元功用,然后让你拼装,现在看来,的确有点悟了。
然后在cneter这个首要的路由上,动态装备其内部的子组件,这样就完成的React的动态路由
const toRenderRoute = (item) => {
const { children } = item;
let arr = [];
if (children) {
arr = children.map((item) => {
return toRenderRoute(item);
});
}
return (
<Route
children={arr}
key={item.path}
path={item.path}
element={item.element}
></Route>
);
};
上面这段便是递归创立路由的算法逻辑,这样就完成了路由的动态装备了。
当然我这种合作权限,动态设置路由的方式仅仅我的一种计划,仅供参考,23333。
完成monorepo,简化发动流程
项目目前是前端+后端的项目,后端运用的koa完成的简易版nodejs服务,这两个放在了一个repo里发动的话,需求顺次装置依靠而且发动,这样操作稍微不便利,那么咱们简略的经过yarn workspace完成一下monorepo,然后仅仅一条指令就能完成发动。
先在package.json中装备workspace
然后在项目根目录下创立packages文件,然后把前后端两个项目移动这儿。
然后履行履行:
// 第一步:装置依靠
yarn
// 第二步:发动
yarn start
经过这样的小技巧,就能快速的发动项目了,简化了开发流程。
结尾
这篇先讲这么多,下一篇,咱们详细聊聊如何规划权限办理,以及nodejs开发服务的逻辑。
项目地址 github.com/DLand-Team/…
至此,一个对新手友爱的办理后台项目就构建好了,而且还在不断完善中,未来会补全Java后端服务项目,敬请期待,有问题能够随时咨询我,或许留言,我整了个群叫闲D岛,群号551406017,结识一帮情投意合的小伙伴,交流技能,欢迎水群(我就会玩qq,整其他,我也不会,比方公众号啥的。。。哈哈哈哈)