作者:chainhelen from 迅雷
本文講述作者是如何定位發現 Node.js 存在的一處問題,Node.js 最新版本已經修復了該問題。本文主要分享定位問題的思路和方法,當你在開發當中遇到疑難問題的時候,不排除是依賴的技術和框架出現了問題,當你嘗試找到並修復它,我相信不光可以收穫到貢獻程式碼的成就感,也會帶來技術水平和信心的提升。
1. 問題
前幾日,我在測試express
框架的時候,構造了一個測試樣例死活過不來,即便除錯到測試框架superagent
,依然不對。最終發現是Node.js
的”問題”,而且最新版本的Node.js
已經”修復”了,導致我中間饒了幾圈都沒發現是Node.js
的事,下面來重現問題流程。
2. 環境預備
- 安裝一下
gnvm
地址,後面需要控制一下版本(windows10 需要用管理員許可權的 cmd 或者 powershell) - 安裝 git 環境(主要要使用
curl
命令) - 摘抄如下程式碼
// main.js
var http = require(`http`)
var tmpObject = Object
tmpObject.prototype[`love`] = `express`
var server = http.createServer(function (_, res) {
res.setHeader("m", "w")
res.end()
})
server.listen(3010)
複製程式碼
3.問題復現
- 安裝 Node.js (當前穩定版)版本
gnvm install 8.11.2
gnvm use 8.11.2
複製程式碼
- 執行程式碼,
Node.js main.js
- 使用
curl -i 127.0.0.1:3010
命令,得到如下
$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK
m: w
e: x
Date: Fri, 18 May 2018 14:06:47 GMT
Connection: keep-alive
Content-Length: 0
複製程式碼
能理解有一個頭
m: w
,但是e: x
是從哪來的?明明奇怪的改動只是 Object.prototype.love=`express`
4.再次測試
修改一下main.js
的程式碼,註釋掉res.setHeader("m", "w")
試試看
// main.js
var http = require(`http`)
var tmpObject = Object
tmpObject.prototype[`love`] = `express`
var server = http.createServer(function (_, res) {
// res.setHeader("m", "w")
res.end()
})
server.listen(3010)
$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK
Date: Fri, 18 May 2018 14:30:01 GMT
Connection: keep-alive
Content-Length: 0
複製程式碼
竟然沒有了
5.解釋
翻閱v8.11.2
程式碼
_http_outgoing.js#L497
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
...
if (!this[outHeadersKey])
this[outHeadersKey] = {};
const key = name.toLowerCase();
this[outHeadersKey][key] = [name, value];
...
};
複製程式碼
那麼3測試
裡面程式碼執行的時候,儲存 header 的資料是這樣的
this[outHeaderKey] = {
"m": ["m", "w"]
}
複製程式碼
另外注意變數初始化this[outHeadersKey] = {}
,那麼this[outHeaderKey]
的原型鏈指向Object.prototype
有了上面的認知,來看下res.end()
做了哪些事,寫一下呼叫鏈
_http_outgoing.end() => _http_server._implicitHeader() => _http_server.writeHead() => _http_outgoing._storeHeader()
看一下_http_server.writeHead()
,_http_server#L202
headers = this[outHeadersKey];
this._storeHeader(statusLine, headers);
複製程式碼
繼續看一下_http_outgoing.storeHeader()
,_http_server#L307
if (headers === this[outHeadersKey]) {
for (key in headers) {
var entry = headers[key];
field = entry[0];
value = entry[1];
...
}
複製程式碼
1.當上述for in
遍歷到自定義res.setHeader("m", "w")
中的 "m":["m": "w"]
key
=m
,entry
= [m, w]
則取出資料 field
= m
,value
= w
,沒毛病
2.但當for in
遍歷到原型鏈的時候,key = `love`
,entry = `express`
那麼field = entry[0] = `e`
,value = entry[1] = `x`
故而響應頭中的 e:x
就是這麼來的
6.小結
本質上是for in
遍歷到原型鏈,加上Node.js
儲存 outHeadersKey
的”奇怪”陣列方式
才會導致發包過程中出現了一個難以理解的header
另外,對於for in
來說,專案中通常採用hasOwnProperty
來規避問題,但是新版本Node.js
不是這樣做的,下面是最新的Node.js
這塊程式碼
_http_outgoing.js#L121
const headers = this[outHeadersKey] = Object.create(null);
複製程式碼
Object.create(null)
會把建立出來的物件__proto__
指向 null
for in
就不會遍歷到了,可以使用gnvm use v10.1.0
嘗試一下,最新版本已經沒有問題了