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

前言: 關於 http server

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

很是易用和方便,若是咱們想改變端口,默認目錄,或者主機名等等,能夠在啓動時在命令行直接配置html

具體用法就是 cmd: http-server -p 3001 那麼啓動時就會訪問3001端口,其餘配置能夠參考npm官網瞭解,在這裏就不贅述了。本篇主要想經過http-server的底層原理,實現一個簡易的http-server包,實現後還能夠發到npm上成爲本身的做品哦,一塊兒看看吧。

npm 註冊

想要了解發布npm的同窗能夠在npm官網註冊屬於本身的npm帳號,注意郵箱必定要驗證哦,否則發佈不了本身的包node

開始項目前最好提早了解的一些內容

  • nodejs環境
  • fs模塊,包括讀取文件,讀寫流等
  • async,promise
  • http知識
  • ejs模板引擎

一 項目搭建

package.json

首先要安裝node環境,由於咱們的項目是基於nodejs的http工具。 建立項目文件夾後用npm或yarn初始化均可以,建立項目的package.json文件,配置過程跟隨包管理工具的提示便可,輸入name的時候,可使用你想要發佈的包的名字;author輸入你在npm官網註冊的用戶名,版本就默認是1.0.0就好,個人pachage.json配置以下npm

這裏bin咱們配置爲 啓動命令行自定義名:"bin/www.js",做爲咱們的命令行啓動配置文件
main: index.js是咱們主要邏輯腳本
author是做者名,這裏使用npm官網的用戶名保持一致便可json

文件夾結構

建議你們這樣配置 首先有bin文件夾,下放www.js,這裏咱們用於配置命令行工具,也是啓動包的關鍵所在
node_modules爲安裝依賴後生成的,請忽略
public是咱們想要讓用戶訪問的靜態資源目錄,能夠隨意放一些文件夾和文件
src是咱們的核心js和配置
src/template.html是展現的界面,由於咱們要搭建的是一個靜態資源站點,用戶須要訪問頁面,能夠點擊目錄或文件等操做。windows

二 核心代碼

1. src/config.js

首先咱們構建config.js,即默認配置項數組

module.exports = {
    port: 8080, // 默認端口
    host: 'localhost', // 默認主機名
    dir: process.cwd() // 默認讀取目錄
}
複製代碼

咱們導出默認的端口號,默認的主機名,和默認的文件目錄,
其中 process.cwd()是讀取進程當前的工做目錄,你在哪啓動,就讀哪一個目錄promise

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中啓動服務器
簡要架構:

咱們引入的模塊主要有:

  • 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,沒有什麼問題的話,咱們會彈出

瀏覽器並訪問public目錄
點擊a文件夾

打開文件

源文件:

至此,咱們實現了傳說中的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

注意密碼輸入的時候是看不見的,我一開始也不知道,實際是輸入對的 _ _(:з」∠)__,郵箱必須是你的認證郵箱

npm publish

登陸成功後,就能夠執行
npm publish指令,發佈成功有以下提示

這時候就能夠去官網看一下你的包是否是成功了。
更新代碼的流程和發佈是同樣的,可是你要更新package.json中的version號。 這是個人包的地址,你們有興趣能夠看看 zyx-http-server

使用本身的包

依然是使用 npm i zyx-http-server -g 全局安裝或局部安裝這個包,咱們試一下:

能夠啓動

中文文件夾bug已修復

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


但願個人文章能夠幫到你~

相關文章
相關標籤/搜索