NodeJS和命令列程式

奇舞週刊發表於2019-04-15

本文作者:劉觀宇,360 奇舞團高階前端工程師、技術經理,W3C CSS 工作組成員。

NodeJS和命令列程式

造物無言卻有情,每於寒盡覺春生。千紅萬紫安排著,只待新雷第一聲。 —— 清.張維屏 《新雷》

源起

植根於Unix系統環境下的程式,很多都把貫徹Unix系統設計的哲學作為一種追求。Unix系統管道機制的發明者Douglas McIlroy把Unix哲學總結為三點:

  1. 專注做一件事,並做到極致。
  2. 程式協同工作。
  3. 面向通用介面,如文字資料流。

隨著Unix/Linux系統在伺服器上影響力越發強大,以及各種跨平臺解決方案的發展,這種哲學也被帶到了各種平臺上。若干年前,筆者第一次接觸NodeJS和其包管理解決方案NPM時候,就感覺到其官方倡導的風格,和Unix系統哲學非常契合。近年來,隨著NodeJS在服務端以及前端構建領域上的不斷開拓,NodeJS的這種思想也正快速的滲透到這些領域。

其實,NodeJS的本身,也是開發命令列程式的一個重要利器。本文就將介紹幾個常用的NodeJS相關命令列程式,之後介紹幾個開發命令列中常用的元件,最後介紹釋出npm包以及帶scope的包的釋出方法。

命令列是如何工作的

命令列,可以簡單定義為是一種基於文字流的使用者互動介面和互動方式。命令列程式常常通過命令列引數的傳遞來得到不同的執行方式。而由於一切命令的下達,都是基於文字的,所以也為超程式設計,提供了便利。

命令列程式可以是編譯執行的,也可以是解釋執行的。對於編譯後的命令列程式,將直接以機器碼執行。而對於大多數的解釋型的命令列程式,執行往往需要藉助命令列解釋程式。

這篇文章中提到的命令列程式特指需要解釋程式的命令列程式。

可以充當命令列解釋程式的,其實包含了大家聽說過的常見的直譯器,比如bash、zsh、perl、python、ruby、tcl等等,當然還有NodeJS。

開啟一個命令列程式,比較標準的寫法是在第一行寫明解釋程式的路徑,如:

#!/usr/local/opt/python/bin/python3.6
複製程式碼

這裡 #! 成為shebang,一般位於檔案的最開頭。在Unix系統中,#!所在行後面的部分將被視為直譯器指令。同時會把檔案所在路徑作為引數附在直譯器後面。上例中,如果檔案是/usr/local/bin/pip,則直接執行/usr/local/bin/pip的效果,等同於/usr/local/opt/python/bin/python3.6 /usr/local/bin/pip

這樣做,使得使用者無需關心解釋程式,無需關心程式碼編寫的語言,直接執行對應的命令列程式本身就好了。這也是shebang存在的意義。不過,由於系統設定的原因,使用windows的同學可能無法享受這種便利,一般還需手動指定解釋程式的路徑。但是,他們可以雙擊執行:-)。

可以試著用文字編輯工具開啟一個NodeJS寫成的指令碼如:webpack,會發現其第一行是#!/usr/bin/env node。這句話並不是直接的NodeJS的解析程式。這裡, /usr/bin/env是一個程式,目的是從系統的PATH中尋找對應名字的解釋程式的地址。此時,解釋程式可以被安裝在各種路徑,只要在系統PATH中註冊過,就可以找到了。

可能大家遇到過這種問題,在執行某些NodeJS程式會出現報錯:

/usr/bin/env: node: No such file or directory

此時可以從系統PATH中是否有node這個檔案路徑、某些版本的NodeJS是否名為node等方向來排查問題。

NodeJS相關:好用的命令列工具

在NodeJS目前已經成為前端工作流的主力語言的情況下, babelwebpack基本已經成為前端開發、測試、釋出上的重要工具。同時圍繞babelwebpack有一系列周邊的工具包和外掛協助開發者完成日常開發的方方面面。

同時,目前最為流行的前端框架Angular、react、vue(以首字母為序),各自有自帶的腳手架和開發輔助工具。如ng-clicreate-react-appvue-cli等等。更有Poi這樣的通吃React和Vue的腳手架工具。

上面這部分每一個都可以獨立出來單獨講解。有興趣的讀者可以參考上述工具的官方網站獲取更多資訊。

下面來說幾個其他方面的NodeJS相關的軟體包。

多版本共存 n/nvm

大多數情況,我們只需面對單一的NodeJS版本。等到時機成熟,再統一把NodeJS版本升級到更高版本。

不過筆者就曾經遇到過一個年久失修失修的專案需要重新維護的情況。此時需要把NodeJS版本切到老版本。同時,我們也不想捨棄大多數專案執行的新版本NodeJS環境。

這種情況可以使用n或nvm。下圖展示了,用n下載並切換到一個新版本的過程。

NodeJS和命令列程式

除了下載之外,n還提供了列表的方式切換多個版本,以及刪除某個版本的方法。讀者可以在安裝之後使用n -h檢視所有可用引數。

n採用bash編寫。但提供了一個npm倉庫安裝的入口,可以使用大家傳統意義的npm安裝法進行全域性安裝,前提是你必須有一個可以執行的NodeJS環境。

npm install -g n

或者在沒有NodeJS的環境下,可以使用n-install指令碼。安裝只需執行:curl -L https://git.io/n-install | bash

如果是windows使用者,在windows10下面可以安裝wsl來獲得Linux指令碼執行環境,官方倉庫的一個issues,對此有一個操作說明

對windows10以下的使用者,可以考慮折騰下Cygwin

除了n之外,還有一個管理工具為nvm,也是採用bash指令碼編寫。安裝亦可使用安裝指令碼來完成。如:curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bashwget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash。這裡的v0.34.0是版本號,可能會隨著版本迭代而變化。

使用windows的讀者,除了上述wslCygwin之外,可以考慮使用nvm-windows這個用Golang編寫的版本。

就目前的最新版本來說,n和nvm的都會嘗試處理公共的依賴庫,然而處理方式是不一樣的。

n和nvm都會在首次使用某個版本時將此版本的NodeJS下載至本地,不同的是:n將嘗試用新版本代替系統路徑中,關鍵路徑如bin、lib、include、share的包。nvm則是保留每一個版本的副本,並將NodeJS的系統路徑指向.nvm維護的沙箱地址。

從處理上,nvm顯得更輕量和高效,但是需要修改系統的PATH,這一步nvm指令碼會自動完成。n則無需入侵系統路徑,但每次修改時候均需作業系統路徑,且此時最好使用sudo n執行,避免因許可權不足,拒絕向系統路徑複製。

由於nvm會修改PATH地址,所以如果同時預設安裝nvm和n,n會運轉不正常。一種方案是避免同時安裝,另外可以手動修改PATH,使預設的NodeJS路徑先於nvm的系統路徑,如修改PATH片段為:

/usr/local/bin:/Users/leon/.nvm/versions/node/v10.6.0/bin:

執行輔助 nodemon/npx

nodemon是一個執行器,意義在於,如果版本變化或者程式變化,無需重新啟動。這在開發時候非常有用。

nodemon還可以指定執行的埠,如:

nodemon ./server.js localhost 8080

除了控制NodeJS包之外,nodemon還可以控制非NodeJS指令碼,比如:nodemon --exec "python -v" ./app.py,將監控app.py的內容,並在最開始以及發生變化時候,呼叫python -v進行解析。當然,如果你的app.py指定了shebang,也可以不需指定解析函式。

NodeJS和命令列程式

nodemon有很多靈活的配置,通過這些配置,可以實現環境變數設定、延遲啟動、命令執行、監控定製副檔名、優雅重啟、事件監聽等功能。做法是在需要這些配置的目錄下,提供相關的配置nodemon.json,也可以在package.json中通過nodemonConfig欄位指明。

這裡是官方提供的一份配置檔案的樣例,供讀者參考。

再來說說npx。什麼是npx呢?簡單說,就是找到並執行一個包,並且“用完即走”。

這裡有兩層意思:

  1. 找到。從哪裡找:先是當前的依賴,然後是PATH,還找不到就到網上找來安裝。
  2. 用完即走。即使從網上安裝的,執行完就會刪掉,不會留下執行的包。 讀者可以試著執行下:npx github:piuccio/cowsay "awesome npx"體驗下。

這實在是居家旅行、開發除錯的利器。比如我要在當前目錄下開一個http服務,可以直接執行:npx http-server

NodeJS和命令列程式

之後就可以直接在瀏覽器訪問這個地址進行除錯了。

另外,如果你需要臨時用一個老版本的node來執行某個指令碼,也可以祭出npx,這個node會被臨時安裝、臨時使用、用完即走。

npx -p node@6 npm init

切換NodeJS登錄檔 nrm/yrm

nrm/yrm維護了一個列表,包括npm主站和其他映象。可以使用nrm/yrm use 快速切換,以達到最快的下載速度。nrm維護的是npm的登錄檔,yrm維護的是yarn登錄檔。

NodeJS和命令列程式

輔助編寫NodeJS包

除了直接用大神們寫好的命令之外,我們也可以按照自己的需求定製自己需要的NodeJS包。我們知道,命令列其實也是一種人機互動,因此,互動上有很多可以借鑑的效果。編寫者只需將包倒入就可以使用這些互動效果。這裡筆者給大家推薦幾個包

命令列引數讀取 commander

命令列的一個特點就是根據引數的不同調整執行策略。然而處理命令列輸入以及驗證是一個非常繁瑣的事情。為此,TJ大神曾經創立了commander包。最基礎的用法如下:


var program = require('commander');

program
  .version('0.1.0')
  .option('-p, --peppers', 'Add peppers')
  .option('-P, --pineapple', 'Add pineapple')
  .option('-b, --bbq-sauce', 'Add bbq sauce')
  .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
  .parse(process.argv);

console.log('you ordered a pizza with:');
if (program.peppers) console.log('  - peppers');
if (program.pineapple) console.log('  - pineapple');
if (program.bbqSauce) console.log('  - bbq');
console.log('  - %s cheese', program.cheese);

複製程式碼

預設地,commander會自動建立-h的幫助檔案,即利用每一個option的輸入產生幫助文案。

NodeJS和命令列程式

使用者的每一個輸入,都會放置在program對應option長名的欄位的駝峰形式上,如果沒有提供長名,則放在短名欄位上。上例中,如使用: testcommander -p 111 -P 222 -b 333則依次儲存在programpepperspineapplebbqSauce上。

同時,commander提供多種驗證方式,如正規表示式:

program.option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')

則指定只能輸入特定的值。

同時,commander提供一個方案,允許使用者設定子命令。commander稱之為Git風格的子命令。


var program = require('commander');

program
  .version('0.1.0')
  .command('install [name]', 'install one or more packages')
  .command('search [query]', 'search with optional query')
  .command('list', 'list packages installed', {isDefault: true})
  .parse(process.argv);

複製程式碼

這個例子中,假設命令列名字為pm,則當使用者輸入pm-installpm-searchpm-list時候,commander會嘗試在入口檔案的同一級目錄找到installsearchlist,並交給這個檔案去執行。

進度條 progress

在編寫web程式時候,大家經常會展示一個進度條。用以緩解使用者在等待時候的焦慮。其實在命令列程式中也會有這種互動方式。比如wget就會在下載過程中給出進度提示。

在NodeJS中也有這樣的效果可以使用。這就是progress包。下面的程式碼,執行結果是下載CentOS安裝盤。在下載之中,會實時列印進度

const ProgressBar = require("progress")
const request = require("request")
const progress = require("request-progress")
const fs = require("fs")

const download = (url, headers, target, totalSize) => {
    let percent = 0

    const bar = new ProgressBar('下載中: ├:bar┤ 完成:percent 預估完成時間:eta秒 用時:elapseds', {
        total: 100,
        complete: "█",
        incomplete: "─",
        width: 60
    })

    let opt = {
        headers,
        url: url
    }

    return new Promise((resolve, reject) => {
        progress(request.get(opt))
            .on('progress', function (state) {
                let progressFix = ((state.percent) * 100).toFixed(2)
                delta = progressFix - percent
                bar.tick(delta)
                percent = progressFix
            })
            .on("error", () => {
                return reject()
            })
            .on('end',  () => {
                bar.tick(100 - percent)
                console.log('\n')
                return resolve(target)
            })
            .pipe(fs.createWriteStream(target));
    })
}

const foo = {
    getHeaders: () => {
        const headers = {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Charset': 'UTF-8,*;q=0.5',
            'Accept-Encoding': 'gzip,deflate,sdch',
            'Accept-Language': 'en-US,en;q=0.8',
            'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0'
        }

        return Object.assign({}, headers)
    },

    download: function (url, target, totalSize){
        let headers = this.getHeaders()
        headers = Object.assign(headers)

        download(url, headers, target, totalSize)
    }
}


foo.download("http://mirrors.cmich.edu/centos/7.6.1810/isos/x86_64/CentOS-7-x86_64-DVD-1810.iso",
    "CentOS-7-x86_64-DVD-1810.iso", 4508876.8
    )

複製程式碼

執行的結果如圖:

NodeJS和命令列程式

這個包的核心就是根據內建和自定義的token在命令列列印出相應的字元,用以完成互動。

互動著色 chalk

chalk是一個命令列互動的著色工具。在命令列支援的情況下,可以支援最多16位色域(前提是命令列終端可以支援)。一般可以配合console.log使用,如:

const chalk = require('chalk');
const log = console.log;

// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
複製程式碼

筆者曾經做過一個在命令列下顯示圖片的程式,就是利用的chalk和console.log進行的配合。

NodeJS和命令列程式

互動式問答 inquirer

在需要不斷的同使用者進行互動式問答,並根據使用者的輸入進行驗證和路徑選擇,這個時候inquirer是非常趁手的工具。它內建了單選、多選、問答等多種互動方式。大家可以感受下:

NodeJS和命令列程式

NodeJS和命令列程式

NodeJS和命令列程式

NodeJS和命令列程式

NodeJS和命令列程式

NodeJS和命令列程式

甚至可以通過外掛實現suggest

NodeJS和命令列程式

vue框架的腳手架vue-cli是一個使用inquire的絕佳案例,讀者可以通過閱讀原始碼,感受下大神出神入化的使用。

小圖示 ora

ora列印出一個優雅的文字小圖示,用於在各種情況下給出使用者優雅而清晰的提示。用法很簡單:

const ora = require('ora');

const spinner = ora('Loading unicorns').start();

setTimeout(() => {
	spinner.color = 'yellow';
	spinner.text = 'Loading rainbows';
}, 1000);
複製程式碼

NodeJS和命令列程式

命令列玩瀏覽器 puppeteer

puppeteer是谷歌開發的無頭瀏覽器,使得命令列亦可操作瀏覽器,並能根據瀏覽器的執行結果進行進一步操控。因為puppeteer源自官方,所以之前類似專案PhantomJS的開發者決定不再更新PhantomJS。

目前puppeteer已經廣泛用於前端測試,端對端測試,以及爬蟲。

鑑於篇幅無法展開介紹,讀者可以參考其官方文件。同時,奇舞週刊中黃小璐老師的的這篇文章以及李光釗老師的這篇文章都曾經介紹過puppeteer的使用。

釋出NodeJS包

寫好的NodeJS包需要釋出出去,才能給大家使用。npm publish就是為了這個需求而產生的。為了釋出你需要在npm上註冊使用者,並登入,然後釋出就好了。npm的詳情頁面以及各個映象會在一段時間內自動更新。

如果你的NodeJS包,是使用尚未廣泛支援的語法寫成的。那麼需要在package.json的script欄位加入prepublish命令,呼叫babel等預編譯器處理,使得程式可以有更多的相容性。

對於希望使用者在全域性使用的命令,要注意在根目錄寫好入口,一般是在package.json中的bin欄位,指定入口檔案。在安裝時,如果是全域性安裝,npm將會使用符號連結把這些檔案連結到prefix/bin,如果是本地安裝,會連結到./node_modules/.bin/。

除了通常的包,還有一種是帶有scope的包,vue-cli的3.0版本就是@vue開頭的。這個scope是組織的名字。每一個帶有scope的包有公有和私有之分,私有的需要付費給npm。

目前npm的讀寫許可權策略如下:

NodeJS和命令列程式

如果是個人,可以考慮增加公有的名稱空間。如果是企業付費使用者,你在釋出相關包之前,需要申請成為這個scope的member。

對公有scope,首先將包的name改為@scope名字/包名,同時,在釋出時,使用npm publish --access public即可。

小結

本文簡述了命令列的意義和優勢,介紹瞭解釋型命令列的執行機制,同時介紹了幾個NodeJS相關的命令列工具,推薦了幾款撰寫命令列程式常用的包,最後,概述了釋出包和使用scope的釋出情況。希望給大家的NodeJS命令列相關開發和技術選型,提供一些有用的幫助。

關於奇舞週刊

《奇舞週刊》是360公司專業前端團隊「奇舞團」運營的前端技術社群。關注公眾號後,直接傳送連結到後臺即可給我們投稿。

NodeJS和命令列程式

相關文章