中后台开发模式:表单 / 表格 / ECharts 三大场景

0 0

前言

做前端架构的人要不要懂中后台?我的回答是 ——不是去抄 Element Plus 表格,而是建立”表单 / 表格 / ECharts 三大场景”的工程模式。原因有三:

  1. 简历里”800+ 页面 ERP 沉淀”是真实经验。中后台 = 重复劳动最多的领域,架构师的职责是抽象共性,减少业务方重复
  2. 表单 / 表格 / ECharts 三大场景占中后台 80%。把它们的工程模式跑通,剩下 20% 的边角问题都好解决。
  3. 可复用 vs 可定制的取舍。中后台组件库不是”写最完美的组件”,而是”覆盖 80% 场景 + 留 20% 逃生口”。

这一篇是『前端架构修仙路』的第 24 篇。我跳过具体组件 API(不讲 Element Plus 怎么用),用表单 / 表格 / ECharts 三大场景的实战模式 + 真实案例,把中后台架构师必会的工程模式讲透。下一篇深度聊微前端 Module Federation。

一、中后台三大场景全景

图 1:中后台三大场景

二、表单方案:React Hook Form vs Formik vs 受控组件

2.1 三大方案对比

方案bundle性能类型推荐度
受控组件0大量字段重渲染差⭐⭐(小表单)
Formik13KB⭐⭐⭐
React Hook Form9KB(uncontrolled)⭐⭐⭐⭐⭐
Vee-Validate15KB⭐⭐⭐⭐(Vue)

2.2 React Hook Form 实战

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2, '至少 2 个字符'),
  email: z.string().email('邮箱格式不对'),
  age: z.number().min(0).max(150),
});

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
    defaultValues: { name: '', email: '', age: 18 },
  });
  
  const onSubmit = (data) => console.log(data);
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      
      <input {...register('email')} />
      {errors.email && <span>{errors.email.message}</span>}
      
      <button type="submit">提交</button>
    </form>
  );
}

优势

  • uncontrolled 模式:用 ref 而非 state,避免每次输入触发 re-render
  • Zod 校验:schema 即文档,TypeScript 类型 + 运行时校验统一
  • 性能:100 个字段的表单 re-render 次数从 100+ 降到 1

生产案例:顺丰 ERP 800+ 表单用 RHF + Zod,首屏渲染 200ms → 80ms(避免 controlled 模式的 100 个 input re-render)。

三、表格方案:TanStack Table vs AG Grid vs vxe-table

3.1 三大方案对比

方案特点适用场景
TanStack TableHeadless、TS 友好、框架无关强烈推荐,自定义 UI 灵活
AG Grid功能最全、Excel 风格企业级、需 Excel 功能
vxe-tableVue 生态、配置式 APIVue 中后台
Element Plus Table简单用、配置项少简单列表

3.2 TanStack Table + 虚拟滚动

import { useReactTable, getCoreRowModel, getSortedRowModel, flexRender } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';

function BigTable({ data, columns }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });
  
  const rows = table.getRowModel().rows;
  const parentRef = useRef();
  
  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40,  // 每行 40px
    overscan: 10,
  });
  
  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => {
          const row = rows[virtualRow.index];
          return flexRender(row.getVisibleCells().map(cell => /* ... */));
        })}
      </div>
    </div>
  );
}

生产案例:FinUI 表格 300 列 × 500 行 + 虚拟滚动 = 50ms 渲染 + 60fps 滚动

四、ECharts 性能优化

4.1 三大常见坑

// ❌ 反模式 1:每次 render 都创建新实例
function BadChart({ data }) {
  const chart = echarts.init(ref.current);  // 内存泄漏 + 性能差
  chart.setOption({...});
}

// ✅ 推荐:useEffect + 销毁清理
function GoodChart({ data }) {
  const ref = useRef();
  useEffect(() => {
    const chart = echarts.init(ref.current);
    chart.setOption({...});
    return () => chart.dispose();  // 卸载清理
  }, []);
  
  // 数据变化时增量更新
  useEffect(() => {
    chart?.setOption({ series: [{ data }] }, { notMerge: false });
  }, [data]);
}
// ❌ 反模式 2:大数据点全量渲染
chart.setOption({
  series: [{ type: 'line', data: 100000 个点 }]  // 100K 点 = 卡 5s
});

// ✅ 推荐:dataZoom + sampling
chart.setOption({
  series: [{ type: 'line', data: 100000 个点, sampling: 'lttb' }],  // 降采样到 1K 点
  dataZoom: [{ type: 'inside' }],  // 用户可缩放查看细节
});
// ❌ 反模式 3:多图实例,每个都创建 + 销毁
function Dashboard() {
  return (
    <div>
      <Chart1 />  // 创建 + 销毁
      <Chart2 />  // 创建 + 销毁
      <Chart3 />  // 创建 + 销毁
    </div>
  );
}

// ✅ 推荐:图表池 + 复用
const chartPool = new EChartsPool(5);  // 最多 5 个实例复用
function Dashboard() {
  return (
    <div>
      <Chart pool={chartPool} />  // 从池里取
    </div>
  );
}

生产案例:某大屏 20 个 ECharts 图,改图表池后从 3s 渲染 → 800ms

五、800+ 页面 ERP 沉淀的工程模式

5.1 路由按业务域分模块

// 不是按"技术"分,是按"业务域"分
/router
  /finance  ← 财务
    /invoice   /report
  /hr       ← 人力
    /employee /attendance
  /crm      ← 客户
    /customer /order

架构师心法业务域 = 团队 = 部署单元。每个业务域独立 owner改动不互相阻塞

5.2 通用查询 Hooks

// 复用 useTable / useForm / usePermission
function useTable<T>(apiUrl, columns) {
  const { data, loading, refresh } = useQuery(apiUrl);
  const [pagination, setPagination] = useState({ page: 1, size: 20 });
  const [filters, setFilters] = useState({});
  
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
  
  return { table, loading, refresh, pagination, setPagination, filters, setFilters };
}

// 用法:业务方只关心 columns
function CustomerList() {
  const { table, loading } = useTable('/api/customer', customerColumns);
  // 不用再写 fetch / loading / pagination
}

生产案例:800+ 页面用 5 个核心 hooks(useTable / useForm / usePermission / useAsync / useToast),业务页面 80% 代码是渲染逻辑重复代码减少 70%

六、踩坑提醒(资深架构师请重点看)

  1. 不要让 controlled 模式处理 100+ 字段的表单每次输入触发 100+ re-render。用 React Hook Form 的 uncontrolled 模式。
  2. 不要让大表格用 Element Plus 简单 Table。300 列 × 500 行 = 5s 渲染 + 5fps 滚动必须用 TanStack Table + 虚拟滚动
  3. 不要让 ECharts 每个组件实例 new 一个大屏场景用图表池复用
  4. 不要按”技术”分模块。按业务域分(finance / hr / crm),业务域 = 团队 = 部署单元
  5. 不要忘记可访问性。中后台常被忽略 WCAG,但政府采购 / 医疗系统必查——表格要有 aria-row / aria-col,表单要有 aria-required / aria-invalid。

总结

这一篇用 6 个关键事实把中后台开发串起来:

  • 三大场景:表单 / 表格 / ECharts 占中后台 80% 代码,架构师必会
  • React Hook Form + Zod:uncontrolled 模式 + schema 校验,100 字段表单 re-render 从 100+ 降到 1。
  • TanStack Table + 虚拟滚动:headless 灵活 + 大表格性能,300 列 × 500 行 = 50ms 渲染。
  • ECharts 三大坑:实例未清理 / 大数据点未采样 / 多图无图表池。
  • 业务域分模块:按 finance / hr / crm 分,业务域 = 团队 = 部署单元
  • 通用 hooks 抽象:useTable / useForm / usePermission,800+ 页面代码减 70%

下一篇:微前端 Module Federation 实战——12min → 3min 构建优化、运行时隔离、跨框架。

5 道重点面试问题方向

Q1(答案):React Hook Form 相比 Formik / 受控组件的核心优势是什么?为什么大型表单必用?

A:核心优势:uncontrolled 模式——用 ref 而非 state,避免每次输入触发 re-render。Formik / 受控组件每次输入触发 re-render,100 字段表单 = 100 个 re-render;React Hook Form只在你调用 watch / formState 时才订阅,re-render 次数从 100 降到 1。Zod 集成:schema 即 TypeScript 类型 + 运行时校验,类型推导自动从 schema 生成大型表单必用:100 字段 + 校验 + 异步提交,controlled 模式首屏 200ms → 80ms,re-render 减 100 倍。生产案例:顺丰 ERP 800+ 表单用 RHF + Zod,表单性能 + 类型安全双赢。

Q2(思考):TanStack Table 和 Element Plus Table 怎么选?大表格用什么方案?

A:TanStack Table 是 headless(无 UI 库),只提供数据 + 状态管理,自己写 UI 灵活度最高,TS 类型推导完整;Element Plus Table 简单用但配置项少,100 列以上 + 单元格编辑 = 撑不住。大表格必用 TanStack Table + 虚拟滚动:useReactTable 管数据 + 状态,useVirtualizer 管可见行渲染,300 列 × 500 行从 5s 渲染 → 50ms生产案例:FinUI 大表格 v1 用 Element Plus 撑不住,v2 改 TanStack Table + react-virtual 提速 16 倍。

Q3(思考):ECharts 在大屏 / 实时刷新场景下的性能瓶颈是什么?怎么优化?

A:三大性能瓶颈:① 实例未清理——echarts.init 不调 dispose() 内存泄漏 + GPU 占用;② 大数据点未采样——10W 点的折线图渲染 5s,sampling: 'lttb' 降采样到 1K 点;③ 多图无图表池——大屏 20 个图每个 new 一个,改图表池复用从 3s → 800ms。实时刷新场景:setOption 增量更新(不要 destroy + init);大数据流用 appendData;高频刷新用 requestAnimationFrame 节流

Q4(思考):800+ 页面的大型 ERP 项目,怎么按业务域拆分模块?路由怎么组织?

A:按业务域分,不是按技术分——/finance(财务) + /hr(人力) + /crm(客户),业务域 = 团队 = 部署单元路由组织:每个业务域独立 router module,主路由用 lazy import 加载,domain 间用 <RouterOutlet> 嵌套。跨 domain 通信:用 Pinia / Zustand 全局 store,避免 event bus(难调试)。共享代码:domain-specific / common / shared 三层,domain 互不依赖。部署策略:微前端 / Module Federation / 简单 monorepo 都行,业务复杂度决定方案

Q5(思考):你在顺丰 ERP 项目里抽象的 5 个核心 hooks(useTable / useForm / usePermission / useAsync / useToast)具体怎么设计的?

A:5 个核心 hooks 设计哲学:80% 业务页面用同样的 5 个 hook。① useTable(apiUrl, columns) — 内部封装 useQuery + useReactTable + 分页 + 筛选 + loading,业务只关心 columns;② useForm(schema, defaultValues) — RHF + Zod,返回 register / formState;③ usePermission()useCan('edit:user') 权限 hook,路由级 + 组件级都可用;④ useAsync(asyncFn, deps)useEffect 封装,自动 cleanup;⑤ useToast() — 全局 toast 队列,错误自动 toast业务页面平均 100 行,数据 + 状态全 hook 化,800+ 页面用同一套。

💡 每道题后面都有 AI 助手按钮,一键拿到详细答案。

参考资料

🔗 原文链接 分享让更多人看到

评论