npm
裏有個 http-server
的模塊,是一個簡單的、零配置的 HTTP 服務,它很是強大,同時很是簡單,能夠方便的幫助咱們開啓本地服務器,以及局域網共享,能夠用來作測試,開發,學習時的環境配置,咱們本節就模擬 http-server
實現一個本身的啓動本地服務的命令行工具。css
http-server
服務器經過命令行啓動,使用時須要安裝,安裝命令以下:html
npm install http-server -g前端
啓動本地服務器時在根目錄下執行下面命令便可:node
http-server [path] [option]npm
path
默認狀況下是 ./public
,不然是 ./
,啓動後能夠經過 http://localhost:8080 來訪問服務器,options
爲其餘參數, npm
官方文檔 www.npmjs.com/package/htt… 有詳細說明。json
當經過瀏覽器訪問 http://localhost:8080 之後,會將咱們服務器根目錄的目錄結構顯示在瀏覽器頁面上,當點擊文件夾時,能夠繼續顯示內部的文件和文件夾,當點擊文件時會直接經過服務器訪問文件,並將文件內容顯示在瀏覽器頁面上。數組
chalk
模塊是用來控制命令行輸出的文字顏色的第三方模塊,使用前須要安裝,安裝命令以下:瀏覽器
npm install chalk緩存
chalk
模塊的用法以下,模塊支持的顏色和更多的 API 能夠在 npm
官方文檔 www.npmjs.com/package/cha… 中查看。bash
const chalk = require("chalk");
// 在命令行打印綠色和紅色的 hello
console.log(chalk.green("hello"));
console.log(chalk.red("hello"));複製代碼
在命令行窗口輸入 node chalk-test.js
查看命令行打印 hello
的顏色。
debug
模塊能夠匹配當前環境變量 DEBUG
的值並輸出相關信息,做用在於命令行工具能夠根據不一樣狀況輸出的信息進行調試,是第三方模塊,使用前需安裝,命令以下。
npm install debug
debug
的簡單使用以下,若是想了解更詳細的 API 能夠在 npm
官方文檔 www.npmjs.com/package/deb… 中查看。
const debug = require("debug")("hello");
debug("hi panda");複製代碼
當咱們在命令行中執行 node debug-test1.js
時發現命令窗口什麼也沒有打印,那是由於當前根目錄的環境變量 DEBUG
的值必須和咱們設置的 hello
相匹配纔會打印相關信息。
設置環境變量,Window 能夠經過 set DEBUG=hello
設置,Mac 能夠經過 export DEBUG=hello
設置,設置環境變量後再次執行 node debug-test.js
,咱們會發現命令行打印出了下面內容。
hello hi panda +0ms
其中 hello
爲咱們設置 DEBUG
環境變量的值,hi panda
爲調試方法 debug
方法打印的信息,+0ms
爲距離上次執行的間隔時間。
const debugA = require("debug")("hello:a");
const debugB = require("debug")("hello:b");
debugA("hi panda");
debugB("hello panda");複製代碼
上面的代碼目的是可讓咱們不一樣的 debug
方法能夠匹配不一樣的環境變量,因此須要從新將環境變量的值設置爲 hello:*
,這樣再次執行 node debug-test2.js
發現命令窗口打印了以下內容。
hello:a hi panda +0ms
hello:b hello panda +0ms
使用 debug
的好處就是能夠在開發的時候打印一些調試用的信息,在開發完成後由於匹配不到環境變量,這些信息就會被隱藏。
commander
是著名的 Node 大神 TJ
的 「做品」,是一個開發命令行工具的解決方案,提供了用戶命令行輸入和參數解析的強大功能,commander
是第三方模塊,使用時須要安裝,命令以下。
npm install commander
基本用法以下:
let commander = require("commander");
// 解析 Node 進程執行時的參數
commander.version("1.0.0").parse(process.argv);複製代碼
上面文件中 version
方法表明當前執行文件模塊的版本,parse
爲解析參數爲當前命令行進程參數的方法,process.argv
爲參數集合(數組),第一個參數爲執行的 node.exe
文件的絕對路徑,第二個參數爲當前執行文件的絕對路徑,後面爲經過命令行傳入的參數,如 --host
、--port
等。
在命令行執行 node commander-test.js --help
時默認會在命令行輸出以下信息:
Usage: [options]
Options:
-V, --version output the version number
-h, --help output usage information
固然在咱們的命令行工具中,參數不僅 --version
和 --help
兩個,咱們更但願更多的參數更多的功能,而且可定製的描述信息,使用案例以下。
let commander = require("commander");
// 解析 Node 進程執行時的參數
commander
.version("1.0.0")
.usage("[options]")
.option('-p, --port <n>', 'server port')
.option('-o, --host <n>', 'server host')
.option('-d, --dir <n>', 'server dir')
.parse(process.argv);
console.log(commander.port); // 3000
console.log(commander.host); // localhost
console.log(commander.dir); // public複製代碼
在執行命令 node commander-test2.js --help
後會在命令窗口輸出以下信息:
Usage: yourname-http-server [options]
Options:
-V, --version output the version number
-p, --port server port
-o, --host server host
-d, --dir server dir
-h, --help output usage information
usage
方法可讓咱們詳細的定製參數的類型和描述,option
方法可讓咱們添加執行 --help
指令時打印的命令以及對應的描述信息。
執行下面命令:
node commander-test2.js --port 3000 --host localhost --dir public
執行命令後咱們發現其實給咱們的參數掛在了 commander
對象上,方便咱們取值。
在咱們使用別人的命令行工具時會發如今上面輸出信息的時候常常會在下面輸出 How to use
的列表,更詳細的描述了每條命令的做用及用法。
let commander = require("commander");
// 必須寫到 parse 方法的前面
commander.on("--help", function () {
console.log("\r\n How to use:")
console.log(" yourname-http-server --port <val>");
console.log(" yourname-http-server --host <val>");
console.log(" yourname-http-server --dir <val>");
});
// 解析 Node 進程執行時的參數
commander
.version("1.0.0")
.usage("[options]")
.option('-p, --port <n>', 'server port')
.option('-o, --host <n>', 'server host')
.option('-d, --dir <n>', 'server dir')
.parse(process.argv);複製代碼
再次執行命令 node commander-test2.js --help
後會在命令窗口輸出以下信息:
Usage: yourname-http-server [options]
Options:
-V, --version output the version number
-p, --port server port
-o, --host server host
-d, --dir server dir
-h, --help output usage information
How to use:
yourname-http-server --port
yourname-http-server --host
yourname-http-server --dir
以上是 commander
模塊的基本用法,如想了解更詳細的 API 和使用案例能夠到 npm
官方文檔查看,地址以下 www.npmjs.com/package/com… 。
static
|- bin
| |- yourname-http-server.js
|- public
| |- css
| | |- style.css
| |- index.html
| |- 1.txt
|- tests
| |- chalk-test.js
| |- commander-test1.js
| |- commander-test2.js
| |- commander-test3.js
| |- debug-test1.js
| |- debug-test2.js
|- config.js
|- index.html
|- index.js
|- package-lock.json
|- package.json複製代碼
在啓動靜態服務的時候,咱們但願能夠經過命令行傳參的形式來定義當前啓動服務的主機名端口號,以及默認檢索的文件根目錄,因此須要配置文件來實現靈活傳參。
module.exports = {
port: 3000,
host: "localhost",
dir: process.cwd()
}複製代碼
在上面的配置中,默認端口號爲 3000
,默認主機名爲 localhost
,咱們設置默認檢索文件的根目錄爲經過命令行啓動服務器的目錄,而 process.cwd()
的值就是咱們啓動命令行執行命令的目錄的絕對路徑。
由於咱們的命令行工具啓動本地服務多是在系統的任意位置,或者指定啓動服務訪問的域,提升可配置性,而且要更方便給服務器擴展更多的方法處理不一樣的邏輯,因此須要建立一個 Server
類。
// 引入依賴
const http = require("http");
const url = require("url");
const path = require("path");
const fs = require("mz/fs");
const mime = require("mime");
const zlib = require("zlib");
const chalk = require("chalk");
const ejs = require("ejs");
const debug = require("debug")("http:a");
// 引入配置文件
const config = require("./config");
// 讀取模板文件
const templateStr = fs.readFileSync(path.join(__dirname, "index.html"), "utf8");
class Server {
constructor() {
this.config = config; // 配置
this.template = templateStr; // 模板
}
}複製代碼
咱們在上面代碼中引入了 config.js
配置文件,讀取了用於啓動服務後展現頁面 index.html
的內容,並都掛在了 Server
類的實例上,目的是方便內部的方法使用以及達到不輕易操做全局變量的目的。
後面爲了方便代碼的拆分,咱們將原型上的方法統一使用 Server.prototype.xxx
的方式來書寫,實際的案例都是寫在 Server
類裏面的。
Server.prototype.start = function () {
// 建立服務
const server = http.createServer(this.handleRequest.bind(this));
// 從配置中解構端口號和主機名
let { port, host } = this.config;
// 啓動服務
server.listen(port, host, () => {
debug(`server start http://${host}:${chalk.green(port)}`);
});
}複製代碼
在 start
方法中建立了服務,在啓動服務時只須要建立 Server
的實例並調用 start
方法,因爲服務的回調中會處理不少請求響應的邏輯,會致使 start
方法的臃腫,因此將服務的回調函數抽取成 Server
類的一個實例方法 handleRequest
,須要注意的是 handleRequest
內部的 this
指向須要咱們修正。
在啓動服務時咱們根據配置能夠靈活的設置服務的地址,當設置 host
後,服務將只能經過 host
的值做爲主機名的地址訪問靜態服務器,啓動服務的提示咱們經過匹配環境變量 DEBUG
的 debug
方法來打印,並將端口號設置成綠色。
在實現 handleRequest
以前咱們應該瞭解要實現的功能,在 http-server
中,若是訪問的服務地址路徑後面指定具體要訪問的文件,而且當前啓動服務根目錄按照訪問路徑能夠查找到文件,將文件內容讀取後響應給客戶端,若是沒指定文件,應該檢索當前啓動服務根目錄或默認設置的目錄結構,並將文件的結構經過模板渲染成超連接後將頁面響應給客戶端,再次點擊頁面的上的連接,若是是文件,直接讀取並響應文件內容,若是是文件夾,則繼續檢索內部結構經過模板渲染成頁面。
Server.prototype.handleRequest = async function (req, res) {
// 獲取訪問的路徑,默認爲 /
this.pathname = url.parse(req.url, true).pathname;
// 將訪問的路徑名轉換成絕對路徑,取到的 dir 就是絕對路徑
this.realPath = path.join(this.config.dir, this.pathname);
debug(realPath); // 打印當前訪問的絕對路徑,用於調試
try {
// 獲取 statObj 對象,若是 await 同步使用 try...catch 捕獲非法路徑
let statObj = await fs.stat(this.realPath);
if (statObj.isFile()) {
// 若是是文件,直接返回文件內容
this.sendFile(req, res, statObj);
} else {
// 若是是文件夾則檢索文件夾經過模板渲染後返回頁面
this.sendDirDetails(req, res, statObj);
}
} catch (e) {
// 若是路徑非法,發送錯誤響應
this.sendError(req, res, e);
}
}複製代碼
handleRequest
因爲內部須要使用異步操做獲取 statObj
對象,因此咱們使用了 async
函數,爲了函數內部可使用 await
避免異步回調嵌套,因爲 await
會等待到異步執行完畢後繼續向下執行,咱們可使用 try...catch...
捕獲非法的訪問路徑,並作出錯誤響應。
若是路徑合法,咱們須要檢測訪問路徑對應的是文件仍是文件夾,若是是文件則執行響應內容的邏輯,是文件夾執行檢索文件夾渲染內部文件列表返回頁面的邏輯。
因此咱們將錯誤處理邏輯、響應文件內容邏輯和返回文件夾詳情頁面的邏輯分別抽離成 Server
類的三個實例方法 sendError
、sendFile
和 sendDirDetails
,使得 handleRequest
方法邏輯清晰且不那麼臃腫。
在服務器處理不一樣的請求和響應時可能須要處理不一樣的錯誤,這些錯誤的不一樣就是捕獲錯誤對象的不一樣,因此咱們的 sendError
方法爲了更方便的或取請求參數、處理響應以及更好的複用,將參數設置爲請求對象、響應對象和錯誤對象。
Server.prototype.sendError = function (req, res, err) {
// 打印錯誤對象,方便調試
console.log(chalk.red(err));
// 設置錯誤狀態碼並響應 Not Found
res.statusCode = 404;
res.end("Not Found");
}複製代碼
在渲染文件夾詳情以前咱們首先要作的就是異步讀取文件目錄,因此咱們一樣使用 async
函數來實現,NodeJS 中有不少渲染頁面的模板,咱們本次使用 ejs
,語法簡單,比較經常使用,ejs
爲第三方模塊,使用前需安裝,更詳細的用法可參照 npm
官方文檔 www.npmjs.com/package/ejs。
npm install ejs
sendDirDetails
的參數爲請求對象、響應對象和 statObj
。
Server.prototype.sendDirDetails = async function (req, res, statObj) {
// 讀取當前文件夾
let dirs = await fs.readdir(this.realPath);
// 構造模板須要的數據
dirs = dirs.map(dir => ({ name: dir, path: path.join(this.pathname, dir)}));
// 渲染模板
let pageStr = ejs.render(this.template, { dirs });
// 響應客戶端
res.setHeader("Content-Type", "text/html;charset=utf8");
res.end(pageStr);
}複製代碼
還記得 Server
類的實例屬性 template
存儲的就是咱們的模板(字符串),裏面寫的就是 ejs
的語法,咱們使用 ejs
模塊渲染的 render
方法能夠將模板中的 JS 執行,並用傳給該方法的參數的值替換掉模板中的變量,返回新的字符串,咱們直接將字符串響應給客戶端便可。
path
爲超連接標籤要跳轉的路徑,若是直接使用 dir
的值,多級訪問仍是會在根目錄去查找,因此路徑非法會返回 Not Found
,咱們須要在每次訪問的時候都將上一次訪問的路徑與當前訪問的文件夾或文件名進行拼接,保證路徑的正確性。
上面已經知道了該怎樣使用 ejs
對模板進行渲染,也對模板構造了數據,接下來就是使用 ejs
的語法編寫咱們的模板內容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Server</title>
</head>
<body>
<%dirs.forEach(function (item) {%>
<li><a href="<%=item.path%>"><%=item.name%></a></li>
<%})%>
</body>
</html>複製代碼
<% %>
包裹,使用 <%= %>
輸出變量。
因爲都是根據路徑查找或操做文件目錄並作出響應,sendFile
方法與 sendDirDetails
方法的參數相同,分別爲 req
、res
和 statObj
。
Server.prototype.sendFile = function (req, res, statObj) {
// 設置和處理緩存
if (this.cache(req, res, statObj)) {
res.statusCode = 304;
return res.end();
}
// 建立可讀流
let rs = fs.createReadStream(this.realPath);
// 響應文件類型
res.setHeader("Content-Type", `${mime.getType(this.realPath)};charset=utf8`);
// 壓縮
let zip = this.compress(req, res, statObj);
if (zip) return rs.pipe(zip).pipe(res);
// 處理範圍請求
if (this.range(req, res, statObj)) return;
// 響應文件內容
rs.pipe(res);
}複製代碼
其實上面的方法經過在根目錄執行 node index.js
啓動服務後,經過咱們默認配置的地址訪問服務器,表面上就已經實現了 http-server
的功能,可是咱們爲了服務器的性能和功能更強大,又在這基礎上實現了緩存策略、服務器壓縮和處理範圍請求的邏輯。
咱們將上面的三個功能分別抽離成了 Server
類的三個原型方法,cache
、compress
和 range
,而且這三個方法的參數都爲 req
、res
和 statObj
。
咱們本次的緩存兼容 HTTP 1.0
和 HTTP 1.1
版本,而且同時使用強制緩存和協商緩存共同存在的策略。
Server.prototype.cache = function (req, res, statObj) {
// 建立協商緩存標識
let etag = statObj.ctime.toGMTString() + statObj.size;
let lastModified = statObj.ctime.toGMTString();
// 設置強制緩存
res.setHeader("Cache-Control", "max-age=30");
res.setHeader("Expires", new Date(Date.now() + 30 * 1000).toUTCString());
// 設置協商緩存
res.setHeader("Etag", etag);
res.setHeader("Last-Modified", lastModified);
// 獲取協商緩存請求頭
let { "if-none-match": ifNodeMatch, "if-modified-since": ifModifiedSince } = req.headers;
if (etag !== ifNodeMatch && lastModified !== ifModifiedSince) {
return false;
} else {
return true;
}
}複製代碼
304
狀態碼,若是協商緩存失效則讀取文件內容返回瀏覽器。
爲了減小文件數據在傳輸過程當中消耗的流量和時間,咱們在瀏覽器支持解壓的狀況下使用服務器壓縮功能,瀏覽器會在請求時默認發送請求頭 Accept-Encoding
通知咱們的服務器當前支持的壓縮格式,咱們要作的就是按照壓縮格式的優先級進行匹配,按照最高優先級的壓縮格式進行壓縮,將壓縮後的數據返回,並經過響應頭 Content-Encoding
通知瀏覽器當前的壓縮格式(壓縮流的本質爲轉化流)。
Server.prototype.compress = function (req, res, statObj) {
// 獲取瀏覽器支持的壓縮格式
let encoding = req.headers["accept-encoding"];
// 支持 gzip 使用 gzip 壓縮,支持 deflate 使用 deflate 壓縮
if (encoding && encoding.match(/\bgzip\b/)) {
res.setHeader("Content-Encoding", "gzip");
return zlib.createGzip();
} else if (encoding && encoding.match(/\bdeflate\b/)) {
res.setHeader("Content-Encoding", "deflate");
return zlib.createDeflate();
} else {
return false; // 不支持壓縮返回 false
}
}複製代碼
當瀏覽器支持壓縮時,compress
方法返回的爲優先級最高壓縮格式的壓縮流,不支持返回 false
,存在壓縮流,則將數據壓縮並響應瀏覽器,與不壓縮響應不一樣的是,須要使用壓縮流將可讀流轉化爲可寫流寫入響應 res
中,因此可讀流執行了兩次 pipe
方法。
range
方法處理的場景爲客戶端發送請求只想獲取文件的某個範圍的數據,此時經過 range
方法讀取文件範圍對應的內容響應給客戶端,經過響應頭 Accept-Ranges
通知瀏覽器當前響應範圍請求,經過響應頭 Content-Range
通知客戶端響應的範圍以及文件的總字節數。
Server.prototype.range = function (req, res, statObj) {
// 獲取 range 請求頭
let range = req.headers["range"];
if (range) {
// 獲取範圍請求的開始和結束位置
let [, start, end] = range.match(/(\d*)-(\d*)/);
// 處理請求頭中範圍參數不傳的問題
start = start ? ParseInt(start) : 0;
end = end ? ParseInt(end) : statObj.size - 1;
// 設置範圍請求響應
res.statusCode = 206;
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Range", `bytes ${start}-${end}/${statObj.size}`);
fs.createReadStream(this.realPath, { start, end }).pipe(res);
return true;
} else {
return false;
}
}複製代碼
range
方法默認返回值爲布爾值,當不是範圍請求時返回值爲 false
,則直接向下執行 sendFile
中的代碼,正常讀取文件所有內容並響應給瀏覽器,若是是範圍請求則會處理範圍請求後在直接結束後返回 true
,會在 sendFile
中直接 return
,再也不向下執行。
http-server
其實是經過命令行啓動、並傳參的,咱們須要將咱們的程序與命令行關聯,關聯命令行只需如下幾個步驟。
首先,在根目錄 package.json
文件中加入 bin
字段,值爲對象,對象內屬性爲命令名稱,值爲對應執行文件的路徑。
{
"name": "yourname-http-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"chalk": "^2.4.1",
"commander": "^2.17.1",
"debug": "^3.1.0",
"ejs": "^2.6.1",
"mime": "^2.3.1",
"mz": "^2.7.0"
},
"bin": {
"yourname-http-server": "bin/yourname-http-server.js"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}複製代碼
其次,在 yourname-http-server.js
文件中首行加入註釋 #! /usr/bin/env node
,在命令行執行命令時,默認會以 Node 執行 yourname-http-server.js
文件。
最後,想要使用咱們的命令啓動 yourname-http-server.js
文件,則須要將這條命令鏈接到全局(與 -g 安裝效果相同),在當前根目錄下執行如下命令。
npm link
yourname-http-server
時,Node 會默認執行 yourname-http-server.js
文件。
咱們如今知道在命令行執行命令後用 Node 啓動的文件爲 yourname-http-server.js
,在啓動文件時咱們應該啓動咱們的服務器,並結合 commander
模塊的參數解析,則須要用命令行傳遞的參數替換掉 config.js
中的默認參數。
const commander = require("commander");
const Server = require("../index");
// 增長 How to use
commander.on("--help", function () {
console.log("\r\n How to use: \r\n")
console.log(" zf-server --port <val>");
console.log(" zf-server --host <val>");
console.log(" zf-server --dir <val>");
});
// 解析 Node 進程執行時的參數
commander
.version("1.0.0")
.usage("[options]")
.option("-p, --port <n>", "server port")
.option("-o, --host <n>", "server host")
.option("-d, --dir <n>", "server dir")
.parse(process.argv);
// 建立 Server 實例傳入命令行解析的參數
const server = new Server(commander);
// 啓動服務器
server.start();複製代碼
咱們以前把 config.js
的配置直接掛在了 Server
實例的 config
屬性上,建立服務使用的參數也是直接從該屬性上獲取的,所以咱們要用 commander
對象對應的參數覆蓋實例上 config
的參數,因此在建立 Server
實例時傳入了 commander
對象,下面稍微修改 Server
類的部分代碼。
class Server {
constructor(options) {
// 經過解構賦值將 options 的參數覆蓋 config 的參數
this.config = { ...config, ...options }; // 配置
this.template = templateStr; // 模板
}
}複製代碼
執行下面命令,並經過瀏覽器訪問 http://127.0.0.1:4000 來測試服務器功能。
yourname-http-server --port 4000 --host 127.0.0.1
因爲 JS 是單線程的,在命令行輸入命令啓動服務的同時不能去作其餘的事,此時要靠多進程來幫助咱們打開瀏覽器,在 JS 中開啓一個子進程來打開瀏覽器。
const commander = require("commander");
const Server = require("../index");
// 增長 How to use
commander.on("--help", function () {
console.log("\r\n How to use: \r\n")
console.log(" zf-server --port <val>");
console.log(" zf-server --host <val>");
console.log(" zf-server --dir <val>");
});
// 解析 Node 進程執行時的參數
commander
.version("1.0.0")
.usage("[options]")
.option("-p, --port <n>", "server port")
.option("-o, --host <n>", "server host")
.option("-d, --dir <n>", "server dir")
.parse(process.argv);
// 建立 Server 實例傳入命令行解析的參數
const server = new Server(commander);
// 啓動服務器
server.start();
// ********** 如下爲新增代碼 **********
let { exec } = require("child_process");
// 判斷系統執行不一樣的命令打開瀏覽器
let systemOrder = process.platform === "win32" ? "start" : "open";
exec(`${systemOrder} http://${commander.localhost}:${commander.port}`);
// ********** 以上爲新增代碼 **********複製代碼
在發佈咱們本身實現的 npm
模塊以前須要先作一件事,就是解除當前模塊與全局環境的 link
,咱們能夠經過兩種方式,第一種方式是直接到系統存儲命令文件的文件夾刪除模塊對應命令的 yourname-http-server.cmd
文件,第二種方式是在模塊根目錄啓動命令行並輸入以下命令。
npm unlink
輸入下面命令進行登陸:
npm login
登陸成功後執行下面命令進行發佈:
npm publish
發佈成功後再次使用本身的模塊須要經過 npm
下載並全局安裝,命令以下:
npm install yourname-http-server -g
任意文件夾內打開命令行,並執行命令啓動服務驗證。
nrm
切換過其餘的源,必須切換回 npm
,再進行登陸和發佈操做。
其實咱們實現的靜態服務器核心還在於處理請求和響應的邏輯上,只是再也不手動輸入 node
命令啓動,而是藉助一些第三方模塊關聯到了命令行並經過命令啓動,開發其餘類型的命令行工具也須要藉助這些第三方模塊,靜態服務器只是其中之一,其實相似這種命令行工具在開發的角度來說屬於 「造輪子」 系列,能夠獨立開發命令行工具是一個成爲前端架構的必備技能,但願經過本篇文章能夠了解命令行工具的開發流程,在將來 「造輪子」 的道路上提供幫助。