ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

HZERO前端开发实战:基于React与DataSet的企业级应用构建指南

2026/8/13 13:42:19 拓冰建站 浏览量
HZERO前端开发实战:基于React与DataSet的企业级应用构建指南

1. 项目概述:从零到一构建HZERO前端应用

如果你正在接触企业级中后台项目,尤其是基于微服务架构的,那么“HZERO”这个名字大概率已经出现在你的技术雷达上了。它不是一个具体的UI框架,而是一个企业级的微服务应用平台,提供了一整套开箱即用的解决方案,涵盖了从后端服务治理到前端开发范式的方方面面。今天我们不聊后端,聚焦于前端。当我们在说“HZERO前端开发”时,我们实际上在谈论一套基于React技术栈,深度融合了Choerodon UI和DataSet数据管理模型的开发实践。这不仅仅是写几个React组件那么简单,它关乎如何在复杂的企业级业务场景下,高效、规范、可维护地组织你的前端代码。

很多刚接触的开发者会感到困惑:文档看了,Demo也跑了,但一到自己动手从零开始搭一个功能完整的页面,就不知从何下手。路由怎么配?表单数据如何与后端API优雅交互?表格的查询、分页、排序如何与DataSet结合?页面权限如何控制?这些问题单点看都有答案,但串联成一个流畅的开发流程,就需要一个完整的脉络。本文的目的,就是充当这个脉络。我将以一个典型的“用户管理”模块为例,手把手带你走通HZERO前端开发的完整链路,从项目初始化、路由配置、模型定义,到列表页、详情页、创建/编辑页的实现,最后涉及权限与部署。过程中,我会重点分享那些官方文档可能一笔带过,但在实际开发中会让你卡壳的“坑”和“技巧”。

2. 环境搭建与项目初始化:选对起跑线

在开始写业务代码之前,一个稳定且符合规范的项目基础是至关重要的。HZERO前端基于React,官方推荐使用Umi作为开发框架和构建工具。Umi提供了路由、构建、部署等开箱即用的能力,并且其插件体系与HZERO生态完美融合。

2.1 技术栈选型与工具准备

首先明确我们的核心依赖:

  • React 17/18: 组件化开发的基石。
  • Umi 3/4: 企业级前端应用框架。本文以Umi 3为例,因其在HZERO生态中更为成熟稳定。
  • Choerodon UI: 基于Ant Design进行企业级定制的React UI组件库。它不仅是UI组件,更深度集成了DataSet数据管理模型,这是HZERO前端开发的核心。
  • @hzero-front-ui: HZERO官方提供的前端基础组件和工具包,包含了许多平台级的功能封装。

初始化项目,我强烈推荐使用Umi的官方脚手架@umijs/create-umi-app,它能帮你生成一个最符合Umi规范的项目结构。

# 使用 npm npx @umijs/create-umi-app my-hzero-app # 或使用 yarn yarn create @umijs/umi-app my-hzero-app

在创建过程中,选择app模板,并确保包含typescript。创建完成后,进入项目目录安装核心依赖:

cd my-hzero-app npm install antd @choerodon-ui/dataset @choerodon-ui/pro @choerodon-ui/boot hzero-front-ui --save # 同时安装Umi的相关插件 npm install @umijs/plugin-model @umijs/plugin-initial-state --save-dev

注意@choerodon-ui/pro是Choerodon UI的主包,包含了所有组件。@choerodon-ui/dataset@choerodon-ui/bootDataSet模型和其启动配置所必需的。hzero-front-ui的版本需要与你后端的HZERO平台版本大致对应,避免API不兼容。

2.2 核心配置文件详解

项目初始化后,你需要重点关注config/config.ts(或.umirc.ts) 这个Umi的配置文件。这里是连接所有能力的枢纽。

// config/config.ts import { defineConfig } from 'umi'; export default defineConfig({ // 1. 路由配置 routes: [ { path: '/', component: '@/pages/index' }, { path: '/user', component: '@/pages/user' }, ], // 2. 开启Choerodon UI的按需编译与主题定制 theme: { 'primary-color': '#3F51B5', // 定制主色 }, // 3. 配置额外的Babel插件(用于Choerodon UI按需加载) extraBabelPlugins: [ [ 'import', { libraryName: '@choerodon-ui/pro', libraryDirectory: 'es', style: true, }, '@choerodon-ui/pro', ], ], // 4. 代理配置(解决本地开发跨域问题) proxy: { '/hzero': { target: 'http://your-hzero-backend-domain.com', // 你的HZERO后端地址 changeOrigin: true, // pathRewrite: { '^/api': '' }, // 根据后端实际情况决定是否需要重写 }, }, // 5. 开启Umi的model插件(状态管理) model: {}, // 6. 定义全局常量 define: { 'process.env.API_HOST': 'http://your-hzero-backend-domain.com', }, });

这里最关键的几点是代理按需加载。代理配置让你在本地开发时能无缝对接后端API,而按需加载的Babel配置能显著减小最终打包体积。很多同学在引入Choerodon UI后发现打包文件巨大,问题往往就出在这里。

2.3 初始化DataSet全局配置

DataSet是Choerodon UI的灵魂,它是一个前端数据管理模型,将表单字段、表格数据、查询条件、提交动作等统一管理。在使用前,需要在应用入口进行配置。

通常我们在src/app.tsx文件中进行全局配置:

// src/app.tsx import React from 'react'; import { Boot } from '@choerodon-ui/boot'; // 这是一个自定义的Axios实例,用于统一处理请求拦截(如添加Token)、响应拦截和错误处理 const axiosInstance = axios.create({ baseURL: '/hzero', // 与代理配置对应 timeout: 30000, }); // 请求拦截器示例:添加认证Token axiosInstance.interceptors.request.use((config) => { const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // 响应拦截器示例:统一处理错误 axiosInstance.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // 跳转到登录页 window.location.href = '/login'; } // 可以在这里统一处理后端返回的业务错误信息 return Promise.reject(error); } ); // 使用Boot.configure方法全局配置DataSet Boot.configure({ // 配置axios实例 axios: axiosInstance, // 配置反馈组件(如message)的全局属性 feedback: { duration: 3 }, // 配置字段的默认校验信息 field: { defaultValidationMessages: { valueMissing: '请输入{label}', typeMismatch: '请输入正确的{label}', }, }, }); export function rootContainer(container) { return React.createElement(React.StrictMode, null, container); }

这个配置步骤经常被忽略,导致后续使用DataSet发起请求时遇到跨域、认证失败或错误信息无法统一处理的问题。提前做好这步,能为后续开发扫清很多障碍。

3. 路由设计与页面骨架搭建

在单页应用(SPA)中,路由是应用的导航骨架。Umi采用了约定式路由配置式路由相结合的方式,对于中后台项目,配置式路由因其清晰、可集中管理权限而更受青睐。

3.1 配置式路由与布局组件

我们继续修改config/config.ts中的路由部分,构建一个典型的后台管理布局。

// config/config.ts routes: [ { path: '/', component: '@/layouts/BasicLayout', // 全局布局组件 routes: [ { path: '/', redirect: '/dashboard', // 默认重定向到仪表盘 }, { path: '/dashboard', component: '@/pages/Dashboard', name: '仪表盘', // 用于菜单显示 icon: 'DashboardOutlined', }, { path: '/system', name: '系统管理', icon: 'SettingOutlined', routes: [ { path: '/system/user', component: '@/pages/system/User/List', name: '用户管理', hideInMenu: false, // 在菜单中显示 }, { path: '/system/user/:id', component: '@/pages/system/User/Detail', name: '用户详情', hideInMenu: true, // 不在菜单中显示,通过链接进入 }, { path: '/system/user/new', component: '@/pages/system/User/Edit', name: '新建用户', hideInMenu: true, }, ], }, ], }, { path: '/login', component: '@/pages/Login', layout: false, // 登录页不需要全局布局 }, ],

对应的,我们需要创建布局组件src/layouts/BasicLayout/index.tsx

// src/layouts/BasicLayout/index.tsx import React from 'react'; import { ProLayout } from '@choerodon-ui/pro-layout'; // Choerodon UI的布局组件 import { Link, useLocation, useModel } from 'umi'; import { Avatar, Dropdown, Menu } from '@choerodon-ui/pro'; const BasicLayout: React.FC = (props) => { const location = useLocation(); const { initialState } = useModel('@@initialState'); // 假设有全局初始状态模型 const menuHeaderRender = (logo, title) => ( <Link to="/"> {logo} <h1 style={{ marginLeft: '10px' }}>{title || 'HZERO应用'}</h1> </Link> ); const rightContentRender = () => ( <Dropdown overlay={ <Menu> <Menu.Item key="profile">个人中心</Menu.Item> <Menu.Item key="logout">退出登录</Menu.Item> </Menu> } > <Avatar size="small" src={initialState?.currentUser?.avatar} /> </Dropdown> ); return ( <ProLayout title="HZERO Demo" logo="/logo.svg" location={location} menuHeaderRender={menuHeaderRender} menuItemRender={(item, dom) => <Link to={item.path!}>{dom}</Link>} rightContentRender={rightContentRender} // 将Umi路由配置转换为ProLayout需要的menuData route={props.route} > {props.children} </ProLayout> ); }; export default BasicLayout;

ProLayout组件极大地简化了后台布局的开发,它自动处理了菜单、页头、页脚、面包屑等。关键在于menuItemRender属性,它决定了菜单项如何渲染为路由链接。这里的一个常见坑是,如果路由配置中的path是相对路径,需要处理好与Link组件的结合,否则点击菜单可能无法正确跳转。

3.2 动态路由与权限注入

在实际的HZERO平台中,菜单和路由通常不是前端硬编码的,而是根据用户权限从后端动态获取的。这涉及到动态路由。Umi提供了patchRoutes方法,允许你在运行时修改路由配置。

我们可以在src/app.tsx中增加动态路由的逻辑:

// src/app.tsx (续) export async function patchRoutes({ routes }) { // 1. 从后端API获取当前用户有权限的菜单列表 const menuData = await fetch('/hzero/v1/menus/current-user').then(res => res.json()); // 2. 定义一个递归函数,将后端菜单数据转换为Umi路由格式 const convertMenuToRoute = (menus) => { return menus.map(menu => ({ path: menu.path, component: menu.component ? dynamic({ loader: () => import(`@/pages${menu.component}`), // 动态导入组件 }) : undefined, name: menu.name, icon: menu.icon, routes: menu.children ? convertMenuToRoute(menu.children) : undefined, // 可以携带其他元信息,如权限编码 meta: { permission: menu.permission }, })); }; // 3. 找到布局路由(通常是path为‘/’的那条),将动态生成的路由插入其children中 const layoutRoute = routes.find(r => r.path === '/'); if (layoutRoute && layoutRoute.routes) { // 假设我们把动态路由插入到固定路由(如仪表盘)之后 const dynamicRoutes = convertMenuToRoute(menuData); layoutRoute.routes = [...layoutRoute.routes, ...dynamicRoutes]; } }

同时,为了在组件内方便地判断权限,我们可以结合Umi的useAccess钩子(需要先定义access.ts)或自定义的权限Hooks。动态路由是HZERO前端进阶的必由之路,它实现了前端资源与后端权限服务的解耦,让应用更加灵活。

4. 数据模型(DataSet)的核心概念与实战

终于来到了HZERO前端开发最核心、也最具特色的部分——DataSet。你可以把它理解为一个超级加强版的Formik+React Query+ 状态管理库。它管理着数据的生命周期:查询、绑定、校验、提交。

4.1 创建第一个DataSet:用户查询列表

假设我们有一个用户查询接口GET /hzero/v1/users,支持分页、排序和条件查询(如按用户名、状态过滤)。我们为这个列表页创建一个DataSet。

首先,在src/pages/system/User目录下创建一个listDS.ts文件:

// src/pages/system/User/listDS.ts import { DataSet } from '@choerodon-ui/pro'; export default function useListDS() { // 1. 定义查询字段 const queryFields = [ { name: 'username', // 字段名,对应后端查询参数 label: '用户名', type: 'string', maxLength: 30, }, { name: 'enabled', label: '状态', type: 'boolean', // 值列表,用于渲染下拉框或开关 lookupCode: 'HPFM.ENABLED_FLAG', // 通常来自值列表API,这里简化 options: [ { value: true, meaning: '启用' }, { value: false, meaning: '禁用' }, ], }, ]; // 2. 创建DataSet实例 const listDS = new DataSet({ // 主查询配置 queryFields, // 绑定查询表单的字段 autoQuery: false, // 是否在组件挂载后自动查询?建议false,手动控制 paging: 'server', // 服务端分页 pageSize: 10, // 每页大小 // 数据请求配置 transport: { read: { url: '/hzero/v1/users', // 查询API method: 'get', // 请求参数转换器:将DataSet的查询参数(分页、排序、条件)转换为后端需要的格式 params: ({ dataSet, params, data }) => { const { currentPage, pageSize, sortField, sortOrder } = dataSet; const queryParams = { ...params }; // HZERO后端常见的分页参数格式 queryParams.page = currentPage - 1; // 后端页码通常从0开始 queryParams.size = pageSize; if (sortField && sortOrder) { queryParams.sort = `${sortField},${sortOrder}`; } // 将查询表单的值合并进去 Object.assign(queryParams, data); return queryParams; }, }, }, // 数据响应转换器:将后端返回的数据转换为DataSet能识别的格式 dataToJSON: 'normal', // 或 'dirty' 等 // 事件钩子 events: { // 查询成功后的回调 query: ({ dataSet, data }) => { console.log('查询成功,数据:', data); }, // 查询失败后的回调 queryError: ({ error }) => { console.error('查询失败:', error); }, }, }); return listDS; }

这个listDS实例将被注入到我们的列表页面组件中。transport.read.paramsdataToJSON是两个关键配置点。不同后端接口的数据格式约定可能不同,HZERO后端通常遵循一定的RESTful和分页规范,你需要根据实际接口调整这个转换逻辑。我见过很多同学在这里踩坑,前端拼命传参,后端却说没收到,问题就出在参数格式没对上。

4.2 在React组件中使用DataSet

有了DataSet,组件内的逻辑会变得异常清晰。我们创建列表页组件src/pages/system/User/List/index.tsx

// src/pages/system/User/List/index.tsx import React, { useRef } from 'react'; import { observer } from 'mobx-react-lite'; // DataSet基于Mobx,组件需要被observer包裹 import { Table, TextField, Select, Button, Form, DataSet } from '@choerodon-ui/pro'; import useListDS from '../listDS'; // 导入我们定义的DS const UserListPage: React.FC = observer(() => { // 1. 初始化DataSet const listDS = useListDS(); const formRef = useRef<Form>(null); // 2. 查询按钮处理函数 const handleQuery = () => { listDS.query(); // 触发DataSet的查询动作 }; // 3. 重置查询表单 const handleReset = () => { listDS.queryDataSet?.reset(); // 重置查询条件DataSet // 重置后立即查询,显示所有数据 listDS.query(); }; // 4. 新建用户 const handleCreate = () => { // 通过Umi的history跳转到创建页 history.push('/system/user/new'); }; // 5. 表格列定义 const columns = [ { name: 'username', header: '用户名', width: 150, sortable: true, // 支持服务端排序 }, { name: 'realName', header: '真实姓名', width: 150, }, { name: 'email', header: '邮箱', width: 200, }, { name: 'enabled', header: '状态', width: 100, renderer: ({ value }) => (value ? '启用' : '禁用'), }, { header: '操作', width: 150, renderer: ({ record }) => ( <> <Button onClick={() => handleViewDetail(record.get('id'))}>查看</Button> <Button onClick={() => handleEdit(record.get('id'))}>编辑</Button> </> ), }, ]; const handleViewDetail = (id) => { history.push(`/system/user/${id}`); }; const handleEdit = (id) => { history.push(`/system/user/${id}?action=edit`); }; return ( <div style={{ padding: '24px' }}> <h2>用户管理</h2> {/* 查询表单区域 */} <Form dataSet={listDS.queryDataSet} ref={formRef} columns={3} style={{ marginBottom: '16px' }}> <TextField name="username" placeholder="请输入用户名" /> <Select name="enabled" placeholder="请选择状态" clearButton /> <div style={{ textAlign: 'right', gridColumn: '3' }}> <Button onClick={handleReset}>重置</Button> <Button color="primary" onClick={handleQuery} style={{ marginLeft: '8px' }}> 查询 </Button> <Button onClick={handleCreate} style={{ marginLeft: '8px' }}> 新建 </Button> </div> </Form> {/* 数据表格区域 */} <Table dataSet={listDS} columns={columns} queryBar="none" // 我们使用自定义的查询表单,所以隐藏Table自带的 pagination={{ showSizeChanger: true, showQuickJumper: true, pageSizeOptions: ['10', '20', '50'], }} style={{ marginTop: '16px' }} /> </div> ); }); export default UserListPage;

观察这个组件,你会发现状态逻辑几乎都收敛到了DataSet中。查询条件绑定在FormdataSet={listDS.queryDataSet}上,表格数据绑定在TabledataSet={listDS}上。点击“查询”按钮,只是调用了listDS.query(),DataSet会自动收集查询表单的值,组织参数,发起请求,并将返回的数据填充到表格中。分页、排序等操作也只需在Table上配置,DataSet会自动处理。

4.3 创建/编辑页的DataSet设计

列表页的DS主要用于“读”,而创建/编辑页的DS则侧重于“写”。我们创建一个用于表单提交的DataSet,src/pages/system/User/editDS.ts

// src/pages/system/User/editDS.ts import { DataSet } from '@choerodon-ui/pro'; export default function useEditDS(isNew: boolean, initialData?: any) { const editDS = new DataSet({ // 字段定义,包含校验规则 fields: [ { name: 'username', type: 'string', label: '用户名', required: true, maxLength: 30, validator: async (value) => { if (!isNew) return true; // 编辑时不做重复校验 if (value) { // 调用后端接口校验用户名是否已存在 const res = await axios.get(`/hzero/v1/users/check?username=${value}`); return res.data.valid ? true : '用户名已存在'; } return true; }, }, { name: 'realName', type: 'string', label: '真实姓名', required: true }, { name: 'email', type: 'string', label: '邮箱', required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // 简单邮箱正则 }, { name: 'enabled', type: 'boolean', label: '状态', defaultValue: true, // 新建时默认启用 }, ], // 数据提交配置 transport: { // 创建 create: ({ data: [data] }) => ({ url: '/hzero/v1/users', method: 'post', data, // DataSet会自动将变更的数据组织好 }), // 更新 update: ({ data: [data] }) => ({ url: `/hzero/v1/users/${data.id}`, method: 'put', data, }), // 读取(用于编辑时回显数据) read: (props) => ({ url: `/hzero/v1/users/${props.params.id}`, method: 'get', transformResponse: (response) => { // 对后端返回的数据进行预处理,适配DataSet字段 return response; }, }), }, // 事件 events: { submit: ({ dataSet, data }) => { console.log('提交成功', data); message.success(isNew ? '创建成功' : '更新成功'); // 提交成功后返回列表页 history.push('/system/user'); }, submitError: ({ error }) => { console.error('提交失败', error); message.error(error.message || '操作失败'); }, }, }); // 如果是编辑模式,并且传入了初始数据ID,则加载数据 React.useEffect(() => { if (!isNew && initialData?.id) { editDS.query(initialData.id); // 触发read transport } }, [isNew, initialData?.id]); return editDS; }

在编辑页组件中,我们这样使用:

// src/pages/system/User/Edit/index.tsx import React from 'react'; import { observer } from 'mobx-react-lite'; import { Form, TextField, Select, Button } from '@choerodon-ui/pro'; import { useParams, history } from 'umi'; import useEditDS from '../editDS'; const UserEditPage: React.FC = observer(() => { const { id } = useParams<{ id?: string }>(); const isNew = !id; const editDS = useEditDS(isNew, { id }); const handleSubmit = async () => { // 触发表单校验 const valid = await editDS.validate(); if (valid) { // 提交数据,DataSet会根据当前记录状态(新建或更新)自动调用对应的transport editDS.submit(); } else { message.error('请检查表单填写是否正确'); } }; const handleCancel = () => { history.goBack(); }; return ( <div style={{ padding: '24px', maxWidth: '600px' }}> <h2>{isNew ? '新建用户' : '编辑用户'}</h2> <Form dataSet={editDS} labelWidth={100}> <TextField name="username" disabled={!isNew} /> {/* 编辑时用户名不可改 */} <TextField name="realName" /> <TextField name="email" /> <Select name="enabled" /> <div style={{ marginTop: '24px', textAlign: 'center' }}> <Button onClick={handleCancel}>取消</Button> <Button color="primary" onClick={handleSubmit} style={{ marginLeft: '16px' }}> 提交 </Button> </div> </Form> </div> ); }); export default UserEditPage;

这里的关键在于editDS.submit()方法。它会自动判断DataSet中记录的状态(statusaddupdate),然后调用对应的transport.createtransport.update配置。这种设计让创建和编辑页的逻辑高度统一,代码复用率极高。

5. 深入DataSet:高级特性与性能优化

掌握了基础用法后,我们来看看如何用DataSet解决更复杂的问题,并优化性能。

5.1 字段联动与动态校验

业务表单中,字段之间常常存在联动。例如,选择某个国家后,城市下拉框的选项需要动态更新。DataSet通过字段的dynamicProps属性可以优雅地实现。

fields: [ { name: 'country', type: 'string', label: '国家', lookupCode: 'COUNTRY', // 假设有国家值列表 }, { name: 'city', type: 'string', label: '城市', // dynamicProps 是一个函数,返回动态的属性对象 dynamicProps: { // 根据country字段的值,动态决定city字段的lookupCode lookupCode: ({ record }) => { const country = record.get('country'); return country ? `CITY.${country}` : null; // 例如 COUNTRY.US -> CITY.US }, // 也可以动态控制是否必填、是否禁用 required: ({ record }) => record.get('country') === 'CN', // 仅在中国时城市必填 }, }, ]

动态校验也是如此,validator函数可以访问到记录和其他字段的值。

{ name: 'endDate', type: 'date', label: '结束日期', validator: (value, name, record) => { const startDate = record.get('startDate'); if (startDate && value && value < startDate) { return '结束日期不能早于开始日期'; } return true; }, }

5.2 多数据集(DataSets)协同与主子表

在复杂的页面,如一个订单头(主表)带多个订单行(子表),我们需要多个DataSet协同工作。

// 主表DS const headerDS = new DataSet({...}); // 子表DS const lineDS = new DataSet({ parent: headerDS, // 关键:指定父DataSet autoQuery: false, transport: { read: { url: '/hzero/v1/orders/{parent.id}/lines', // 使用父记录ID method: 'get', }, }, events: { // 当主表记录切换时,自动查询对应的子表数据 ['parent.record.change']: ({ dataSet }) => { const parentRecord = dataSet.parent.current; if (parentRecord) { dataSet.query(); // 会自动将 {parent.id} 替换为实际值 } else { dataSet.removeAll(); // 清空子表 } }, }, });

在组件中,将两个DataSet分别绑定到主表单和子表格即可。这种父子关系让主子表数据的同步变得非常简单。

5.3 性能优化:缓存与批量提交

  • 查询缓存:对于不常变动的下拉框数据(如值列表),不要每次打开表单都去查询。可以在应用初始化时一次性加载并缓存到全局状态(如Umi的useModel),或者使用DataSet的cacheSelection属性。
  • 批量提交:子表行项目较多时,逐条提交性能差。可以配置子表DS的transport.update为批量接口,并在submit事件中,通过dataSet.updated获取所有变更的行数据,一次性提交。
  • 虚拟滚动:对于可能包含大量数据的表格,使用Choerodon UI Table的virtual属性开启虚拟滚动,能极大提升渲染性能。
  • 按需加载组件:使用Umi的dynamic函数或React的lazy+Suspense对路由组件进行代码分割,减少首屏加载体积。

6. 部署与上线前的最后检查

开发完成后,我们需要将前端应用构建并部署到服务器。Umi提供了强大的构建命令。

# 生产环境构建 npm run build

构建完成后,会在项目根目录生成dist文件夹,里面就是静态资源(HTML, JS, CSS)。你可以将这些文件放到任何静态文件服务器(如Nginx, Apache)或对象存储(如阿里云OSS)上。

6.1 部署配置要点

在部署前,务必检查config/config.prod.ts(生产环境配置):

// config/config.prod.ts export default { // 1. 修改公共路径(publicPath),如果你的应用不是部署在域名根目录下 publicPath: process.env.PUBLIC_PATH || '/your-sub-path/', // 2. 修改API请求的基础路径,指向生产环境后端 define: { 'process.env.API_HOST': 'https://prod.your-domain.com', }, // 3. 配置资源输出文件名哈希,利于缓存 hash: true, // 4. 可能需要的CDN配置 // chunks: ['vendors', 'umi'], // chainWebpack: function (config, { webpack }) { // config.merge({ // output: { // filename: '[name].[contenthash:8].js', // }, // }); // }, };

6.2 Nginx配置示例

如果你使用Nginx,一个基本的配置如下:

server { listen 80; server_name your-frontend-domain.com; root /path/to/your/dist; index index.html; # 处理前端路由(History模式) location / { try_files $uri $uri/ /index.html; } # 代理API请求到后端 location /hzero/ { proxy_pass https://your-backend-domain.com/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } }

关键点是try_files $uri $uri/ /index.html;这行,它确保了在直接访问前端路由(如/system/user)时,Nginx能正确返回index.html文件,由React Router来处理路由,而不是返回404。

6.3 常见部署问题排查

  • 页面空白,控制台报资源加载404:检查publicPath配置是否正确,以及Nginx的root指令是否指向了正确的dist目录。
  • API请求404或跨域:检查Nginx的proxy_pass地址是否正确,以及后端服务是否允许前端域名的跨域请求(CORS)。生产环境通常通过Nginx反向代理解决跨域,而不是像开发环境那样配置代理。
  • 路由刷新后404:确保Web服务器(如Nginx)配置了对所有非静态文件路径的请求都返回index.html(即上述try_files配置)。
  • 页面加载慢:检查是否开启了Gzip压缩,静态资源是否配置了长期缓存,并考虑使用CDN加速。

从环境搭建到部署上线,我们完整地走通了一个HZERO前端模块的开发流程。这套以DataSet为核心、Choerodon UI为界面、Umi为工程骨架的开发模式,在应对企业级中后台复杂表单、表格和交互时,能显著提升开发效率和代码可维护性。当然,任何框架都有学习曲线,初期可能会觉得配置繁琐,但一旦熟悉了这种“数据驱动”的思维,你会发现很多重复劳动都被框架消化了,你可以更专注于业务逻辑本身。在实际项目中,你还会遇到更复杂的场景,如工作流审批、图表集成、国际化、主题定制等,但万变不离其宗,理解好DataSet这个核心,其他都是在此基础上添砖加瓦。