嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

zhangyuxiang1226發表於2018-08-17

前言: 關於 http server

應該有小夥伴瞭解或用過http-serverhttp-server是一個node環境下的命令列http伺服器,這裡是npm官網的連結 www.npmjs.com/package/htt… , 可以從npm的官網查到其用法:
即npm安裝後,在命令列輸入指令http-server直接開啟伺服器,在伺服器啟動的目錄下,預設會找public靜態資源目錄,去訪問預設的127.0.0.1:8080 可以訪問到靜態目錄站點。

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

非常易用和方便,如果我們想改變埠,預設目錄,或者主機名等等,可以在啟動時在命令列直接配置

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)
具體用法就是cmd: http-server -p 3001 那麼啟動時就會訪問3001埠,其他配置可以參考npm官網瞭解,在這裡就不贅述了。本篇主要想通過http-server的底層原理,實現一個簡易的http-server包,實現後還可以發到npm上成為自己的作品哦,一起看看吧。

npm 註冊

想要了解發布npm的同學可以在npm官網註冊屬於自己的npm賬號,注意郵箱一定要驗證哦,不然釋出不了自己的包

開始專案前最好提前瞭解的一些內容

  • nodejs環境
  • fs模組,包括讀取檔案,讀寫流等
  • async,promise
  • http知識
  • ejs模板引擎

一 專案搭建

package.json

首先要安裝node環境,因為我們的專案是基於nodejs的http工具。 建立專案資料夾後用npm或yarn初始化都可以,建立專案的package.json檔案,配置過程跟隨包管理工具的提示即可,輸入name的時候,可以使用你想要釋出的包的名字;author輸入你在npm官網註冊的使用者名稱,版本就預設是1.0.0就好,我的pachage.json配置如下

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

這裡bin我們配置為 啟動命令列自定義名:"bin/www.js",作為我們的命令列啟動配置檔案
main: index.js是我們主要邏輯指令碼
author是作者名,這裡使用npm官網的使用者名稱保持一致即可

資料夾結構

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

建議大家這樣配置 首先有bin資料夾,下放www.js,這裡我們用於配置命令列工具,也是啟動包的關鍵所在
node_modules為安裝依賴後生成的,請忽略
public是我們想要讓使用者訪問的靜態資源目錄,可以隨意放一些資料夾和檔案
src是我們的核心js和配置
src/template.html是展示的介面,因為我們要搭建的是一個靜態資源站點,使用者需要訪問頁面,可以點選目錄或檔案等操作。

二 核心程式碼

1. src/config.js

首先我們構建config.js,即預設配置項

module.exports = {
    port: 8080, // 預設埠
    host: 'localhost', // 預設主機名
    dir: process.cwd() // 預設讀取目錄
}
複製程式碼

我們匯出預設的埠號,預設的主機名,和預設的檔案目錄,
其中 process.cwd()是讀取程式當前的工作目錄,你在哪啟動,就讀哪個目錄

2. src/template.html

上面講到了template.html用於展示目錄,我們這裡採用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>Document</title>
</head>
<body>
    <h2><%=name%></h2>
    <%arr.forEach(item=>{%>
      <li><a href="<%=item.href%>"><%=item.name%></a></li>
    <%})%>
</body>
</html>
複製程式碼

h2標籤中我們展示當前路徑 li為檔案目錄結構,可供點選進入檔案或資料夾;可以看出,我們要給這個頁面輸出name和arr兩個資料,在核心js中會詳細講如何渲染template.

3. src/index.js

這裡是我們的主要邏輯指令碼了,本js裡會封裝Server類,用於在www.js中啟動伺服器
簡要架構:

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

我們引入的模組主要有:

  • http http模組
  • util util工具模組,主要使用promisify方法
  • url url模組 可方便獲取路徑
  • zlib 檔案壓縮模組,用於建立gzip或deflate的轉化流
  • fs 檔案系統模組
  • path 路徑拼接
  • querystring 路徑處理
  • ejs 模板引擎
  • chalk 粉筆模組 可以給輸出的命令列文字加顏色喔
  • mime 獲取檔案型別
  • debug debug模組
  • config 我們自己寫的配置檔案

下面開始構建 Server 類,用於處理請求中的各種情況,返回不同的內容

① 構造Server

class Server {
    constructor(command) {
        this.config = {...config,...command} // config和命令列的內容展示
        this.template = template;
    }
}
複製程式碼

② 主要的請求處理方法 ※

class Server {
    ...
    async handleRequest(req, res) {
        let { dir } = this.config; // 需要將請求的路徑和dir拼接在一起
        //如 http://localhost:8080/index.html
        let { pathname } = url.parse(req.url);
        // 如果獨到的是網站小圖示,就直接輸出
        if (pathname === '/favicon.ico') return res.end();
        pathname = decodeURIComponent(pathname); // 對資料夾名稱進行轉碼處理
        // p是決定檔案路徑
        let p = path.join(dir, pathname);
        try {
          // 判斷當前路徑是檔案 還是資料夾
          let statObj = await stat(p);
          if (statObj.isDirectory()) {
            // 讀取當前訪問的目錄下的所有內容 readdir 陣列 把陣列渲染回頁面
            res.setHeader('Content-Type', 'text/html;charset=utf8')
            let dirs = await readdir(p);
            dirs = dirs.map(item=>({
              name:item,
              // 因為點選第二層時 需要帶上第一層的路徑,所有拼接上就ok了
              href:path.join(pathname,item)
            }))
            // 渲染template.html中需要填充的內容,name是當前檔案目錄,arr為當前資料夾下的目錄陣列
            let str = ejs.render(this.template, {
              name: `Index of ${pathname}`,
              arr: dirs
            });
            
            // 響應中返回填充內容
            res.end(str);
        
          } else {
          // 如果不是資料夾,則直接輸出檔案內容
            this.sendFile(req, res, statObj, p);
          }
        } catch (e) {
          debug(e); // 傳送錯誤
          this.sendError(req, res);
        }
  }
    ...
}
複製程式碼

③ 處理使用者快取

用於告知伺服器是否本次快取,如果是瀏覽器客戶端已經快取的檔案,直接讀取快取即可,優化效能

class Server {
    ...
    
    cache(req, res, statObj, p ) {
        // 設定快取頭
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).getTime());
        // 設定etag和上次最新修改時間
        let eTag = statObj.ctime.getTime() + '-' + statObj.size;
        let lastModified = statObj.ctime.getTime();
        // 傳給客戶端
        res.setHeader('Etag', eTag);
        res.setHeader('Last-Modified', lastModified);
        // 客戶端把上次設定的帶過來
        let ifNoneMatch = req.headers['if-none-match'];
        let ifModifiedSince = req.headers['if-modified-since'];
        // 其中任意一個不生效快取就不生效
        if (eTag !== ifNoneMatch && lastModified !== ifModifiedSince) {
            return false;
        }

        return true;
    }
    
    ...
}
複製程式碼

④ 是否壓縮

返回壓縮檔案,優化訪問速度

class Server {
    ...

     // 是否壓縮
    gzip(req, res, statObj, p) {
        // 判斷請求頭是否設定了接收編碼
        let encoding = req.headers['accept-encoding'];
        // 如果有則判斷是否有gzip或者deflate
        if (encoding) {
            // gzip
            if (encoding.match(/\bgzip\b/)) {
                res.setHeader('Content-Encoding', 'gzip');
                return zlib.createGzip();
            }
            // deflate
            if (encoding.match(/\bdeflate\b/)) {
                res.setHeader('Content-Encoding', 'deflate');
                return zlib.createDeflate();
            }
            return false;
        }
        else {
            return false;
        }
    }
    
    ...
}

複製程式碼

⑤ 判斷是否有範圍請求

判斷是否請求頭

class Server {
    ...
    range(req, res, statObj, p) {
        let range = req.headers['range'];
        // 有範圍請求時返回讀流,斷點續傳
        if (range) {
            let [, start, end] = range.match(/bytes=(\d*)-(\d*)/);
            start = start ? Number(start) : 0;
            end = end ? Number(end) : statObj.size - 1;
            res.statusCode = 206;
            res.setHeader('Content-Range', `bytes ${start}-${end}/${statObj.size - 1}`);
            fs.createReadStream(p, {start, end}).pipe(res);
        }
        else {
            return false;
        }
    }
    ...
    
}
複製程式碼

⑥ 傳送檔案

傳送檔案方法,即我們在 handleRequest時如果判斷走到認為讀取的是一個檔案,則傳送這個檔案展示給使用者

class Server {
    ...
     sendFile(req, res, statObj, p) {
        if (this.cache(req, res, statObj, p)) {
            res.statusCode = 304;
            return res.end();
        }
        // 是範圍請求就忽略
        if (this.range(req, res, statObj, p)) return;
        // 設定檔案型別頭,如果不設定,我們訪問一個html檔案可能會導致下載
        res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
        // 如果是需要壓縮則定義gzip轉化流,講檔案壓縮後輸出
        let transform = this.gzip(req, res, statObj, p);
        if (transform) {
            return fs.createReadStream(p).pipe(transform).pipe(res);
        }
        // 如果不是不需要壓縮則直接返回檔案
        fs.createReadStream(p).pipe(res);
    }
    ...
    
}
複製程式碼

⑦ 處理報錯

在handleRequest方法中的處理錯誤方法,

class Server {
    ...
   sendError(req, res){
    // 返回的狀態碼設定為404
        res.statusCode = 404;
        // 頁面返回文字
        res.end(`404 Not Found`);
        this.start();
    }
    ...
    
}
複製程式碼

⑧ 啟動方法

原生http模組的建立伺服器方法,建立成功後再cmd介面輸出,告知主機和埠

class Server {
    ...
   start() {
        let server = http.createServer(this.handleRequest.bind(this));
        server.listen(this.config.port, this.config.host, ()=> {
            console.log(`server start http://${this.config.host}:${chalk.green(this.config.port)}`);
        });
    }
    ...
    
}
複製程式碼

至此,我們的核心程式碼已經寫完了,簡易處理了伺服器端需要處理的一些狀況,有興趣的同學可以補充和完善

三 bin/www.js 執行指令碼

我們在命令列中輸入的指令,調器執行指令碼,並開啟伺服器
注意在www.js開頭一定要寫 #! /usr/bin/env node 告知作業系統node環境執行,以下為www.js的內容

#! /usr/bin/env node
let Server = require('../src/index.js'); // 匯入Server
let commander = require('commander'); // 匯入命令列模組
let {version} = require('../package.json'); // 讀取package.json的版本

// 配置命令列
commander
.option('-p,--port <n>', 'config port') // 配置埠
.option('-o,--host [value]', 'config hostname') // 配置主機名
.option('-d,--dir [value]', 'config directory') // 配置訪問目錄
.version(version, '-v,--version').parse(process.argv); // 展示版本

let server = new Server(commander);
server.start(); // 啟動

let config =require('../src/config');

commander = {...config, ...commander}

let os = require('os');
// 執行模組
let {exec} = require('child_process')

// 判斷作業系統平臺,win32是windows,執行訪問程式,會自動彈出預設瀏覽器喔
if (os.platform() === 'win32') {
    exec(`start http://${commander.host}:${commander.port}`);
}
else {
    exec(`open http://${commander.host}:${commander.port}`);
}
複製程式碼

再看一下我們在package.json中的配置

"bin": {
    "zyx-http-server": "bin/www.js"
  },
複製程式碼

即我們的啟動命令是 zyx-http-server,也可以根據自己的配置,補充其他命令,如 zyx-http-server -d public,則讀取public作為靜態資源根目錄

npm link

處理npm install 我們包中的依賴(ejs, chalk, debug等)之外還需要執行
npm link:將一個任意位置的npm包連結到全域性執行環境,從而在任意位置使用命令列都可以直接執行該npm包

四 執行

在資料夾啟動命令列工具 執行zyx-http-server -d public,沒有什麼問題的話,我們會彈出

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)
瀏覽器並訪問public目錄
點選a資料夾

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

開啟檔案

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)
原始檔:

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

至此,我們實現了傳說中的http-server靜態伺服器,由於我叫zyx啦,所以取名叫zyx-http-server

五 釋出到npm官網

沒有註冊的同學看到這一步的話請先去npm官網註冊一個屬於自己的賬號,然後我們才能釋出到自己包。
進行之前有這麼幾點需要注意

  • 切換源到npm節點,如果平時使用cnpm或者其他節點的同學,請在命令列輸入nrm use npm切換
  • 在npm的註冊郵箱一定要驗證才可以,官網會發一份驗證郵件給你,點選進行驗證;我遇到了一種情況是驗證過了,但是沒生效,這時候你去官網再改一次郵箱試試
  • 要發的包的命名,是package.json中的name配置項,一開始沒有配置的,可以去寫一個自己的包名,可以通過訪問 www.npmjs.com/package/ + 你的包名,看看在npm有沒有被佔用,被佔用的話就換一個哦,一般被佔用的話,你也無法提交

npm login

需要在命令列執行 npm login登入npm,如果你一開始沒有切換到npm官網節點,cnpm用npm賬號也是可以登入的,所以請提前先切換到npm

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)
注意密碼輸入的時候是看不見的,我一開始也不知道,實際是輸入對的 _ _(:з」∠)__,郵箱必須是你的認證郵箱

npm publish

登入成功後,就可以執行
npm publish指令,釋出成功有如下提示

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)
這時候就可以去官網看一下你的包是不是成功了。
更新程式碼的流程和釋出是一樣的,但是你要更新package.json中的version號。 這是我的包的地址,大家有興趣可以看看 zyx-http-server

使用自己的包

依然是使用 npm i zyx-http-server -g 全域性安裝或區域性安裝這個包,我們試一下:

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

嘗試手寫一個 nodejs http-server(含釋出到npm的流程)

可以啟動

中文資料夾bug已修復

在handleRequest方法中對pathname進行轉碼處理


希望我的文章可以幫到你~

相關文章