最近工作比較繁忙,每天能用於學習知識的時間越來越少,深感這樣不利於自己的技術提升。恰好想起 狼叔 所說的 “迷茫時學習 Node.js 最好的方法 - 每天看十個 npm 模組“,雖然每天沒有那麼多時間看十個模組,但時間就像海綿一樣,擠一擠,每天閱讀一個模組還是能做到的。
希望通過這一系列的文章,一方面提醒自己在工作中牢記技術的初心,另一方面鞭策自己在 Node.js 的路上不斷前行。
一句話介紹
第一個 npm 模組我選擇的是 username,用於獲取當前使用者的使用者名稱,當前版本為 3.0.0,周下載量 6萬+。
用法
username 支援同步和 Promise 非同步的寫法:
const username = require('username');
// 同步
console.log(username.sync()); // => 'elvin'
// 非同步
username().then(username => {
console.log(username); // => 'elvin'
});
複製程式碼
原始碼學習
核心程式碼一共二十多行,主體邏輯為:
- 首先通過
process.env
變數中的值獲得使用者名稱,若存在,直接返回; - 接著若存在
os.userinfo
函式,則通過os.userinfo().username
獲得使用者名稱並返回; - 若上述方法均失敗,則在 OS X/Linux 下通過執行
id -un
命令,Windows 下通過whoami
命令獲得使用者名稱並返回。
接下來將結合原始碼對這三步進行探究。
process.env
// 原始碼 1-1
function getEnvVar() {
const env = process.env;
return env.SUDO_USER ||
env.C9_USER /* Cloud9 */ ||
env.LOGNAME ||
env.USER ||
env.LNAME ||
env.USERNAME;
}
const envVar = getEnvVar();
if (envVar) {
return Promise.resolve(envVar);
}
複製程式碼
process.env
返回的是一個包含使用者當前環境變數(environment variable)的物件,可以在命令列執行 printenv
命令檢視所有的環境變數,也可以 printenv v_name
命令獲取某一個環境變數的值:
$ printenv
// => LANG=zh_CN.UTF-8
// => PWD=/Users/elvin/
// => SHELL=/bin/zsh
// => USER=elvin
// => ...
$ printenv USER
// => elvin
複製程式碼
Shell 變數(shell variables)容易與環境變數(environment variable)弄混,通過
set
命令可以檢視所有的 Shell 變數。關於這兩者的區別和使用建議參考這篇英文資料 How To Read and Set Environmental and Shell Variables on a Linux VPS。
在 Node.js 中,關於 process.env
還有三個地方需要了解:
-
可以通過
process.env.foo = "bar"
的方式設定環境變數,目前所有型別的值都允許且會被轉化為 string 型別,根據官方文件,在將來的版本中將只允許 string、number 和 boolean 型別的值,設定其他型別的值將會丟擲異常。process.env.foo = undefined; console.log(process.env.foo, typeof process.env.foo); // => 'undefined', 'string' process.env.foo = {}; console.log(process.env.foo, typeof process.env.foo); // => '[object Object]', 'string' 複製程式碼
-
可以通過
delete
方法刪除環境變數。process.env.foo = undefined; delete process.env.foo console.log(process.env.foo) // => undefined 複製程式碼
-
在 Node.js 中對
process.env
的修改並不會反映在 node 程式之外,不過可以在外部設定環境變數然後通過 Node.js 程式碼去獲取,實踐中經常通過這種方式設定NODE_ENV
變數,然後在 webpack 配置程式碼中讀取它的值來判斷環境進行不同的構建。$ node -e 'process.env.foo = "bar"' && echo $foo // => 空 $ NODE_ENV=production node -e 'console.log(process.env.NODE_ENV)' // => 'production' 複製程式碼
Windows 下不支援直接
NODE_ENV=production
這種方式,需要安裝 cross-env 包進行相容。
回到原始碼 1-1 中的 getEnvVar
函式,可以看到依次嘗試從 process.env
的 SUDO_USER、C9_USER、LOGNAME、USER、LNAME 和 USERNAME 這些環境變數獲得使用者名稱,這裡著重介紹一下 SUDO_USER
和 C9_USER
這兩個變數:
-
當使用者身份是 root 時,此時的
USER
變數會返回 root,而SUDO_USER
變數返回的是登陸為 root 的賬戶名,例如:當我以 elvin 賬戶通過sudo su
變為 root 使用者後,USER
會返回 root,SUDO_USER
會返回 elvin。$ sudo su // => input password $ printenv USER // => root $ printenv SUDO_USER // => elvin 複製程式碼
-
C9_USER
從註釋來看是針對 Cloud9 的適配,它是亞馬遜推出的用於編寫、執行和除錯程式碼的雲 IDE,感興趣的同學可以試一試~
os.userInfo
原始碼中從 process.env
無法獲取使用者名稱時,會嘗試通過 os.userInfo()
函式獲取:
// 原始碼 1-2
if (os.userInfo) {
return Promise.resolve(os.userInfo().username);
}
複製程式碼
os.userinfo()
返回的是當前使用者的一些資訊,相較於 process.env
而言資訊少很多,而且 Node.js V6.0 及以上版本才支援:
const os = require('os');
console.log(os.userInfo());
// => {
// => uid: 501,
// => gid: 20,
// => username: 'elvin',
// => homedir: '/Users/elvin',
// => shell: '/bin/zsh'
// => }
複製程式碼
上述各欄位的意思是:
- uid: 使用者 id(user id),每一個使用者在系統內都由唯一的 id 標示,例如 id 501 表示使用者 elvin。在 Linux 系統上,uid 資訊儲存在
/etc/passwd
檔案中,root 使用者的 uid 為 0。 - gid: 使用者所屬組 id(group id),一個使用者可以屬於多個組,例如 gid 20 表示使用者 elvin 屬於 id 為 20 的組。在 OS X/Linux 系統上,gid 資訊儲存在
/etc/group
檔案中,root 使用者的 gid 為 0。 - username: 當前使用者名稱,以 elvin 使用者登陸 root 時,它返回的值是 root 而不是 elvin。
- homedir: 當前使用者的主目錄。
- shell: 當前使用者的 shell 路徑。
執行命令列命令
當前兩種方式都無法獲取使用者名稱時,在 OS X/Linux 下會通過 id -un
命令獲取使用者名稱,在 Windows 下會通過 whoami
命令獲取使用者名稱。
// 原始碼 1-3
function cleanWinCmd(x) {
return x.replace(/^.*\\/, '');
}
function noop() {}
if (process.platform === 'darwin' || process.platform === 'linux') {
return execa('id', ['-un']).then(x => x.stdout).catch(noop);
} else if (process.platform === 'win32') {
return execa('whoami').then(x => cleanWinCmd(x.stdout)).catch(noop);
}
複製程式碼
上述程式碼首先通過 process.platform
判斷作業系統,若是 OS X(即 darwin)或是 Linux,則執行 id -un
獲取使用者名稱;若是 Windows(即 win32),則執行 whoami
獲取平臺 & 使用者名稱,再通過 cleanWinCmd
函式利用正則提取使用者名稱。其實在 OS X/Linux 上也能通過 whoami
獲取使用者名稱,但其已經在文件中宣告被 id
命令淘汰(obsoleted)。
根據 Node.js 文件,process.platform
會返回當前的平臺,包括 aix | darwin | freebsd | linux | openbsd | sunos | win32 | android,所以其實可以看出上述程式碼只考慮了其中的三種情況,個人覺得可以適當做一些如下修改:
if (process.platform === 'win32') {
return execa('whoami').then(x => cleanWinCmd(x.stdout)).catch(noop);
} else {
return execa('id', ['-un']).then(x => x.stdout).catch(noop);
}
複製程式碼
這裡提出的改進已經通過 PR #20 被 merge 到最新的程式碼中 ?
在原始碼 1-3 中,noop
空函式的使用也值得學習:當命令執行異常時,通過 noop
函式吞掉報錯,並返回 undefined
。一開始我會好奇這裡為什麼不將詳細的異常資訊返回便於出錯時定位,但後來站在包使用者的角度來看,我認為直接返回 undefined
有兩個好處:
- 作為使用者,一般只關心能否拿到正確結果,不會關心包內部的異常資訊,此時詳細的出錯資訊反而是一種干擾。
- 返回
undefined
的話,使用者在編寫呼叫程式碼時會更簡單,當然這一點和個人風格有關。
另外需要注意的是,這裡使用的是第三方包 execa 而不是 Node.js 內建的 child_process.exec
模組來執行命令列命令,execa 在原生模組的基礎上進行了提升,目前每週下載量約為 600 萬,這裡主要是利用了其提供 Promise 的介面。
寫在最後
今天通過 username 六十行的程式碼:
- 瞭解系統環境變數,懂得了
SUDO_USER
與USER
變數的區別; - 學會 Node.js 中
process.env
增刪查改; - 瞭解 Node.js 中
os.userInfo()
返回的資訊; - 知道
process.platform
返回的值不止 darwin | win32 | Linux,也許username
這裡能有更好的處理; noop
空函式在 Promise 出錯時吞掉異常的優點。
其實 username 還通過 mem 包對結果進行緩衝提升了效率,明天將會閱讀 mem 包進行學習。
關於我:畢業於華科,工作在騰訊,elvin 的部落格 歡迎來訪 ^_^