腿夹腿,带你用 react 撸后台,系列二(项目骨架篇)

77 天前
 zhangfg

Github 地址 | 文档 | 在线预览 | 拓展 pro 版在线预览

react-antd-console 是一个后台管理系统的前端解决方案,封装了后台管理系统必要功能(如登录、鉴权、菜单、面包屑等),帮助开发人员专注于业务快速开发。项目基于 React 18Ant design 5ViteTypeScript 等新版本。对于使用到的各项技术,会被持续更新至最新版本。可放心用于生产环境。

为了方便大家更好的掌握和使用本项目,推出系列文章:

如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star

1. 概述

上篇我们搞定了构建工具,接下来,我们继续搭建项目骨架。值得一提的是,项目骨架搭建好后,会是一个通用的 react 开发模板,也可以用来开发其他 react 项目

2. 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 />
  );

参考文档:

3. react-router 初始化

3.1 准备工作

在初始化 react-router 之前,我们先来考虑一些事情:

3.1.1 一套配置生成多种数据

众所周知,一个路由对应一个页面。 而侧边菜单,往往也是一个路由对应一个菜单项(除了一些特定的页面例如登录页/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;

3.1.2 组件外跳转

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>
  );
}

我们把 historyHistoryRouter 都导出在了 react-router-toolset 中

// router/index.ts
export * from 'react-router-toolset'; // 包括 history 和 HistoryRouter
// 组件外跳转示例
import { history } from '@/router';
history.push('/login');

3.2 初始化

准备工作做好后,我们可以很简洁地初始化 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;
}

3.3 关于 react-router-toolset

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;
}

4. 继续完善

4.1 动态浏览器标题

每个页面都有自己的标题,我们希望在浏览器标题上动态展示页面对应的标题,可以使用 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>
  );
}

4.2 antd

antd 甚至不用初始化,安装好直接用即可。

npm i -S antd

打包的时候还能自动做 tree shaking (说是这么说,但是 antd 打出来的包依然很大。如果你嫌大,可以把 antd 做 cdn 引入。可以参考 vite-plugin-cdn-importVite 配置 build.rollupOptions.external 去解决,在此不做展开)

值得一提的是,antd 功能覆盖的场景还是非常多的,很可能有一些很有用的功能,由于我们查看文档不够仔细而被忽略掉

antd 文档还是比较详细的,平时开发会经常用到,随时查阅,在此不做赘述

4.3 请求

可参考 react-antd-console 文档

4.4 Mock

可参考 react-antd-console 文档

4.5 国际化

可参考 react-antd-console 文档

5. 目录结构

完成上述工作后,我们最后再确定下目录结构

./src
├── assets      # 公共静态资源
├── components  # 公共组件
├── consts      # 公共常量
├── hooks       # 公共 hooks
├── http        # http
├── layouts     # 通用布局
├── locales     # 多语言配置
├── mock        # mock
├── models      # 公共数据管理
├── pages       # 页面组件
├── router      # 路由配置
├── services    # 接口配置
├── styles      # 公共样式
├── utils       # 公共工具
├── App.tsx     # 根组件
└── main.tsx    # 入口组件

下面从:

三种目录概述

5.1 固定目录

5.2 配置目录

5.3 公共目录

以下目录,定义为通用目录,存放的都是可以复用的代码

逻辑相关性强的文件之间,应当尽可能的靠近,最好放在同一目录下。而公共目录,应当只包含:全局可复用的代码

特别需要注意的是,除非由特殊原因,我们不认为每个页面组件都需要对应一个model数据,因为这么做会增加结构复杂度和维护难度。model中的数据,应该是一些全局可复用的数据

至此,项目骨架搭建完毕。

6. 系列文章

如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star

Github 地址 | 文档 | 在线预览 | 拓展 pro 版在线预览

1619 次点击
所在节点    分享创造
5 条回复
zhangfg
77 天前
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.
xyovo999
77 天前
做的不错
zhangfg
77 天前
@xyovo999 谢谢你😊
xhawk
75 天前
@zhangfg 我觉得做得挺好的. 如果考虑去掉 antd, 然后添加 shadcn 的话, 可以一起探讨探讨搞一个.
zhangfg
74 天前
@xhawk 很好的想法。我对这个库还不了解,但我去看了一下,看上去像是 react 版的 html 标签,有很多原子组件,样式可定制。如果要实现我们需要的侧边菜单、面包屑等功能,还需要进一步封装组合这些原子组件,才能输出有像 antd 一样效果且有用法简单的组件。看起来它像是一种可以创造类似 antd 的 ui 库的工具,但直接用来做业务开发,没有 antd 方便快捷。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1084331

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX