项目技术栈概览
- 基础: React 18 + TypeScript + Vite 4(开发/构建/预览)
- UI: Ant Design 5 + Ant Design Pro Components(
ProLayout等) - 状态管理: MobX(
RootStore+RootContext) - 路由: React Router v6(
createHashRouter+loader预取用户信息) - 网络请求: Axios 二次封装(
service/handlers.ts+service/request.ts,baseHandler工厂) - 数据可视化/媒体: ECharts、video.js、fabric.js
- 地图: 高德地图(
@amap/amap-jsapi-loader动态加载) - 样式: Less + Tailwind CSS + PostCSS Autoprefixer
- 工具库:
@boiboif/tree-utils(菜单/树过滤)、ahooks、lodash-es等
路由与权限
- 多应用拆分:
project/app1~project/app5与project/entry各自维护独立的router/ui/layout/pages/store,共享components、service、utils。 - Hash 路由: 统一使用
createHashRouter:
// project/entry/App.tsx
import { Suspense } from 'react'
import { createHashRouter, RouterProvider, type RouteObject } from 'react-router-dom'
import { routes } from './router'
const router = createHashRouter(routes as RouteObject[])
export default function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<RouterProvider router={router} />
</Suspense>
)
}- 用户信息加载(loader): 进入路由前先获取用户信息,存入全局
rootStore;失败用统一错误页兜底。
// 任一 appX/router/index.tsx
import { redirect, type RouteObject } from 'react-router-dom'
import { getUserInfo } from '@service'
import { rootStore } from '../store'
import { ErrorElement, NotFound } from '@components'
import Layout from '../ui/layout'
const _getUserInfo = async () => {
if (rootStore.userInfo) return rootStore.userInfo
const { data } = await getUserInfo()
rootStore.setUserInfo(data)
return data
}
export const routes: RouteObject[] = [
{
loader: _getUserInfo,
errorElement: <ErrorElement message="获取用户信息失败,请刷新页面重试!" />,
element: <Layout />,
children: [
// ...子路由
],
},
{
path: '/login',
loader: async () => (localStorage.getItem('token') ? redirect('/') : null),
element: <div>Login</div>,
},
{ path: '*', element: <NotFound /> },
]- 菜单与权限联动: 菜单由路由派生,基于后端返回的
resource_distinct_paths: string[]进行过滤(platform::/pagePath::operate?)。app4等使用平台前缀(如school::/path)过滤。
// 以 app4/ui/layout 为例,按平台前缀过滤菜单
import { useMemo } from 'react'
import { filterTree } from '@boiboif/tree-utils'
import { routes } from '@/app4/router'
const filterRoutes = useMemo(
() =>
filterTree(routes, (item) => {
const isMatches = userInfo.resource_distinct_paths.some((full) => {
const [platform, pagePath] = full.split('::')
const hasCurPlatform = platform === 'school' || platform === '*'
return hasCurPlatform && pagePath === item.path
})
return isMatches || item.showInMenuNoAuth === true
}),
[userInfo.resource_distinct_paths],
)- 路由守卫(组件级): 无论菜单如何,页面组件用
RouteAuth二次校验,未授权渲染 403。
// app4/warpper/routeAuth.tsx(示意)
import { useLocation } from 'react-router-dom'
import { useContext } from 'react'
import { RootContext } from '../store'
export default function RouteAuth({ permission, children, fallback }) {
const { userInfo } = useContext(RootContext)
const location = useLocation()
const allow = userInfo?.resource_distinct_paths?.some((full) => {
const [platform, pagePath] = full.split('::')
const hasCurPlatform = platform === 'school' || platform === '*'
return hasCurPlatform && pagePath === (permission ?? location.pathname)
})
if (allow) return <>{children}</>
return fallback ?? <div>403 无权限</div>
}- 登录/重定向:
- 登录页
loader检查localStorage.token,存在则重定向到默认页。 - 首次进入后,按
resource_distinct_paths中首个可访问页面进行跳转(部分应用内置该逻辑)。
- 登录页
资源加载技术
- 代码分割: 页面级
React.lazy+Suspense,降低首屏包体积。 - 静态资源: 通过 Vite 原生静态资源导入(如
import logo from './logo.png'),产物带 hash,利于缓存。 - 地图 SDK 动态加载: 高德 JS API 运行时按需加载,避免打入主包。
// utils/_AMapLoader.ts(建议从运行时配置读取 key)
import AMapLoader from '@amap/amap-jsapi-loader'
export const _AMapLoader = (param?: Record<string, any>) => {
const key = window.__APP_CONFIG__?.AMAP_KEY // 推荐:运行时注入
return AMapLoader.load({
key,
version: '2.0',
plugins: [],
...(param ?? {}),
})
}接口层模式
- 统一工厂
baseHandler: 使用泛型定义入参/出参,统一方法与错误处理,接口文件按领域聚合。
// service/api.ts(示例)
import { baseHandler } from './handlers'
export const login = baseHandler<ResData<API.UserInfoDetail>, API.LoginParam>('/system/user/login')
export const getUserInfo = baseHandler<ResData<API.UserInfoDetail>, never>('/system/user/detail', 'GET')- 类型声明:
types/api.d.ts定义核心实体、分页结构、字典/资源/用户等类型,前后端契约清晰,自动类型推导友好。
项目两点分析
-
亮点
- 权限闭环完整: loader 取用户 → 菜单过滤 → 路由守卫,支持平台前缀与操作粒度扩展。
- 多应用拆分清晰: 子应用独立开发,复用公共组件与服务层,利于团队并行与领域隔离。
-
风险与改进
- 权限前缀魔法常量: 平台前缀(如
school::、town::)在多处硬编码,建议集中为枚举与工具函数,消除分散风险。 - 地图 Key 硬编码: 建议改为运行时注入(如
public/config.js→window.__APP_CONFIG__),增强安全与多环境切换能力。
- 权限前缀魔法常量: 平台前缀(如
搭建与开发记录(实践指南)
1. 环境准备
- Node.js ≥ 16(建议 18+)
- 包管理器:pnpm(仓库包含
pnpm-lock.yaml)
2. 安装依赖
pnpm install3. 启动与构建
# 开发
pnpm dev
# 构建
pnpm build
# 预览构建产物
pnpm preview4. 应用结构
- 子应用目录:
project/app1~project/app5、project/entry - 常见目录:
router/index.tsx: 路由树与loaderui/layout: 布局(ProLayout)store:RootStore/RootContextpages: 页面组件- 公共:
components、service、types、utils
5. 路由与页面开发
- 新建页面:在对应应用的
pages/xxx/index.tsx - 注册路由:在该应用的
router/index.tsx使用lazy动态引入 - 受控页面:用
RouteAuth包裹,默认使用pathname作为权限点
// app4/router 中的一个受控页面
{
path: '/physical/examItem',
name: '体测项目分析',
element: (
<RouteAuth>
<ExamItem />
</RouteAuth>
),
}- 平台前缀:如应用需要
school::/path或town::/path前缀匹配,确保使用对应应用的RouteAuth版本。
6. 权限与菜单
- 后端返回
resource_distinct_paths: string[],形如:platform::/pagePath::operate? - 菜单生成:
ProLayout的route.children来源于对路由树的过滤(用户可访问才展示) - 页面守卫:
RouteAuth再校验,未授权返回 403 - 登录跳转:登录页检测
token,存在则跳转;首进可按可访问路径做默认跳转
7. 网络请求与类型
- 新增接口:在
service/api.ts使用baseHandler声明,指定类型参数 - 增加类型:在
types/api.d.ts维护接口实体定义 - 页面使用:
import { getUserInfo } from '@service'
const { data } = await getUserInfo()
// data 自动推导为 API.UserInfoDetail8. 全局状态(MobX)
- 存储用户:
rootStore.setUserInfo(data) - 读取 Store:通过
RootContext或useStore('counterStore')获取
9. 资源与样式
- 图片/媒体:
import logo from '@/app4/assets/img/logo.png' - 地图:
_AMapLoader({ plugins: ['AMap.Scale'] }) - 样式:Tailwind 原子类 + Less 组件样式协同使用
10. 常见问题与建议
- 权限不生效:检查
resource_distinct_paths是否包含目标路径;平台前缀是否与当前应用匹配 - 刷新 403:确认
loader成功返回;检查接口域名/CORS/鉴权是否正确 - Key 与环境:将高德 Key、后端网关域名迁移至运行时配置(如
public/config.js注入window.__APP_CONFIG__)
11. 上线与配置
- 构建产物由 Web 服务器托管
- 接口域名/前缀在请求层统一配置(按环境切换)
- 运行时配置(推荐):
public/config.js注入变量 → 启动时读取(例如window.__APP_CONFIG__.API_BASE、AMAP_KEY)
小结
- 技术栈: React + TS + Vite + AntD/ProComponents + MobX + React Router v6。
- 权限: 基于后端的
resource_distinct_paths实现菜单与路由双重校验,支持平台前缀与操作级扩展。 - 资源:
React.lazy分包、Vite 静态资源与地图 SDK 动态加载。 - 建议: 统一权限前缀常量与判断工具;地图 Key/后端域名改为运行时配置,增强安全与可运维性。
最近更新:12/9/2025, 2:17:53 AM