前端開發中的大小寫敏感問題#
大小寫敏感(case sensitivity)是軟體開發領域的議題,指同一個 “詞” 拼寫的大小寫字母的不同可能會導致不同效果的場景。
接下來,我們談談前端開發領域中一些常見的大小寫敏感 / 不敏感的場景:
- HTTP Header
- HTTP Method
- URL
- Cookie
- E-Mail Address
- HTML5 Tags and Attribute name
- CSS Property
- File's name in Git
HTTP Header 區分大小寫嗎?#
HTTP Header 的名稱字段是不區分大小寫的。
RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-3.2 中規定:
Each header field consists of a case-insensitive field name followed
by a colon (":"), optional leading whitespace, the field value, and
optional trailing whitespace.
但是需要注意的是,HTTP/2 多了額外的限制,因為增加了頭部壓縮,要求在編碼前必須轉成小寫。
RFC 7540 - Hypertext Transfer Protocol Version 2 (HTTP/2)#section-8.1.2 中規定:
However, header field names MUST be converted to lowercase prior to their
encoding in HTTP/2.
而如今大多數客戶端與 HTTP 服務端都默認會把 HTTP Header 的名稱字段統一改成小寫,避免使用方再重複做大小寫轉換的處理邏輯。
比如 NodeJS 中的 HTTP 模塊就會自動將 Header 字段改成小寫 node/_http_outgoing.js at main · nodejs/node · GitHub:
// lib/_http_outgoing.js
ObjectDefineProperty(OutgoingMessage.prototype, '_headers', {
__proto__: null,
get: internalUtil.deprecate(function() {
return this.getHeaders();
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066'),
set: internalUtil.deprecate(function(val) {
if (val == null) {
this[kOutHeaders] = null;
} else if (typeof val === 'object') {
const headers = this[kOutHeaders] = ObjectCreate(null);
const keys = ObjectKeys(val);
// Retain for(;;) loop for performance reasons
// Refs: https://github.com/nodejs/node/pull/30958
for (let i = 0; i < keys.length; ++i) {
const name = keys[i];
headers[StringPrototypeToLowerCase(name)] = [name, val[name]];
}
}
}, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066')
});
PS. 在寫這篇文章的時候,發現 NodeJS 的這部分文檔有處錯誤,示例中的獲取 headers 存在大寫字母,於是順手提了個 PR,現已經合入了。doc: fix typo in http.md by airingursb · Pull Request #43933 · nodejs/node · GitHub
除此之外,Rust 的 HTTP 模塊也會將 Header 的字段名默認改成小寫,理由是 HeaderMap 會處理得更快。
文檔可見http::header - Rust:
The
HeaderName
type represents both standard header names as well as custom header names. The type handles the case insensitive nature of header names and is used as the key portion ofHeaderMap
. Header names are normalized to lower case. In other words, when creating aHeaderName
with a string, even if upper case characters are included, when getting a string representation of theHeaderName
, it will be all lower case. This allows for fasterHeaderMap
comparison operations.
源碼可見:src/headers/name.rs - http 0.1.3 - Docs.rs
作為前端開發,會更加關注 Chromium 和 WebKit 對這塊的處理。
對於請求頭而言,我們在 Chrome 中做個實驗:
var ajax = new XMLHttpRequest();
ajax.open("GET", "https://y.qq.com/lib/interaction/h5/interaction-component-1.2.min.js");
ajax.setRequestHeader("X-test", "AA");
ajax.send();
對於 HTTP2 的請求,抓包之後可以發現,這裡的 X-test
被改寫成了小寫 x-test
:
為了嚴謹起見,這裡又使用了 Chrome 自帶的 netlog-viewer 去抓包查看,這裡的請求頭確實是被轉成小寫了沒有問題:
注:以上測試在 Safari 中也會得到同樣的結果,但是必須要抓包看,如果只是在 Safari 的控制台看,它展示大小寫是有問題的,而 Chrome 的 Devtools 則沒有問題。這裡猜測是 Safari 的 Bug。
於是去閱讀 Blink 中 XMLHTTPRequest 的源碼,可惜的是沒有找到在哪裡轉成了小寫。
我也去查 XMLHTTPRequest 的標準,XMLHttpRequest Standard 中也沒有提到需要將 header 字段名改成小寫。
甚至在 XHR 標準中也曾有人建議過直接在 XHR 把 Header 字段名轉成小寫(Should XHR store and send HTTP header names in lower case? · Issue #34 · whatwg/xhr · GitHub),但是被拒絕了。
按照規範 HTTP2 的 Header 名都需要轉成小寫,但我找了個 HTTP1.1 的站點測試了一下:
var ajax = new XMLHttpRequest();
ajax.open("get", "http://www.cn1t.com/airing.js");
ajax.setRequestHeader("X-test", "AA");
ajax.send();
發現這裡的 Header 字段名則並不會轉成小寫:
可以得知,請求頭的字段名大小寫轉換不是 XMLHTTPRequest 做的事情,而是底層網絡庫的邏輯。
PS. 我這裡 Debug 了許久,沒有找到更底層具體是哪里轉成了小寫,若有知曉的同學可以直接評論告知,萬分感謝。
而響應頭中的字段名則不一樣了。如果你使用 XMLHTTPRequest 的 getAllResponseHeaders
等方法去獲取響應頭,Blink 會將 Header 的名稱字段改成小寫:
String XMLHttpRequest::getAllResponseHeaders() const {
// ...
for (const auto& header : headers) {
string_builder.Append(header.first.LowerASCII());
string_builder.Append(':');
string_builder.Append(' ');
string_builder.Append(header.second);
string_builder.Append('\r');
string_builder.Append('\n');
}
// ...
}
此外,Chromium 在解析網絡響應包的時候,如果走 HTTP2 協議,發現了 Header 字段名有大寫字母,會直接導致網絡包解析失敗:
absl::optional<ParsedHeaders> ConvertCBORValueToHeaders(
const cbor::Value& headers_value) {
// |headers_value| of headers must be a map.
if (!headers_value.is_map())
return absl::nullopt;
ParsedHeaders result;
for (const auto& item : headers_value.GetMap()) {
if (!item.first.is_bytestring() || !item.second.is_bytestring())
return absl::nullopt;
base::StringPiece name = item.first.GetBytestringAsString();
base::StringPiece value = item.second.GetBytestringAsString();
// If name contains any upper-case or non-ASCII characters, return an error.
// This matches the requirement in Section 8.1.2 of [RFC7540].
if (!base::IsStringASCII(name) ||
std::any_of(name.begin(), name.end(), base::IsAsciiUpper<char>))
return absl::nullopt;
// ...other
}
return result;
}
HTTP Method 區分大小寫嗎?#
根據規範 RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-3.1.1:
The method token indicates the request method to be performed on the target resource. The request method is case-sensitive.
HTTP Method 是區分大小寫的,並且全部為大寫。
但其實如果你的 XMLHttpRequest 實例傳入小寫的 Method 也是沒有關係的(如調用 ajax.open("get", "https://ursb.me")
),Blink 等瀏覽器內核會將其規範化改成大寫 Source/core/xmlhttprequest/XMLHttpRequest.cpp - chromium/blink - Git at Google:
void XMLHttpRequest::open(const AtomicString& method,
const KURL& url,
bool async,
ExceptionState& exception_state) {
//...
method_ = FetchUtils::NormalizeMethod(method);
// ...
}
AtomicString FetchUtils::NormalizeMethod(const AtomicString& method) {
// https://fetch.spec.whatwg.org/#concept-method-normalize
// We place GET and POST first because they are more commonly used than
// others.
const char* const kMethods[] = {
"GET", "POST", "DELETE", "HEAD", "OPTIONS", "PUT",
};
for (auto* const known : kMethods) {
if (EqualIgnoringASCIICase(method, known)) {
// Don't bother allocating a new string if it's already all
// uppercase.
return method == known ? method : known;
}
}
return method;
}
URL 區分大小寫嗎?#
根據規範 RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1)#section-2.7.3 中的描述:
The scheme and host are case-insensitive and normally provided in lowercase; all other components are compared in a case-sensitive manner.
HTTP 協議 (scheme /protocol) 和域名 (host) 不區分大小寫,但 path、query、fragment 是區分的。
在 Chromuim 中,URL 統一使用 kUrl
類管理,其內部會對 protocol
與 host
做規範化處理(canonicalization),這個過程會將 protocol 與 host 改為小寫。
除此之外,其中比較有意思的是 path
,雖然規範約定了 path
是區分大小寫的,但實際情況卻取決於 Web Server 的底層文件消息,因此 path 有可能也是不區分大小寫的。
比如 IIS 伺服器就是不區分的,因為它取決於 Windows 的文件系統,Windows 使用的 NTFS 和 FAT 系列文件系統,默認大小寫不敏感(但大小寫保留)。對應的,如果 Apache 伺服器部署在大小寫不敏感的 Mac (HFS) 上,也同樣不區分大小寫。
擴展介紹一些主流操作系統的底層文件系統:
- Windows 使用的 NTFS 和 FAT 系列文件系統,默認大小寫不敏感,但大小寫保留
- macOS 使用的 APFS 和 HFS+ 文件系統,默認大小寫不敏感,但大小寫保留
- Linux 使用的 ext3/ext4 文件系統,默認大小寫敏感
除此之外,還和伺服器的策略有關係,比如 https://en.wikipedia.org/wiki/Case_sensitivity 和 https://en.wikipedia.org/wiki/case_sensitivity 指向同一篇文章,但是就不能簡單認為它大小寫不敏感,因為 https://en.wikipedia.org/wiki/CASE_SENSITIVITY 就直接 404 了。
Cookie 區分大小寫嗎?#
先說結論,符合直觀,Cookie 的名稱是區分大小寫的。但是規範的演進比較坎坷。
早期的規範 RFC 2109 - HTTP State Management Mechanism Cookie 的名字是不區分大小寫的:
Attributes (names) (attr) are case-insensitive. White space is permitted between tokens. Note that while the above syntax description shows value as optional, most attrs require them.
This document reflects implementation experience with RFC 2109 and obsoletes it.
但是在 Cookie 的最新規範 RFC 6265 中並沒有寫明 Cookie 是否區分大小寫,那麼默認可以認為是區分的,主流瀏覽器 Chrome 與 FireFox 的實現也都是區分 Cookie 的大小寫。
E-Mail 地址區分大小寫嗎?#
根據規範 RFC 5321: Simple Mail Transfer Protocol#section-2.3.11:
The standard mailbox naming convention is defined to be "local-part@domain"; contemporary usage permits a much broader set of applications than simple "user names". Consequently, and due to a long history of problems when intermediate hosts have attempted to optimize transport by modifying them, the local-part MUST be interpreted and assigned semantics only by the host specified in the domain part of the address.
而 domain 部分遵循 RFC 1035: Domain names - implementation and specification#section3.1:
"Name servers and resolvers must compare [domains] in a case-insensitive manner"
綜上所述:郵箱中的域名不區分大小寫,而用戶名(local-part)是否區分大小寫,則取決於電子郵件服務商。
HTML5 標籤和屬性名區分大小寫嗎?#
根據規範 HTML Live Standard#section-13.1,HTML 標籤和屬性名不區分大小寫:
Many strings in the HTML syntax (e.g. the names of elements and their attributes) are case-insensitive, but only for characters in the ranges U+0041 to U+005A (LATIN CAPITAL LETTER A to LATIN CAPITAL LETTER Z) and U+0061 to U+007A (LATIN SMALL LETTER A to LATIN SMALL LETTER Z). For convenience, in this section this is just referred to as "case-insensitive".
這意味著文檔類型 <!DOCTYPE html>
寫成 <!doctype html>
也是可以的。
需要注意的是,data attribute 是個例外,它必須要小寫。HTML Live Standard#section3.2.6.6:
A custom data attribute is an attribute in no namespace whose name starts with the string "
data-
", has at least one character after the hyphen , is XML-compatible, and contains no ASCII upper alphas.
Blink 有糾錯邏輯,即便寫了 <div data-Name="airing"></div>
,最後也會被轉成 <div data-Name="airing"></div>
。但考慮到兼容性,這裡的屬性名還是需要小寫 data-name
的。
CSS 區分大小寫嗎?#
All Selectors syntax is case-insensitive within the ASCII range (i.e. [a-z] and [A-Z] are equivalent), except for parts that are not under the control of Selectors. The case sensitivity of document language element names, attribute names, and attribute values in selectors depends on the document language. For example, in HTML, element names are case-insensitive, but in XML, they are case-sensitive. Case sensitivity of namespace prefixes is defined in [CSS3NAMESPACE].
CSS 的選擇器語法不區分大小寫,而屬性名與屬性值是否區分大小寫,取決於所在的文檔語言。 如在 XHTML DOCTYPE 中它們區分大小寫,但是在 HTML DOCTYPE 則不區分。
Git 文件名區分大小寫嗎?#
Git 默認不區分文件名大小寫。
因此,如果我們平常不注意文件的大小寫,在實際使用中可能會遇到這樣的問題:
- 如果團隊中有人在 Linux 系統或者開啟文件系統大小寫敏感的 macOS 或 Window 上開發,他無視了已經存在的
RankItem.tsx
文件,創建了新的rankItem.tsx
文件,並提交成功了; - 那麼此時 Git 伺服器上同時存在
RankItem.tsx
與rankItem.tsx
文件,在 Windows 或 macOS (默認文件系統) 上開發的人,則無法正常拉取到這兩個文件。
這裡建議關閉 Git 的忽略大小寫功能:
git config --global core.ignorecase false
同時在 Windows 或 macOS 上重命名大小寫時,使用 git mv
:
git mv --force rankItem.jsx RankItem.jsx
如果沒有開啟的話,這裡舉個例子,將 readme.md 改成 README.md,這個時候 git status
無法檢測到更變記錄:
如果用 git mv
,則是可以正常檢測到的:
因此在開發的時候,強烈推薦關閉 Git 大小寫忽略的配置,並且使用 git mv
進行重命名操作。若條件允許的話,亦可以修改 macOS 或 Windows 默認的文件系統,將卷宗的文件系統改成大小寫敏感。
以上便是前端開發中的一些大小寫敏感問題,總結一下:
- HTTP Header 的 key 不區分大小寫,但是絕大多數框架會將響應頭的 key 改成小寫。[RFC 7230]
- HTTP2 則因為頭部壓縮,要求必須小寫,瀏覽器會將其改成小寫。[RFC 7540]
- HTTP Method 區分大小寫,且必須為大寫。[RFC 7230]
- URL 的 protocol 和 host 不區分大小寫,瀏覽器會自動改成小寫。[RFC 7230]
- URL 的 path 按規範而言是區分大小寫的,但實際是否區分大小寫取決於 Web Server 的文件系統和服務端配置。[RFC 7230]
- URL 的 query 與 fragment 是區分大小寫的。[RFC 7230]
- Cookie 的 key 是區分大小寫的,雖然規範並沒有明說這點。 [RFC 6265]
- E-Mail 的域名部分是不區分大小寫的 [RFC 1035]
- E-Mail 的用戶名部分是否區分大小寫,取決於郵件服務商。 [RFC 5321]
- HTML5 的標籤名和一般的屬性 key 是不區分大小寫的。[HTML Live Standard 13.1]
- HTML5 的 data- 屬性是區分大小寫的。[HTML Live Standard 3.2.6.6]
- CSS 的選擇器語法不區分大小寫,而屬性名與屬性值是否區分大小寫,取決於所在的文檔語言。[CSS Selectors Level 3]
- Git 默認是忽略文件大小寫的。
- Windows 使用的 NTFS 和 FAT 系列文件系統,默認大小寫不敏感,但大小寫保留。
- macOS 使用的 APFS 和 HFS+ 文件系統,默認大小寫不敏感,但大小寫保留。
- Linux 使用的 ext3/ext4 文件系統,默認大小寫敏感。