命令行工具 —— 手寫相似 http-server 的靜態服務器

在這裏插入圖片描述


閱讀原文


前言

npm 裏有個 http-server 的模塊,是一個簡單的、零配置的 HTTP 服務,它很是強大,同時很是簡單,能夠方便的幫助咱們開啓本地服務器,以及局域網共享,能夠用來作測試,開發,學習時的環境配置,咱們本節就模擬 http-server 實現一個本身的啓動本地服務的命令行工具。css


http-server 使用

http-server 服務器經過命令行啓動,使用時須要安裝,安裝命令以下:html

npm install http-server -g

啓動本地服務器時在根目錄下執行下面命令便可:前端

http-server [path] [option]

path 默認狀況下是 ./public,不然是 ./,啓動後能夠經過 http://localhost:8080 來訪問服務器,options 爲其餘參數, npm 官方文檔 https://www.npmjs.com/package... 有詳細說明。node

當經過瀏覽器訪問 http://localhost:8080 之後,會將咱們服務器根目錄的目錄結構顯示在瀏覽器頁面上,當點擊文件夾時,能夠繼續顯示內部的文件和文件夾,當點擊文件時會直接經過服務器訪問文件,並將文件內容顯示在瀏覽器頁面上。npm


實現命令行工具依賴的模塊

一、chalk 模塊

chalk 模塊是用來控制命令行輸出的文字顏色的第三方模塊,使用前須要安裝,安裝命令以下:json

npm install chalk

chalk 模塊的用法以下,模塊支持的顏色和更多的 API 能夠在 npm 官方文檔 https://www.npmjs.com/package... 中查看。數組

// 文件位置:~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 的顏色。瀏覽器

二、debug 模塊

debug 模塊能夠匹配當前環境變量 DEBUG 的值並輸出相關信息,做用在於命令行工具能夠根據不一樣狀況輸出的信息進行調試,是第三方模塊,使用前需安裝,命令以下。緩存

npm install debug

debug 的簡單使用以下,若是想了解更詳細的 API 能夠在 npm 官方文檔 https://www.npmjs.com/package... 中查看。服務器

// 文件位置:~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 的好處就是能夠在開發的時候打印一些調試用的信息,在開發完成後由於匹配不到環境變量,這些信息就會被隱藏。

三、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 <n> server port
   -o, --host <n> server host
   -d, --dir <n> server dir
   -h, --help output usage information

usage 方法可讓咱們詳細的定製參數的類型和描述,option 方法可讓咱們添加執行 --help 指令時打印的命令以及對應的描述信息。

執行下面命令:

node commander-test2.js --port 3000 --host localhost --dir public

執行命令後咱們發現其實給咱們的參數掛在了 commander 對象上,方便咱們取值。

在咱們使用別人的命令行工具時會發如今上面輸出信息的時候常常會在下面輸出 How to use 的列表,更詳細的描述了每條命令的做用及用法。

// 文件位置:&#126;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 <n> server port
   -o, --host <n> server host
   -d, --dir <n> server dir
   -h, --help output usage information
How to use:
   yourname-http-server --port <val>
   yourname-http-server --host <val>
   yourname-http-server --dir <val>

以上是 commander 模塊的基本用法,如想了解更詳細的 API 和使用案例能夠到 npm 官方文檔查看,地址以下 https://www.npmjs.com/package...


實現靜態服務的功能

一、文件目錄

<pre/>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

二、配置文件

在啓動靜態服務的時候,咱們但願能夠經過命令行傳參的形式來定義當前啓動服務的主機名端口號,以及默認檢索的文件根目錄,因此須要配置文件來實現靈活傳參。

// 文件位置:&#126;static/config.js
module.exports = {
    port: 3000,
    host: "localhost",
    dir: process.cwd()
}

在上面的配置中,默認端口號爲 3000,默認主機名爲 localhost,咱們設置默認檢索文件的根目錄爲經過命令行啓動服務器的目錄,而 process.cwd() 的值就是咱們啓動命令行執行命令的目錄的絕對路徑。

三、建立服務器 Server 類

由於咱們的命令行工具啓動本地服務多是在系統的任意位置,或者指定啓動服務訪問的域,提升可配置性,而且要更方便給服務器擴展更多的方法處理不一樣的邏輯,因此須要建立一個 Server 類。

// 文件位置:&#126;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 類的實例上,目的是方便內部的方法使用以及達到不輕易操做全局變量的目的。

四、啓動服務器的 start 方法

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

// 文件位置:&#126;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 方法來打印,並將端口號設置成綠色。

五、服務回調 handleRequest 方法

在實現 handleRequest 以前咱們應該瞭解要實現的功能,在 http-server 中,若是訪問的服務地址路徑後面指定具體要訪問的文件,而且當前啓動服務根目錄按照訪問路徑能夠查找到文件,將文件內容讀取後響應給客戶端,若是沒指定文件,應該檢索當前啓動服務根目錄或默認設置的目錄結構,並將文件的結構經過模板渲染成超連接後將頁面響應給客戶端,再次點擊頁面的上的連接,若是是文件,直接讀取並響應文件內容,若是是文件夾,則繼續檢索內部結構經過模板渲染成頁面。

// 文件位置:&#126;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 方法邏輯清晰且不那麼臃腫。

六、錯誤響應 sendError 方法

在服務器處理不一樣的請求和響應時可能須要處理不一樣的錯誤,這些錯誤的不一樣就是捕獲錯誤對象的不一樣,因此咱們的 sendError 方法爲了更方便的或取請求參數、處理響應以及更好的複用,將參數設置爲請求對象、響應對象和錯誤對象。

// 文件位置:&#126;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");
}

七、渲染目錄 sendDirDetails 方法

在渲染文件夾詳情以前咱們首先要作的就是異步讀取文件目錄,因此咱們一樣使用 async 函數來實現,NodeJS 中有不少渲染頁面的模板,咱們本次使用 ejs,語法簡單,比較經常使用,ejs 爲第三方模塊,使用前需安裝,更詳細的用法可參照 npm 官方文檔 https://www.npmjs.com/package...

npm install ejs

sendDirDetails 的參數爲請求對象、響應對象和 statObj

// 文件位置:&#126;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,咱們須要在每次訪問的時候都將上一次訪問的路徑與當前訪問的文件夾或文件名進行拼接,保證路徑的正確性。

八、ejs 模板 index.html

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

<!-- 文件位置:&#126;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 邏輯使用 <% %> 包裹,使用 <%= %> 輸出變量。

九、返回文件內容 sendFile 方法

因爲都是根據路徑查找或操做文件目錄並作出響應,sendFile 方法與 sendDirDetails 方法的參數相同,分別爲 reqresstatObj

// 文件位置:&#126;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

十、緩存策略 cache 方法

咱們本次的緩存兼容 HTTP 1.0HTTP 1.1 版本,而且同時使用強制緩存和協商緩存共同存在的策略。

// 文件位置:&#126;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 狀態碼,若是協商緩存失效則讀取文件內容返回瀏覽器。

十一、服務器壓縮 compress 方法

爲了減小文件數據在傳輸過程當中消耗的流量和時間,咱們在瀏覽器支持解壓的狀況下使用服務器壓縮功能,瀏覽器會在請求時默認發送請求頭 Accept-Encoding 通知咱們的服務器當前支持的壓縮格式,咱們要作的就是按照壓縮格式的優先級進行匹配,按照最高優先級的壓縮格式進行壓縮,將壓縮後的數據返回,並經過響應頭 Content-Encoding 通知瀏覽器當前的壓縮格式(壓縮流的本質爲轉化流)。

// 文件位置:&#126;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 方法。

十二、處理範圍請求 range 方法

range 方法處理的場景爲客戶端發送請求只想獲取文件的某個範圍的數據,此時經過 range 方法讀取文件範圍對應的內容響應給客戶端,經過響應頭 Accept-Ranges 通知瀏覽器當前響應範圍請求,經過響應頭 Content-Range 通知客戶端響應的範圍以及文件的總字節數。

// 文件位置:&#126;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,再也不向下執行。


將靜態服務器關聯到命令行

一、命令行啓動服務器

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 文件。

二、命令行的參數傳遞

咱們如今知道在命令行執行命令後用 Node 啓動的文件爲 yourname-http-server.js,在啓動文件時咱們應該啓動咱們的服務器,並結合 commander 模塊的參數解析,則須要用命令行傳遞的參數替換掉 config.js 中的默認參數。

// 文件位置:&#126;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 類的部分代碼。

// 文件位置:&#126;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

三、在啓動服務時自動打開瀏覽器

因爲 JS 是單線程的,在命令行輸入命令啓動服務的同時不能去作其餘的事,此時要靠多進程來幫助咱們打開瀏覽器,在 JS 中開啓一個子進程來打開瀏覽器。

// 文件位置:&#126;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}`);
// ********** 以上爲新增代碼 **********

四、發佈命令行工具到 npm

在發佈咱們本身實現的 npm 模塊以前須要先作一件事,就是解除當前模塊與全局環境的 link,咱們能夠經過兩種方式,第一種方式是直接到系統存儲命令文件的文件夾刪除模塊對應命令的 yourname-http-server.cmd 文件,第二種方式是在模塊根目錄啓動命令行並輸入以下命令。

npm unlink

輸入下面命令進行登陸:

npm login

登陸成功後執行下面命令進行發佈:

npm publish

發佈成功後再次使用本身的模塊須要經過 npm 下載並全局安裝,命令以下:

npm install yourname-http-server -g

任意文件夾內打開命令行,並執行命令啓動服務驗證。

在發佈模塊以前若是使用 nrm 切換過其餘的源,必須切換回 npm,再進行登陸和發佈操做。


總結

其實咱們實現的靜態服務器核心還在於處理請求和響應的邏輯上,只是再也不手動輸入 node 命令啓動,而是藉助一些第三方模塊關聯到了命令行並經過命令啓動,開發其餘類型的命令行工具也須要藉助這些第三方模塊,靜態服務器只是其中之一,其實相似這種命令行工具在開發的角度來說屬於 「造輪子」 系列,能夠獨立開發命令行工具是一個成爲前端架構的必備技能,但願經過本篇文章能夠了解命令行工具的開發流程,在將來 「造輪子」 的道路上提供幫助。

相關文章
相關標籤/搜索