ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

就因为一个大小写,接口对了三天:camelCase / snake_case / Title Case 一次讲透

就因为一个大小写,接口对了三天:camelCase / snake_case / Title Case 一次讲透 前几天一个做全栈的朋友在群里问我“你写代码的时候字段名风格是怎么统一的我们前后端每次联调一半时间都耗在userName和user_name上。”说实话这个问题我也踩过坑而且踩得不轻。去年有次上线前端传orderDetailList后端接的是order_detail_list序列化框架没配命名策略字段直接是 null。排查了三天——先看网关、再看拦截器、最后抓包对比 JSON——才发现卡在第一个字母的大小写上。那天之后我做了两件事把项目里的命名规范写进 lint 配置以及把大小写转换这件小事彻底搞明白。这篇就把整理下来的东西一次讲清楚。问题根源大小写不是风格问题是契约问题很多人觉得命名风格是个人喜好其实不是。在一个系统里字段名、类名、文件名、URL、常量、数据库列名各自都有默认约定的风格。风格一旦跨了边界前端 ↔ 后端 ↔ 数据库 ↔ 文件 ↔ URL大小写就从风格变成了契约。 几个容易出事的地方JSON key 严格区分大小写userName和username是两个不同字段反序列化不会自动兜底HTTP 头字段名不区分大小写RFC 9110 §5.1但body 里的 JSON key 区分数据库标识符MySQL 表名在 Linux 上区分大小写macOS/Windows 默认不区分PostgreSQL 不加引号的标识符会折叠成小写加了引号就严格区分URL 路径/api/userProfile和/api/userprofile是两个路由文件名Linux 区分Windows/macOS 默认不区分——这就是我这能跑到 CI 上就挂的经典来源换句话说大部分莫名 400“字段是 null”“本地好使线上崩”都能追到大小写。九种风格对照表先把常见的转换目标摆在一起这张表基本覆盖了日常工具里能看到的全部按钮风格示例典型用途UPPER CASEORDER DETAIL常量、SQL 关键字、环境变量lower caseorder detail日志、URL 归一化Title CaseOrder Detail标题、UI 文案、报表表头Sentence caseOrder detail段落首句、Git commit 首行tOGGLE cASEoRDER dETAIL大小写反转用于纠正误触 CapsLockcamelCaseorderDetailJava / JS / Go 的变量与方法名PascalCaseOrderDetail类名、组件名、C# 公开成员snake_caseorder_detailPython 变量与函数、数据库列名、Rustkebab-caseorder-detailURL slug、CSS 类名、命令行参数、K8s 资源名⚠️ Title Case 和 Sentence case 的区别前者每个词首字母大写后者只大写句子第一个字母。文档标题用 Title CaseGit commit 描述用 Sentence case混用会被 code review 挑。技术原理转换的核心是分词不是改大小写这是我想重点讲的部分。很多人以为大小写转换就是.upper()/.lower()那一套。但一旦涉及命名风格互转真正的难点根本不在大小写而在怎么把一个字符串切成词。分词的四个难点难点一连续大写的缩写词HTTPResponse应该切成HTTPResponse不是HTTPResponse更不是逐字母拆开。规则是一串大写后面紧跟小写时最后那个大写字母属于下一个词。XMLHttpRequest→XMLHttpRequest难点二数字算谁的getUserId2FA里的2该跟着Id还是跟着FA业界没有统一答案多数实现把数字归到前一个词get/User/Id2/FA。难点三分隔符混用现实里的输入往往是order-detail_list这种混着来的还有--foo--bar--、trim me这种带多余空格的。切词之前要先把所有非字母数字字符统一当分隔符。难点四非 ASCIIcafé、Straße、订单编号、连字ff。用[a-zA-Z]写的正则在这里全部失效。各风格的生成公式拿到词数组之后剩下就是拼装UPPER → words.map(upper).join( ) lower → words.map(lower).join( ) Title → words.map(capitalize).join( ) Sentence → capitalize(words[0]) rest.map(lower).join( ) camelCase → lower(words[0]) rest.map(capitalize).join() PascalCase → words.map(capitalize).join() snake_case → words.map(lower).join(_) kebab-case → words.map(lower).join(-) tOGGLE → 逐字符 swapcase不需要分词注意 tOGGLE 是唯一一个不分词的它就是逐字符反转。语言层面的坑这部分是我踩过之后才补的功课。Python 的str.title()不是你以为的 Title Casetheyre bills friends.title()# TheyRe BillS Friends ← 撇号后面也被大写了HTTPResponse handler.title()# Httpresponse Handler ← 缩写词被压扁了str.title()的规则是非字母字符后的第一个字母大写所以mother-in-law会变成Mother-In-Law。要正确的 Title Case 得用string.capwords()或者第三方titlecase库后者还实现了 of / the / and 这类虚词不大写的排版规则。upper()/lower()会改变字符串长度ß.upper()# SS 长度 1 → 2ff.upper()# FF 连字被展开İ.lower()# i̇ i 组合上点 U0307长度 2如果代码里有len(x) len(y)这种假设或者按下标做 substring遇到这些字符就会错位。土耳其语 iistanbul.upper()在 Python 里得到ISTANBUL但土耳其语正确写法是İSTANBUL带点的大写 I。Python 的str.upper()不做 locale 映射。JavaScript 里对应的是toLocaleUpperCase(tr)/toLocaleLowerCase(tr)。一个可用的 Unicode 分词实现不靠 ASCII 正则只用isupper()/islower()/isdigit()天然支持中文和重音字符deftokenize(text:str)-list[str]:把任意字符串切成词处理缩写、数字、混用分隔符和非 ASCIIout,cur,prev[],[],Nonedefkind(ch):ifch.isupper():returnUifch.islower():returnLifch.isdigit():returnDreturnNone# 其余一律当分隔符forchintext:kkind(ch)ifkisNone:ifcur:out.append(.join(cur));cur[]prevNonecontinue# 一串大写后接小写HTTPResponse - HTTP | ResponseifprevUandkLandlen(cur)1:out.append(.join(cur[:-1]));cur[cur[-1]]# 小写或数字后接大写userName - user | Nameelifprevin(L,D)andkU:out.append(.join(cur));cur[]cur.append(ch);prevkifcur:out.append(.join(cur))returnoutdefsnake(t):return_.join(w.lower()forwintokenize(t))defkebab(t):return-.join(w.lower()forwintokenize(t))defpascal(t):return.join(w.capitalize()forwintokenize(t))defcamel(t):wtokenize(t)returnw[0].lower().join(x.capitalize()forxinw[1:])ifwelse实测结果原始字段snake_casecamelCasePascalCasekebab-caseuser nameuser_nameuserNameUserNameuser-nameOrder Detailorder_detailorderDetailOrderDetailorder-detailHTTP Response Handlerhttp_response_handlerhttpResponseHandlerHttpResponseHandlerhttp-response-handlerXMLHttpRequestxml_http_requestxmlHttpRequestXmlHttpRequestxml-http-requestorder_status_codeorder_status_codeorderStatusCodeOrderStatusCodeorder-status-codeget user id v2get_user_id_v2getUserIdV2GetUserIdV2get-user-id-v2café menu idcafé_menu_idcaféMenuIdCaféMenuIdcafé-menu-idJavaScript 版同理正则写法更短constwordsss.trim().match(/[A-Z](?![a-z])|[A-Z][a-z0-9]*|[a-z0-9]|[^\x00-\x7F]/g)??[];constsnakeswords(s).map(ww.toLowerCase()).join(_);constcamelswords(s).map((w,i)i?w[0].toUpperCase()w.slice(1).toLowerCase():w.toLowerCase()).join();⚠️ 这里的[^\x00-\x7F]只是兜底遇到café会切成café。要正确处理带变音符的词得用 Unicode 属性类JS 开u标志写\p{Lu}/\p{Ll}Python 装regex库后用同样的语法。实操四种做法按场景选方法一编辑器自带单点修改最快VS Code 的命令面板里内置了六个转换命令其中 Transform to Snake Case 是 1.53 版本加的选中文字后Cmd/Ctrl Shift PTransform to Uppercase Transform to Lowercase Transform to Title Case Transform to Snake Case Transform to Camel Case Transform to Kebab Case没有 PascalCase需要装插件或自己绑快捷键。IntelliJ 系IDEA / PyCharm / WebStorm自带的是大小写切换Ctrl Shift U驼峰互转要装 CamelCase 插件。方法二命令行管道里顺手改# 空格/短横线转下划线 全小写printf%s\nOrder Detail List|tr[:upper:][:lower:]|tr -__# order_detail_list# snake_case 转 camelCaseprintf%s\norder_detail_list|awk-F_{ printf %s%s, tolower(substr($1,1,1)), substr($1,2) for (i2; iNF; i) printf %s%s, toupper(substr($i,1,1)), substr($i,2) print }# orderDetailListtr在 UTF-8 locale 下能正确处理CAFÉ→café但在LC_ALLC下是按字节处理的多字节字符会出问题别在 C locale 里跑。方法三脚本批量要改几百个字段时上面那段tokenize直接拿来用。批量转 JSON keyimportjsondefconvert_keys(obj):ifisinstance(obj,dict):return{snake(k):convert_keys(v)fork,vinobj.items()}ifisinstance(obj,list):return[convert_keys(v)forvinobj]returnobj datajson.load(open(resp.json,encodingutf-8))print(json.dumps(convert_keys(data),ensure_asciiFalse,indent2))JS 侧对应的做法是在 HTTP 层统一转换别在每个组件里手改// 出去 camelCase回来 snake_caseapi.interceptors.request.use(cfg{cfg.datatoSnake(cfg.data);returncfg;});api.interceptors.response.use(res({...res,data:toCamel(res.data)}));Java / Spring 更省事一行配置objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);Python 侧用 PydanticfrompydanticimportBaseModel,ConfigDictfrompydantic.alias_generatorsimportto_camelclassOrder(BaseModel):model_configConfigDict(alias_generatorto_camel,populate_by_nameTrue)order_status_code:str方法四在线工具临时、一次性的转换有些场景就是不想开 IDE也不想写脚本——比如写文档时要把一段标题转成 Title Case或者临时看一眼某个字段四种风格分别长什么样。我自己用的是这个在线大小写转换工具。左边粘贴文本上面九个按钮就是前面对照表里的九种风格点一下出结果可以连续点不同按钮做横向对比结果一键复制。页面说明里写了转换是纯前端完成的、不往服务器发请求所以贴内部字段名相对安全一些——当然涉密内容还是别往任何外部页面粘。https://leowh.com/tools/case-convert/index.html它适合一次性、少量、要快速比对的场景。如果是批量改代码、要进 CI、要可复现还是走方法三的脚本把转换逻辑纳入版本控制。四种方法对比方法适合场景批量能力可复现 / 进 CI离线编辑器命令改一两个变量名弱否✅命令行 tr / awk管道里顺手转、改文件名中部分✅脚本改接口字段、批量重构强✅✅在线工具临时比对、写文档弱否视实现实战场景场景一批量重命名文件下载下来一堆My Photo 2024.JPG、Order-Detail List.PDF统一成 snake_case。先 dry-run 看结果确认无误再执行forfin*;dobase${f%.*};ext${f##*.}[$base$f]extnew$(printf%s$base|tr[:upper:][:lower:]|tr -__\|sed-Es/_/_/g; s/^_//; s/_$//)[-n$ext]new$new.$(printf%s$ext|tr[:upper:][:lower:])printf%s\t- %s\n$f$new# 确认后把 printf 换成 mv -n $f $newdone实测输出HTTP Response.log - http_response.log My Photo 2024.JPG - my_photo_2024.jpg Order-Detail List.PDF - order_detail_list.pdf userProfile.json - userprofile.json⚠️ 两个提醒mv一定带-n防止覆盖已有文件userProfile.json → userprofile.json说明tr不会拆驼峰要拆得先过一遍sed -E s/([a-z0-9])([A-Z])/\1_\2/g。场景二DDL 字段批量导出成接口文档表格ddluser name, Order Detail, HTTP Response Handler, XMLHttpRequest, order_status_code, get user id v2print(| 原始字段 | snake_case | camelCase | PascalCase | kebab-case |)print(| --- | --- | --- | --- | --- |)forcolinddl.split(,):colcol.strip()ifcol:print(f| {col} | {snake(col)} | {camel(col)} | {pascal(col)} | {kebab(col)} |)一段脚本字段对照表直接贴进 wiki比人工填表可靠得多。场景三修 CapsLock 误触tOGGLE cASE看起来最没用但键盘 CapsLock 忘了关、打完一整段才发现的时候它是唯一能一步还原的操作——因为它不分词逐字符反转信息无损。Python 里对应str.swapcase()aBcDe.swapcase()得到AbCdE。团队落地把风格交给配置别靠人肉记个人项目怎么命名都行团队项目必须交给工具。ESLint 最简单的做法{rules:{camelcase:[error,{properties:never}]}}TypeScript 项目用typescript-eslint/naming-convention能管得更细{rules:{typescript-eslint/naming-convention:[error,{selector:variableLike,format:[camelCase,UPPER_CASE]},{selector:typeLike,format:[PascalCase]},{selector:property,format:[camelCase,snake_case]}]}}其他生态生态约定工具Python函数与变量 snake_case类 PascalCase常量 UPPER_CASEPEP 8ruff / pylint / blackJava字段方法 lowerCamelCase类 UpperCamelCase常量 CONSTANT_CASEGoogle Java StyleCheckstyleMemberName/TypeNameGoMixedCaps不用下划线gofmt / go vetRustsnake_case常量 SCREAMING_SNAKE_CASErustc 内置 lint数据库列名 snake_case、全小写建表规范 CI 检查 最省事的做法在序列化层配一次命名策略JacksonSNAKE_CASE、axios 拦截器、Pydanticalias_generator让风格转换只发生在一个地方业务代码里永远只写一种风格。常见问题Q为什么后端返了字段前端拿到的是 undefinedA九成是大小写不匹配。先抓包看原始 JSON再对比前端取值路径别一上来就翻框架配置。Q数据库列名到底区不区分大小写A分两层看。标识符本身PostgreSQL 不加引号会折叠成小写加引号严格区分MySQL 列名不区分表名在 Linux 上区分由lower_case_table_names控制。数据内容默认区分除非排序规则是..._ci。QTitle Case 里 of、the、and 要不要大写A英文排版规范Chicago / APA里短虚词不大写写成The Art of War。但多数简单工具包括 VS Code 的 Transform to Title Case是每个词都大写得到The Art Of War。正式文档建议人工再过一遍。Q批量转换会不会破坏已有数据A只做命名层、展示层的转换不会。但如果把已入库的字符串数据批量upper()注意ß会变成SS长度变化可能撞索引长度限制或者破坏外键关联。Q为什么本地跑得好好的CI 上报错找不到文件A大概率是文件名大小写。macOS / Windows 文件系统不区分大小写Linux 区分。代码里写import Utils from ./utils而实际文件是Utils.ts本地能过、CI 挂。执行git config core.ignorecase false能提前暴露问题。Q非英文字段名怎么办A中文没有大小写分词时按连续非 ASCII 字符算一个 token处理即可上面的tokenize已经覆盖。但订单_user_name这种混排在 URL 编码和部分语言里会有麻烦接口字段建议统一用 ASCII。总结大小写转换看起来是.upper()/.lower()两行代码实际上✅ 核心难点是分词不是改大小写——缩写词、数字、混用分隔符、非 ASCII 是四个坑✅ Python 的str.title()和str.upper()都有反直觉行为别直接用在生产逻辑里✅upper()/lower()可能改变字符串长度ß→SS有长度假设的代码要小心✅ 单点改用编辑器命令管道用tr/awk批量和进 CI 用脚本临时比对用在线工具✅ 团队项目把风格交给 linter 和序列化层配置别靠 code review 人肉盯
返回列表