每天閱讀一個 npm 模組(1)- username

elvinnn發表於2018-08-24

最近工作比較繁忙,每天能用於學習知識的時間越來越少,深感這樣不利於自己的技術提升。恰好想起 狼叔 所說的 “迷茫時學習 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'
});
複製程式碼

原始碼學習

核心程式碼一共二十多行,主體邏輯為:

  1. 首先通過 process.env 變數中的值獲得使用者名稱,若存在,直接返回;
  2. 接著若存在 os.userinfo 函式,則通過 os.userinfo().username 獲得使用者名稱並返回;
  3. 若上述方法均失敗,則在 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 還有三個地方需要了解:

  1. 可以通過 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'
    複製程式碼
  2. 可以通過 delete 方法刪除環境變數。

    process.env.foo = undefined;
    delete process.env.foo
    console.log(process.env.foo)
    // => undefined
    複製程式碼
  3. 在 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_USERC9_USER 這兩個變數:

  1. 當使用者身份是 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
    複製程式碼
  2. 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 有兩個好處:

  1. 作為使用者,一般只關心能否拿到正確結果,不會關心包內部的異常資訊,此時詳細的出錯資訊反而是一種干擾。
  2. 返回 undefined 的話,使用者在編寫呼叫程式碼時會更簡單,當然這一點和個人風格有關。

另外需要注意的是,這裡使用的是第三方包 execa 而不是 Node.js 內建的 child_process.exec 模組來執行命令列命令,execa 在原生模組的基礎上進行了提升,目前每週下載量約為 600 萬,這裡主要是利用了其提供 Promise 的介面。

寫在最後

今天通過 username 六十行的程式碼:

  1. 瞭解系統環境變數,懂得了 SUDO_USERUSER 變數的區別;
  2. 學會 Node.js 中 process.env 增刪查改;
  3. 瞭解 Node.js 中 os.userInfo() 返回的資訊;
  4. 知道 process.platform 返回的值不止 darwin | win32 | Linux,也許 username 這裡能有更好的處理;
  5. noop 空函式在 Promise 出錯時吞掉異常的優點。

其實 username 還通過 mem 包對結果進行緩衝提升了效率,明天將會閱讀 mem 包進行學習。

關於我:畢業於華科,工作在騰訊,elvin 的部落格 歡迎來訪 ^_^

相關文章