从 Java 视角去理解 TS 和 React
约 1890 字大约 6 分钟
2026-02-04
摘要:很多 Java 后端在接触前端时会困惑“React到底在干什么”“TypeScript(类型化 JavaScript)和 Java 类型到底像不像”。本文以项目中的 DashboardIndex 页面为主线,串起页面结构、状态管理、API(接口)封装与工程化配置,并用 Java 开发者熟悉的概念做对照,帮助你建立可迁移的前端心智模型。
1. 项目结构:用后端分层思维快速定位
把前端目录当成“后端分层”来看,会更容易理解:
src/pages:页面入口,类似 Controller 的入口层src/components:通用组件库,类似公共模块/基础组件src/store:全局状态中心,类似应用级缓存/上下文src/api:请求封装层,类似 Feign/RestTemplatesrc/lib:拦截器/工具集,类似 Filter/AOP
主线页面: src/pages/Dashboard/index.tsx
它是“指挥平台”的首页入口,负责:
- 页面类型设置
- 拉取今日上报数
- 组合多块业务组件
2. 从 DashboardIndex 看 React 的“状态驱动”思想
在 DashboardIndex 中,React 组件是一个函数,它的职责是根据状态输出 UI(界面)。
核心逻辑可以简化为(真实代码略):
useEffect(() => {
dispatch(setPageType(PageType.dashboard));
}, []);
useEffect(() => {
if (currentArea?.code) {
let cancelled = false;
getTodayReportCountApi({region_code: currentArea.code})
.then((res) => {
if (cancelled) return;
const count = Number(res?.data?.[0]?.cnt ?? 0);
dispatch(setTodayReportCount(Number.isNaN(count) ? 0 : count));
})
.catch(() => {
if (cancelled) return;
dispatch(setTodayReportCount(0));
});
return () => {
cancelled = true;
};
}
dispatch(setTodayReportCount(0));
}, [currentArea]);Java 视角对照:
useEffect(副作用钩子)≈ 生命周期回调/监听器- 依赖数组
[currentArea]≈ “当某个状态变化时触发” cancelled保护 ≈ “异步回调前先判断对象是否仍有效”
总结: React 的更新不是“手动刷新 UI”,而是“更新状态,让 UI 自动变化”。
这和后端的“请求驱动”不同,更像“状态驱动渲染”。
3. TypeScript:像 Java,但更灵活
3.1 接口与类型别名
在 src/types/dashboard.ts 里:
export interface ProblemListReturnContext {
statusKey?: ProblemStatusKey;
activeKey?: string;
eventType?: string;
returnId?: string;
}对比 Java:
- Java 的
interface更偏“名义类型” - TypeScript 的
interface/type更偏“结构化类型”
3.2 可选属性与联合类型
在 src/api/common.ts 中常见:
region_code ? : number | string | null;Java 类比: 在 Java 中可能用 Object + 强制转换或 Optional,
TS 直接在类型层表达“可能是多种类型”,但也要求你更明确地处理空值。
3.3 类型断言的边界
common.ts 中大量 as unknown as Promise<...>:
return getStatisticsApi(...) as unknown as Promise<{ ... }>;注意:
这类似 Java 的强制类型转换,但 TypeScript 的断言只在编译期生效,运行期不会帮你校验。
建议只在“API 封装层”使用断言,业务代码尽量消费“已收敛的类型”。
4. Redux Toolkit(状态管理工具):前端也有“状态中心”
src/store/slice/dashboardSlice.ts:
export const dashboardSlice = createSlice({
name: "dashboardSlice",
initialState: {...},
reducers: {
setTodayReportCount: (state, {payload}) => {
state.todayReportCount = payload;
},
bumpEventListRefreshKey: (state) => {
state.eventListRefreshKey += 1;
},
},
});Java 视角理解:
initialState≈ 初始化配置/默认上下文reducers≈ 状态更新方法dispatch(action)≈ 统一入口调用(类似 service 层更新状态)
Redux Toolkit 内部使用 Immer(不可变数据助手),
所以你看到的 state.xxx = 并不是“直接可变”,而是由库帮你生成新状态。
实践建议:
- 页面级共享数据放
store(状态仓库),组件内部临时状态放useState - 不要把所有数据都塞进全局,避免“全局状态膨胀”
5. API 封装:把前端当成“远程服务调用层”
src/api/common.ts 封装了通用表格查询接口:
export function getStatisticsApi(tableId: string, params?: any) {
const apiUrl = `${window.appConfig.backend}/v1.0/tables/${tableId}/data`;
return axios({
url: apiUrl,
method: "GET",
params,
});
}Java 类比:
- 类似
RestTemplate/WebClient的封装 - 通过统一函数生成 URL、封装请求参数
例如 getTodayReportCountApi 在此基础上封装字段:
export function getTodayReportCountApi(params: {
region_code: string | number;
category?: string;
}) {
const requestParams = {
region_code: params.region_code,
...(params.category ? {category: params.category} : {}),
};
return getStatisticsApi(
TODAY_REPORT_COUNT_TABLE_ID,
requestParams
) as unknown as Promise<{
success: boolean;
code: number;
msg: string;
data: TodayReportCountRow[];
}>;
}总结:
前端的 API 层同样需要“统一入口 + 统一返回结构”,否则业务代码会被散乱的请求逻辑污染。
6. Axios(HTTP 请求库)拦截器:前端的“拦截器(Filter/Interceptor)”
src/lib/request.ts 中构建了统一请求实例:
const service = axios.create({
timeout: 300 * 1000,
});请求拦截:
service.interceptors.request.use((requestConfig: any) => {
const appId = "dashboard-app";
requestConfig.headers["appid"] = appId;
const token = User.access_token;
if (token) {
requestConfig.headers.Authorization = `Bearer ${token}`;
}
store.dispatch(startLoading());
return requestConfig;
}, err);响应拦截:
service.interceptors.response.use((response: any) => {
store.dispatch(endLoading());
if (!response.data) {
if (response.status < 200 && response.status >= 300) {
throw new Error("执行异常");
}
return response;
}
...
if (!result.success) {
if (result.msg) {
message.error(result.msg);
throw result.msg;
}
...
}
return result;
}, err);Java 类比:
- 请求前注入 token(令牌)≈ 认证过滤器
- 错误统一提示 ≈ 全局异常处理
- Loading(加载状态)统一管理 ≈ 请求链路统一埋点
这套机制在前端非常关键,它保证了所有请求行为的一致性。
7. 工程化配置:把“运行期配置”还给环境
项目使用 window.appConfig 作为运行期配置来源:
const apiUrl = `${window.appConfig.backend}/v1.0/tables/${tableId}/data`;在拦截器中也会使用 window.appConfig.baseName 处理登录跳转路径。
这相当于后端的 application.yml + 环境变量:
- 配置与代码解耦
- 不把真实地址写死进仓库
额外补充: 本项目在代码中大量使用 @/ 作为路径别名,
这是前端工程化常见实践,便于跨层级引用而不写复杂相对路径。
8. 组件组合与样式组织:页面是“组合体”
DashboardIndex 页面并不复杂,但它是多个组件组合而成:
- 顶部
Header - 中间
Middle地图区域 - 左右侧业务面板
- 各类弹窗组件(如
ReportModal、DutyCenter)
这相当于后端“组合多个服务模块输出一个完整业务页面”。
样式方面,index.module.less 采用 CSS Modules(样式模块化),
避免全局样式污染,类似“每个组件有自己的样式命名空间”。
9. Java 工程师常见的前端坑(基于当前代码经验)
异步请求返回晚于组件卸载
- 解决:像
cancelled这样的保护必不可少
- 解决:像
null/undefined处理不完整- 解决:
res?.data?.[0]?.cnt ?? 0这种写法是必要的
- 解决:
类型过度宽松导致失控
- 解决:限制
any使用范围,仅在 API 封装边界使用
- 解决:限制
依赖数组写错导致副作用重复执行
- 解决:明确
useEffect的依赖来源
- 解决:明确
全局状态过度膨胀
- 解决:共享才进
store,局部状态留在组件内
- 解决:共享才进
10. 从 Java 迁移到 React 的心智路线
你可以把前端理解为一个“持续运行的客户端应用”,
不同点在于:后端以“请求”为中心,前端以“状态”为中心
10.1 Java 与 React/TypeScript 对照表
| Java 后端概念 | React/TypeScript 概念 | 说明 |
|---|---|---|
| 控制器(Controller) | 页面/组件(Page/Component) | 页面入口负责组织 UI 与数据流 |
| 服务层(Service) | Hooks(钩子) + 业务组件 | 业务逻辑更贴近组件与状态 |
| DTO(数据传输对象) | interface / type(类型定义) | 强类型定义数据形状 |
| 过滤器/拦截器(Filter/Interceptor) | Axios 拦截器 | 统一认证、错误处理、日志 |
| 配置文件(application.yml) | window 运行期配置 | 环境配置与代码解耦 |
10.2 DashboardIndex 的数据流小图
下面用 Mermaid(图表语法)快速画出 DashboardIndex 的关键数据流:
flowchart LR
A["currentArea 变化"] --> B["useEffect 请求 getTodayReportCountApi"]
B --> C["dispatch setTodayReportCount"]
C --> D["store 状态更新"]
D --> E["DashboardIndex 重新渲染"]11. 结语
TypeScript 和 React 在前端领域形成了一套和后端工程化相似的组织方式。
当你把“分层、类型、拦截、数据流”这些后端经验迁移到前端时,你会发现两者的核心目标一致:可维护、可演进、可扩展。
