Github 地址 | 文档 | 在线预览 | 拓展 pro 版在线预览
react-antd-console 是一个后台管理系统的前端解决方案,封装了后台管理系统必要功能(如登录、鉴权、菜单、面包屑等),帮助开发人员专注于业务快速开发。项目基于 React 18
、Ant design 5
、Vite
和 TypeScript
等新版本。对于使用到的各项技术,会被持续更新至最新版本。可放心用于生产环境。
为了方便大家更好的掌握和使用本项目,推出系列文章:
如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star
上篇我们搞定了构建工具,接下来,我们继续搭建项目骨架。值得一提的是,项目骨架搭建好后,会是一个通用的 react 开发模板,也可以用来开发其他 react 项目
上篇提到 <root>/index.html
引入了 src/main.tsx
,接下来我们在 <root>/index.html
中添加一个 div
元素作为 react 根组件的容器
<!-- <root>/index.html -->
<html>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
然后在 src/main.tsx
中初始化 react
// src/main.tsx
import { createRoot } from 'react-dom/client';
import App from './App';
// createRoot lets you create a root to display React components inside a browser DOM node.
const root = createRoot(document.getElementById('root')!);
root.render(
<App />
);
参考文档:
在初始化 react-router 之前,我们先来考虑一些事情:
众所周知,一个路由对应一个页面。 而侧边菜单,往往也是一个路由对应一个菜单项(除了一些特定的页面例如登录页/403 页等,和从某列表点开来的详情/编辑页等)。 而面包屑组件,也是根据菜单数据生成。 因此,我们需要只用一套数据,就可以自动生成 react 路由、菜单、面包屑等数据。
故作如下设计:
数据 | 类型 | 来源 | 作用 |
---|---|---|---|
routesConfig | RouteConfig[] |
手动配置 | 配置路由 |
reactRoutes | RouteObject[] |
自动生成 | 用于生成 react-router 路由 |
routes | RouteConfig[] |
自动生成 | 树型结构,用于生成 菜单、面包屑 等数据 |
flattenRoutes | RouteConfig[] |
自动生成 | routes 的展平数据结构,方便查询路由 |
interface RouteConfig {
/** 配置数据: 路径,同 react-router */
path: string;
/** 生成数据: 对应 react-router 的 pathname */
pathname?: string;
/**
* 生成数据: ['', '/layout', '/layout/layout-children1',
* '/layout/layout-children1/permission']
*/
collecttedPathname?: string[];
/** 生成数据: ['', 'layout', 'layout-children1', 'permission'] */
collecttedPath?: string[];
/** 配置数据: 组件的文件地址 */
component?: () => Promise<any>;
/** 配置数据: 隐藏在菜单 */
hidden?: boolean;
/** 配置数据: 菜单名称 */
name?: string;
/** 配置数据: 菜单 icon */
icon?: React.ReactNode;
/** 配置数据: 页面标题,不传则用 name */
helmet?: string;
/** 配置数据: 菜单权限 */
permission?: string;
/** 配置数据: 重定向 path */
redirect?: string;
/** 配置数据: 将子路由的菜单层级提升到本级 */
flatten?: boolean;
/** 配置数据: 子路由,同 react-router */
children?: RouteConfig[];
/** 配置数据: 同 react-router */
caseSensitive?: boolean;
/** 配置数据: 是否是外链 */
external?: boolean;
/** 生成数据: 父路由 */
parent?: RouteConfig;
};
为此,我们封装了 Router 类,导出在了 react-router-toolset 中。 react-router-toolset 还导出了 useRouter 方法,用于在 react 组件中获取最新的上述数据。
用法:
// router/index.ts
import { Router } from 'react-router-toolset';
import { routesConfig } from './config';
/**
* 路由配置在 `src/router/config/index.tsx` 文件的 `routesConfig` 中
* routesConfig 要满足 RouteConfig 类型约束
* routesConfig 配置参考:
* https://github.com/diandian18/react-antd-console/blob/master/src/router/config/index.tsx
*/
const router = new Router(routesConfig);
// router 包含了 reactRoutes/routes/flattenRoutes 等数据
export default router;
react-router 只提供了组件内跳转路由的 api useNavigate。我们还希望在组件外可以跳转。而 history 是专门做这个事情的 另一方面,react-router 没有提供 history 模式的 Router 组件,我们需要封装一个
// history.ts
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
// HistoryRouter.tsx
import { useState, useLayoutEffect } from 'react';
import { Router } from 'react-router-dom';
import type { BrowserHistory } from 'history';
export interface HistoryRouterProps {
history: BrowserHistory
basename?: string
children?: React.ReactNode
}
export default function HistoryRouter({
basename,
children,
history,
}: HistoryRouterProps) {
const [state, setState] = useState({
action: history.action,
location: history.location,
});
useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
location={state.location}
navigationType={state.action}
navigator={history}
>
{ children }
</Router>
);
}
我们把 history
和 HistoryRouter
都导出在了 react-router-toolset 中
// router/index.ts
export * from 'react-router-toolset'; // 包括 history 和 HistoryRouter
// 组件外跳转示例
import { history } from '@/router';
history.push('/login');
准备工作做好后,我们可以很简洁地初始化 react-router
// src/main.tsx
import { history, HistoryRouter } from '@/router';
root.render(
<HistoryRouter history={history}>
<App />
</HistoryRouter>,
);
// src/App.tsx
import { useRoutes } from 'react-router-dom';
import router from '@/router';
function App() {
const element = useRoutes(router.reactRoutes);
return element;
}
react-router-toolset 作为一个通用的 react-router 工具集,发布了 npm 包,源码地址
下面额外介绍一些其他有用的方法:
/**
* 设置路由项。setItem
* @param pathname 指定路由
* @param cb 参数为 pathname 对应的路由
*/
Router.setItem: (pathname: string | ((routesConfigItem: RouteConfig) => void), cb?: (routesConfigItem: RouteConfig) => void) => void
/**
* 设置 pathname 的兄弟路由
* @param pathname 指定路由
* @param cb 参数 routesConfigs 为 pathname 的兄弟路由
* @param cb 参数 parentRoute 为 pathname 的父路由
*/
Router.setSiblings(pathname: string | ((routesConfig: RouteConfig[], parentRoute: RouteConfig) => void), cb?: (routesConfig: RouteConfig[], parentRoute: RouteConfig) => void): void
/**
* 根据 pathname 获取 router 的 path
* router 的 path 里可能有:id
* @example '/123/home' -> '/:id/home'
*/
Router.getRoutePath(pathname: string): string
/**
* 根据当前路由 params
* 替换掉目标 routePath 中的动态路由参数如":id"
* @example '/:id/home' -> '/123/home'
*/
(method) Router.getPathname(routePath: string): string
/**
* 在 react 组件中获取最新的 reactRoutes/routes/flattenRoutes/curRoute
*/
function useRouter(router: Router): {
reactRoutes: RouteObject[];
routes: RouteConfig[];
flattenRoutes: Map<string, RouteConfig>;
curRoute: RouteConfig | undefined;
}
每个页面都有自己的标题,我们希望在浏览器标题上动态展示页面对应的标题,可以使用 react-helmet-async 实现
// src/App.tsx
import { Helmet, HelmetProvider } from 'react-helmet-async';
import router, { useRouter } from '@/router';
function App() {
// useRouter 除了返回路由相关数据,还返回了当前路由数据
const { curRoute } = useRouter(router);
const element = useRoutes(router.reactRoutes);
const { t: t_menu } = useTranslation('menu');
return (
<HelmetProvider>
<Helmet>
<title>{curRoute?.name ? `${t_menu(curRoute.name)} - ${DEFAULT_TITLE}` : DEFAULT_TITLE}</title>
</Helmet>
{ element }
</HelmetProvider>
);
}
antd 甚至不用初始化,安装好直接用即可。
npm i -S antd
打包的时候还能自动做 tree shaking (说是这么说,但是 antd 打出来的包依然很大。如果你嫌大,可以把 antd 做 cdn 引入。可以参考 vite-plugin-cdn-import 和 Vite 配置 build.rollupOptions.external 去解决,在此不做展开)
值得一提的是,antd 功能覆盖的场景还是非常多的,很可能有一些很有用的功能,由于我们查看文档不够仔细而被忽略掉
antd 文档还是比较详细的,平时开发会经常用到,随时查阅,在此不做赘述
完成上述工作后,我们最后再确定下目录结构
./src
├── assets # 公共静态资源
├── components # 公共组件
├── consts # 公共常量
├── hooks # 公共 hooks
├── http # http
├── layouts # 通用布局
├── locales # 多语言配置
├── mock # mock
├── models # 公共数据管理
├── pages # 页面组件
├── router # 路由配置
├── services # 接口配置
├── styles # 公共样式
├── utils # 公共工具
├── App.tsx # 根组件
└── main.tsx # 入口组件
下面从:
三种目录概述
src/lyaouts
: 通用布局。以后介绍src/http
: 封装了 axios
,配置了请求和响应拦截,导出了 axios
实例src/mock
: 封装了 msw
,若要添加 mock 文件,请在 src/mock/browser.ts
中添加src/router
: 包含路由配置和导出了一些重要的路由方法src/pages
: 业务页面src/locales
: 多语言配置src/services
: 接口配置以下目录,定义为通用目录,存放的都是可以复用的代码
src/assets
: 公共静态资源src/components
: 公共组件src/consts
: 公共常量src/hooks
: 公共 hookssrc/models
: 公共数据管理。以后介绍src/styles
: 公共样式src/utils
: 公共工具逻辑相关性强的文件之间,应当尽可能的靠近,最好放在同一目录下。而公共目录,应当只包含:全局或可复用的代码
特别需要注意的是,除非由特殊原因,我们不认为每个页面组件都需要对应一个model数据,因为这么做会增加结构复杂度和维护难度。model中的数据,应该是一些全局或可复用的数据
至此,项目骨架搭建完毕。
如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star
Github 地址 | 文档 | 在线预览 | 拓展 pro 版在线预览
1
zhangfg OP v.1.1.0 Latest
New features: - Dynamic route: Change route in any way. - Dynamic meta: Change meta info in route. - External link: Click external link will open a new brower tab and arrive the link. - Separation layout: A empty layout. - Single sider layout: Menu has no children. Fixed - router.setSiblings has no effect on non-dynamic parameter prefix routes. |
2
xyovo999 34 天前
|