Skip to Content
Nextra 4.0 is released 🎉

项目技术栈概览

  • 基础: 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.tsbaseHandler 工厂)
  • 数据可视化/媒体: ECharts、video.js、fabric.js
  • 地图: 高德地图(@amap/amap-jsapi-loader 动态加载)
  • 样式: Less + Tailwind CSS + PostCSS Autoprefixer
  • 工具库: @boiboif/tree-utils(菜单/树过滤)、ahookslodash-es

路由与权限

  • 多应用拆分: project/app1 ~ project/app5project/entry 各自维护独立的 router/ui/layout/pages/store,共享 componentsserviceutils
  • 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.jswindow.__APP_CONFIG__),增强安全与多环境切换能力。

搭建与开发记录(实践指南)

1. 环境准备

  • Node.js ≥ 16(建议 18+)
  • 包管理器:pnpm(仓库包含 pnpm-lock.yaml

2. 安装依赖

pnpm install

3. 启动与构建

# 开发 pnpm dev # 构建 pnpm build # 预览构建产物 pnpm preview

4. 应用结构

  • 子应用目录:project/app1 ~ project/app5project/entry
  • 常见目录:
    • router/index.tsx: 路由树与 loader
    • ui/layout: 布局(ProLayout
    • store: RootStore / RootContext
    • pages: 页面组件
    • 公共:componentsservicetypesutils

5. 路由与页面开发

  • 新建页面:在对应应用的 pages/xxx/index.tsx
  • 注册路由:在该应用的 router/index.tsx 使用 lazy 动态引入
  • 受控页面:用 RouteAuth 包裹,默认使用 pathname 作为权限点
// app4/router 中的一个受控页面 { path: '/physical/examItem', name: '体测项目分析', element: ( <RouteAuth> <ExamItem /> </RouteAuth> ), }
  • 平台前缀:如应用需要 school::/pathtown::/path 前缀匹配,确保使用对应应用的 RouteAuth 版本。

6. 权限与菜单

  • 后端返回 resource_distinct_paths: string[],形如:platform::/pagePath::operate?
  • 菜单生成:ProLayoutroute.children 来源于对路由树的过滤(用户可访问才展示)
  • 页面守卫:RouteAuth 再校验,未授权返回 403
  • 登录跳转:登录页检测 token,存在则跳转;首进可按可访问路径做默认跳转

7. 网络请求与类型

  • 新增接口:在 service/api.ts 使用 baseHandler 声明,指定类型参数
  • 增加类型:在 types/api.d.ts 维护接口实体定义
  • 页面使用:
import { getUserInfo } from '@service' const { data } = await getUserInfo() // data 自动推导为 API.UserInfoDetail

8. 全局状态(MobX)

  • 存储用户:rootStore.setUserInfo(data)
  • 读取 Store:通过 RootContextuseStore('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_BASEAMAP_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