前后端命名冲突?驼峰与下划线的统一方案(附可直接复用代码)

0 阅读6分钟

最近和后端又干仗了。

前端这边的规范标准是变量命名必须用小驼峰结构,毕竟TS/JS约定俗称的就是这个结构。

但是后端写的接口,数据库的字段基本上全都是用的下划线。

一边是小驼峰,一边是下划线,他让我改我不改,我让他改他也拧着头不改。到底该怎么统一?

为什么前后端不能统一

很多时候我们回想能不能手动的把 user_name 改成 userName,这种做法,看似简单,实则埋了很多坑。

首先,前端 TS/JS 生态有明确约定:

变量、函数、接口、状态(state)必须用小驼峰,混用下划线会被 ESLint 报错。

也会让团队代码风格混乱,别人接手你的项目时,要反复适应两种命名,效率极低。

其次,后端和数据库用下划线,也是有原因的:

比如数据库字段不区分大小写,用下划线分隔更清晰。

后端语言(Java、Python、Go)的规范,也默认推荐下划线命名。

最关键的是:手动修改字段名,容易出现拼写错误。

比如把 create_time 写成 createTime 还是 creatTime

一旦出错,排查起来相当麻烦。

而且如果接口字段有变动,每个用到该字段的地方都要手动修改,堪称"维护灾难"。

前端接口层自动转换

接口层自动转换是目前大厂最常用、最标准的方案。

核心实现是在前端请求/响应拦截器中,自动完成"前端小驼峰 ↔ 后端下划线"的转换,全程无感,不用手动改任何字段。

1. 准备工具库

一般都是用 lodash 库(几乎所有前端项目都会引入,轻量且稳定),它提供了现成的驼峰、下划线转换方法,不用自己写复杂逻辑。

# 安装
npm install lodash lodash-es
# 或
yarn add lodash lodash-es

2. 转换工具函数

创建一个工具文件(比如 utils/format.ts),编写两个核心函数:

一个把后端的下划线转成前端的小驼峰(请求拦截器部分),一个把前端的小驼峰转成后端的下划线(相应拦截器部分)。

import { camelCase, snakeCase } from 'lodash';
import { mapKeys, isPlainObject } from 'lodash-es';

/**
 * 递归将对象的key从下划线转成小驼峰(处理后端返回的所有数据)
 * @param obj 后端返回的JSON数据(对象或数组)
 * @returns 转成小驼峰后的对象/数组
 */
export const toCamelCase = (obj: any): any => {
  // 如果不是对象(比如字符串、数字),直接返回,不用转换
  if (!isPlainObject(obj)) return obj;
  // 递归遍历对象,把每个key转成小驼峰
  return mapKeys(obj, (_, key) => camelCase(key));
};

/**
 * 递归将对象的key从小驼峰转成下划线(给后端发请求时使用)
 * @param obj 前端要发送的参数(对象)
 * @returns 转成下划线后的对象
 */
export const toSnakeCase = (obj: any): any => {
  if (!isPlainObject(obj)) return obj;
  return mapKeys(obj, (_, key) => snakeCase(key));
};

3. 配置 Axios 拦截器

import axios from 'axios';
// 引入上面写的转换函数
import { toCamelCase, toSnakeCase } from '@/utils/format';

// 创建Axios实例(正常项目都会这么做)
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 你的接口基础地址
  timeout: 5000
});

// 1. 请求拦截器:前端发送请求时,把小驼峰参数转成下划线
request.interceptors.request.use(config => {
  // 如果有请求体(post/put请求),转换参数
  if (config.data) {
    config.data = toSnakeCase(config.data);
  }
  // 如果有URL参数(get请求),转换参数
  if (config.params) {
    config.params = toSnakeCase(config.params);
  }
  return config;
});

// 2. 响应拦截器:后端返回数据时,把下划线转成小驼峰
request.interceptors.response.use(res => {
  // 转换后端返回的所有数据
  if (res.data) {
    res.data = toCamelCase(res.data);
  }
  return res;
});

export default request;

海量数据/高频接口会有性能问题吗?

其实这个问题基本上完全不用担心,毕竟这是久经验证过的方案了。

用上面的 lodash 转换函数,在现代浏览器 Node.js 中,转换速度快到可以忽略不计:

  • 100 条数据:0.0x 毫秒(几乎感知不到)

  • 1000 条数据:0.1~0.3 毫秒

  • 10000 条数据:1~3 毫秒

  • 100000 条数据:10~30 毫秒

一次正常的网络请求,耗时大概是 200~1000 毫秒,转换耗时连网络请求的零头都不到。

所以,性能瓶颈永远在网络请求、DOM 渲染上,绝对不在字段名转换上。

有可能存在性能压力的场景(极其罕见)

只有同时满足以下 3 个条件,才会出现可感知的性能损耗:

  1. 单次接口返回大于 10000 条复杂嵌套数据(比如 5 层以上嵌套)

  2. 接口每秒调用几百上千次(高频场景,比如首页实时刷新)

  3. 没有做分页、虚拟滚动、缓存(正常项目都会做)

就算遇到这种极端场景,也有优化方案。

性能压力场景优化方案

1. 非递归转换(速度提升 5~10 倍)

绝大多数接口返回的是一层对象,不需要递归转换,只转第一层,性能会大幅提升。

前提是确定接口返回的情况。

// 非递归版本,只转第一层,性能更好(推荐优先使用)
export const toCamelCaseFlat = (obj: any): any => {
  if (!obj || typeof obj !== 'object') return obj;
  // 只遍历第一层key,不递归,速度极快
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) => [camelCase(key), value])
  );
};

// 响应拦截器中替换成这个函数即可
request.interceptors.response.use(res => {
  if (res.data) {
    res.data = toCamelCaseFlat(res.data); // 替换成非递归版本
  }
  return res;
});

2. 换更快的专门库(比 lodash 快 2~5 倍)

如果觉得 lodash 不够快,可以用专门做字段转换的库:camelcase-keys / snakecase-keys,专门为高性能设计,用法和 lodash 类似。

// 安装
npm install camelcase-keys snakecase-keys

// 使用(更简洁,性能更好)
import camelcaseKeys from 'camelcase-keys';
import snakecaseKeys from 'snakecase-keys';

// 响应拦截器转换
request.interceptors.response.use(res => {
  if (res.data) {
    // deep: true 表示深度转换(如果有嵌套数据)
    res.data = camelcaseKeys(res.data, { deep: true });
  }
  return res;
});

// 请求拦截器转换
request.interceptors.request.use(config => {
  if (config.data) {
    config.data = snakecaseKeys(config.data, { deep: true });
  }
  return config;
});

3. 让后端直接返回小驼峰(零前端损耗)

这是终极优化方案,后端在 ORM 层(数据库操作层)配置一下,就能直接返回小驼峰,前端完全不用做任何转换,零性能损耗。

而且在接口收发的地方可以通过转换方案将小驼峰转换成下划线,也就是将压力放在了后端。

总结

解决问题的方案很明确:

  • 前端:小驼峰到底。
  • 后端/数据库:下划线到底。
  • 中间:自动转换到底,绝不混用、绝不妥协。