手寫 npm 裡的 http-server

煎蛋面__cq發表於2018-10-09

npm 裡有個 http-server 的模組,是一個簡單的、零配置的 HTTP 服務,它非常強大,同時非常簡單,可以方便的幫助我們開啟本地伺服器,以及區域網共享,可以用來做測試,開發,學習時的環境配置,我們本節就模擬 http-server 實現一個自己的啟動本地服務的命令列工具。

http-server 使用

http-server 伺服器通過命令列啟動,使用時需要安裝,安裝命令如下:

npm install http-server -g

啟動本地伺服器時在根目錄下執行下面命令即可:

http-server [path] [option]

path 預設情況下是 ./public,否則是 ./,啟動後可以通過 http://localhost:8080 來訪問伺服器,options 為其他引數, npm 官方文件 www.npmjs.com/package/htt… 有詳細說明。

當通過瀏覽器訪問 http://localhost:8080 以後,會將我們伺服器根目錄的目錄結構顯示在瀏覽器頁面上,當點選資料夾時,可以繼續顯示內部的檔案和資料夾,當點選檔案時會直接通過伺服器訪問檔案,並將檔案內容顯示在瀏覽器頁面上。

實現命令列工具依賴的模組

1、chalk 模組

chalk 模組是用來控制命令列輸出的文字顏色的第三方模組,使用前需要安裝,安裝命令如下:

npm install chalk

chalk 模組的用法如下,模組支援的顏色和更多的 API 可以在 npm 官方文件 www.npmjs.com/package/cha… 中檢視。

檔案位置:~static/tests/staticchalk-test.js
const chalk = require("chalk");

// 在命令列列印綠色和紅色的 hello
console.log(chalk.green("hello"));
console.log(chalk.red("hello"));複製程式碼

在命令列視窗輸入 node chalk-test.js 檢視命令列列印 hello 的顏色。

2、debug 模組

debug 模組可以匹配當前環境變數 DEBUG 的值並輸出相關資訊,作用在於命令列工具可以根據不同情況輸出的資訊進行除錯,是第三方模組,使用前需安裝,命令如下。

npm install debug

debug 的簡單使用如下,如果想了解更詳細的 API 可以在 npm 官方文件 www.npmjs.com/package/deb… 中檢視。

檔案位置:~static/tests/debug-test1.js —— 用法 1
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 為距離上次執行的間隔時間。

檔案位置:~static/tests/debug-test2.js —— 用法 2
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 的好處就是可以在開發的時候列印一些除錯用的資訊,在開發完成後因為匹配不到環境變數,這些資訊就會被隱藏。

3、commander 模組

commander 是著名的 Node 大神 TJ 的 “作品”,是一個開發命令列工具的解決方案,提供了使用者命令列輸入和引數解析的強大功能,commander 是第三方模組,使用時需要安裝,命令如下。

npm install commander

基本用法如下:

檔案位置:~static/tests/commander-test1.js
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 兩個,我們更希望更多的引數更多的功能,並且可定製的描述資訊,使用案例如下。

檔案位置:~static/tests/commander-test2.js
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 的列表,更詳細的描述了每條命令的作用及用法。

檔案位置:~static/tests/commander-test3.js
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…

實現靜態服務的功能

1、檔案目錄

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複製程式碼

2、配置檔案

在啟動靜態服務的時候,我們希望可以通過命令列傳參的形式來定義當前啟動服務的主機名埠號,以及預設檢索的檔案根目錄,所以需要配置檔案來實現靈活傳參。

檔案位置:~static/config.js
module.exports = {
    port: 3000,
    host: "localhost",
    dir: process.cwd()
}複製程式碼

在上面的配置中,預設埠號為 3000,預設主機名為 localhost,我們設定預設檢索檔案的根目錄為通過命令列啟動伺服器的目錄,而 process.cwd() 的值就是我們啟動命令列執行命令的目錄的絕對路徑。

3、建立伺服器 Server 類

因為我們的命令列工具啟動本地服務可能是在系統的任意位置,或者指定啟動服務訪問的域,提高可配置性,並且要更方便給伺服器擴充套件更多的方法處理不同的邏輯,所以需要建立一個 Server 類。

檔案位置:~static/index.js —— 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 類的例項上,目的是方便內部的方法使用以及達到不輕易操作全域性變數的目的。

4、啟動伺服器的 start 方法

後面為了方便程式碼的拆分,我們將原型上的方法統一使用 Server.prototype.xxx 的方式來書寫,實際的案例都是寫在 Server 類裡面的。

檔案位置:~static/index.js —— start 方法
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 的值作為主機名的地址訪問靜態伺服器,啟動服務的提示我們通過匹配環境變數 DEBUGdebug 方法來列印,並將埠號設定成綠色。

5、服務回撥 handleRequest 方法

在實現 handleRequest 之前我們應該瞭解要實現的功能,在 http-server 中,如果訪問的服務地址路徑後面指定具體要訪問的檔案,並且當前啟動服務根目錄按照訪問路徑可以查詢到檔案,將檔案內容讀取後響應給客戶端,如果沒指定檔案,應該檢索當前啟動服務根目錄或預設設定的目錄結構,並將檔案的結構通過模板渲染成超連結後將頁面響應給客戶端,再次點選頁面的上的連結,如果是檔案,直接讀取並響應檔案內容,如果是資料夾,則繼續檢索內部結構通過模板渲染成頁面。

檔案位置:~static/index.js —— handleRequest 方法
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 類的三個例項方法 sendErrorsendFilesendDirDetails,使得 handleRequest 方法邏輯清晰且不那麼臃腫。

6、錯誤響應 sendError 方法

在伺服器處理不同的請求和響應時可能需要處理不同的錯誤,這些錯誤的不同就是捕獲錯誤物件的不同,所以我們的 sendError 方法為了更方便的或取請求引數、處理響應以及更好的複用,將引數設定為請求物件、響應物件和錯誤物件。

檔案位置:~static/index.js —— sendError 方法
Server.prototype.sendError = function (req, res, err) {
    // 列印錯誤物件,方便除錯
    console.log(chalk.red(err));

    // 設定錯誤狀態碼並響應 Not Found
    res.statusCode = 404;
    res.end("Not Found");
}複製程式碼

7、渲染目錄 sendDirDetails 方法

在渲染資料夾詳情之前我們首先要做的就是非同步讀取檔案目錄,所以我們同樣使用 async 函式來實現,NodeJS 中有很多渲染頁面的模板,我們本次使用 ejs,語法簡單,比較常用,ejs 為第三方模組,使用前需安裝,更詳細的用法可參照 npm 官方文件 www.npmjs.com/package/ejs

npm install ejs

sendDirDetails 的引數為請求物件、響應物件和 statObj

檔案位置:~static/index.js —— sendDirDetails 方法
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,我們需要在每次訪問的時候都將上一次訪問的路徑與當前訪問的資料夾或檔名進行拼接,保證路徑的正確性。

8、ejs 模板 index.html

上面已經知道了該怎樣使用 ejs 對模板進行渲染,也對模板構造了資料,接下來就是使用 ejs 的語法編寫我們的模板內容。

檔案位置:~static/index.html —— 模板
<!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>複製程式碼

模板中 JS 邏輯使用 <% %> 包裹,使用 <%= %> 輸出變數。

9、返回檔案內容 sendFile 方法

由於都是根據路徑查詢或操作檔案目錄並做出響應,sendFile 方法與 sendDirDetails 方法的引數相同,分別為 reqresstatObj

檔案位置:~static/index.js —— sendFile 方法
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 類的三個原型方法,cachecompressrange,並且這三個方法的引數都為 reqresstatObj

10、快取策略 cache 方法

我們本次的快取相容 HTTP 1.0HTTP 1.1 版本,並且同時使用強制快取和協商快取共同存在的策略。

檔案位置:~static/index.js —— cache 方法
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 狀態碼,如果協商快取失效則讀取檔案內容返回瀏覽器。

11、伺服器壓縮 compress 方法

為了減少檔案資料在傳輸過程中消耗的流量和時間,我們在瀏覽器支援解壓的情況下使用伺服器壓縮功能,瀏覽器會在請求時預設傳送請求頭 Accept-Encoding 通知我們的伺服器當前支援的壓縮格式,我們要做的就是按照壓縮格式的優先順序進行匹配,按照最高優先順序的壓縮格式進行壓縮,將壓縮後的資料返回,並通過響應頭 Content-Encoding 通知瀏覽器當前的壓縮格式(壓縮流的本質為轉化流)。

檔案位置:~static/index.js —— compress 方法
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 方法。

12、處理範圍請求 range 方法

range 方法處理的場景為客戶端傳送請求只想獲取檔案的某個範圍的資料,此時通過 range 方法讀取檔案範圍對應的內容響應給客戶端,通過響應頭 Accept-Ranges 通知瀏覽器當前響應範圍請求,通過響應頭 Content-Range 通知客戶端響應的範圍以及檔案的總位元組數。

檔案位置:~static/index.js —— 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,不再向下執行。

將靜態伺服器關聯到命令列

1、命令列啟動伺服器

http-server 實際上是通過命令列啟動、並傳參的,我們需要將我們的程式與命令列關聯,關聯命令列只需以下幾個步驟。

首先,在根目錄 package.json 檔案中加入 bin 欄位,值為物件,物件內屬性為命令名稱,值為對應執行檔案的路徑。

檔案位置:~static/package.json
{
  "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 檔案。

2、命令列的引數傳遞

我們現在知道在命令列執行命令後用 Node 啟動的檔案為 yourname-http-server.js,在啟動檔案時我們應該啟動我們的伺服器,並結合 commander 模組的引數解析,則需要用命令列傳遞的引數替換掉 config.js 中的預設引數。

檔案位置:~static/bin/yourname-http-server.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 類的部分程式碼。

檔案位置:~static/index.js —— 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

3、在啟動服務時自動開啟瀏覽器

由於 JS 是單執行緒的,在命令列輸入命令啟動服務的同時不能去做其他的事,此時要靠多程式來幫助我們開啟瀏覽器,在 JS 中開啟一個子程式來開啟瀏覽器。

檔案位置:~static/bin/yourname-http-server.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}`);
// ********** 以上為新增程式碼 **********複製程式碼

4、釋出命令列工具到 npm

在釋出我們自己實現的 npm 模組之前需要先做一件事,就是解除當前模組與全域性環境的 link,我們可以通過兩種方式,第一種方式是直接到系統儲存命令檔案的資料夾刪除模組對應命令的 yourname-http-server.cmd檔案,第二種方式是在模組根目錄啟動命令列並輸入如下命令。

npm unlink

輸入下面命令進行登入:

npm login

登入成功後執行下面命令進行釋出:

npm publish

釋出成功後再次使用自己的模組需要通過 npm 下載並全域性安裝,命令如下:

npm install yourname-http-server -g

任意資料夾內開啟命令列,並執行命令啟動服務驗證。

在釋出模組之前如果使用 nrm 切換過其他的源,必須切換回 npm,再進行登入和釋出操作。

總結

其實我們實現的靜態伺服器核心還在於處理請求和響應的邏輯上,只是不再手動輸入 node 命令啟動,而是藉助一些第三方模組關聯到了命令列並通過命令啟動,開發其他型別的命令列工具也需要藉助這些第三方模組,靜態伺服器只是其中之一,其實類似這種命令列工具在開發的角度來講屬於 “造輪子” 系列,可以獨立開發命令列工具是一個成為前端架構的必備技能,希望通過本篇文章可以瞭解命令列工具的開發流程,在未來 “造輪子” 的道路上提供幫助。


原文出自:https://www.pandashen.com


相關文章