手把手教你如何實現一個基於node的靜態文件服務器

首先來看看我要準備給你們寫的靜態文件服務器都要實現哪些功能,而後根據具體的功能咱們來一一的介紹html

支持如下功能node

  • 支持輸出日誌debug
  • 讀取靜態文件
    • 文件的讀取 MIME類型支持
    • 文件夾列表展現 handlebars模板引擎
  • 緩存支持/控制
  • Range支持,斷點續傳
  • 支持gzip和deflate壓縮
  • 發佈爲可執行命令並能夠後臺運行,能夠在全局經過下面的命令來設置根目錄端口號和主機名:myselfsever -d指定靜態文件的根目錄 -p指定端口號 -o指定監聽的主機

下載地址git

啓動github

npm install // 安裝依賴
    npm link // 建立鏈接
    myselfserver // 在任意目錄下啓動服務
    // 訪問 localhost:8080
複製代碼

其次咱們先大體看一下咱們的服務器的大體結構npm

接下來咱們來介紹咱們這個服務器json

要想實現一個服務器的功能,咱們每每須要設置一下服務器的主機名,端口號,靜態文件根目錄等信息,在這裏咱們的config.js幫咱們實現了這一配置,固然了這是基礎的配置,後面咱們還會講到如何經過命令行來更改配置。基礎配置以下:windows

let path = require('path');
let config = {
    host: 'localhost',// 監聽主機
    port: 8080,// 主機端口號
    root: path.resolve(__dirname, '..')// 靜態文件根目錄
}
module.exports = config;
複製代碼

配置好以後接下來咱們就須要寫一個咱們的服務了,服務的代碼會在咱們的app.js中體現,其中包括咱們上面列舉的幾乎全部的功能,咱們先來看一下app.js的代碼結構瀏覽器

const fs = require('fs');
	const url = require('url');
	const http = require('http');
	const path = require('path');
	const config = require('./config');
	
	class Server {
	    constructor(argv) {
			// 生產handlebars模板
			this.list = list()
	        // 處理命令行中設置的參數,來重寫基本參數
	        this.config = Object.assign({}, config, argv)
	    }
	    start() {
			/*啓動http服務*/
	        let server = http.createServer();
	        server.on('request', this.request.bind(this));
	        let url = `http://${config.host}:${config.port}`;
	
	        server.listen(this.config.port, ()=>{
                // (1)支持輸出日誌debug 在下文中會講一下使用它的注意事項
	            debug(`server started at ${chalk.green(url)}`);
	        })
	    }
	    /*(2)讀取靜態文件*/
	    async request(req,res) {
	      // 由於響應可能出錯,因此在咱們的代碼中這裏會用try catch包一下。
          // try
          // 文件夾: (2.1)模板引擎渲染展現文件列表  這裏用handlebars模板引擎來渲染
          // 文件:-->sendFile
          // catch
          // 處理錯誤交給-->sendError
	    }
	    /* send file to browser*/
	    sendFile (req, res, filePath, statObj) {
	        // (2.2)處理文件併發送給瀏覽器  添加MIME類型支持
	    }
	    /* handle error*/
	    sendError (error, req, res) {
	        // 公用的的錯誤處理函數
	    }
	    /*cache 是否走緩存*/
	    isCache (req, res, filePath, statObj) {
	        // (3)處理緩存
	    }
	    /*broken-point continuingly-transferring  斷點續傳*/
	    rangeTransfer (req, res, filePath, statObj) {
	        // (4)支持斷點續傳
	    }
	    /*compression 壓縮*/
	    compression (req, res) {
	       // (5)支持gzip和deflate壓縮
	    }
	}
	module.exports = Server;
    (6)myselfsever -d指定靜態文件的根目錄 -p指定端口號 -o指定監聽的主機 這個命令實現的腳本代碼在 bin目錄下的commond文件中
複製代碼

開始進入細節分析階段

注:每用到一個模塊的時候記得提早安裝到本地緩存

(1)如何打印錯誤日誌---debug

const debug = require('debug'); // 安裝引入debug包
debug('staticserver:app');
// debug 第三方模塊返回的是一個函數,該函數執行須要傳入兩個參數,一個是你當前項目的名稱,即package.json 中的name值,第二個參數是你的模塊的服務入口文件的名稱。在我們的這個項目中是app.js

複製代碼

當咱們項目中要使用debug錯誤輸出模塊時,須要配置環境變量咱們的debug日誌纔會被輸出bash

環境變量的配置方法 windows上配置環境變量

  • 配置單個文件,即只有當前文件中的debug日誌會輸出 (優點:能夠靈活的配置,輸出本身想要輸出的文件的debug日誌)
$ set DEBUG=staticserver:app
複製代碼
  • 配置整個項目,整個項目中的debug日誌都會被輸出
$ set DEBUG=staticserver:*
複製代碼

mac上配置環境變量

$ export DEBUG=staticserver:app
  $ export DEBUG=staticserver:*
複製代碼

(2)讀取靜態文件

若是是文件夾的話顯示文件列表,這裏咱們用到了handlebars模板引擎

const handlebars = require('handlebars'); // 模版引擎
    // 調用handlebars.compile方法生成一個模板
	function list () {
	    let tmpl = fs.readFileSync(path.resolve(__dirname,'template','list.html'),'utf8');
	    return handlebars.compile(tmpl)
	}

複製代碼

接下來咱們來看訪問的路徑是文件的狀況。這時候咱們會根據文件的後綴的不一樣返回不一樣的響應類型,這時咱們就用到了咱們的mime模塊

/*靜態文件服務器*/
	const { promisify, inspect } = require('util');
    const mime = require('mime');
	const stat = promisify(fs.stat);
	const readdir = promisify(fs.readdir);
    async request(req,res) {
        // 先取到客戶端想訪問的路徑
        let { pathname } = url.parse(req.url);
        // 若是訪問的是favicon.ico 的話返回一個錯誤信息
        if (pathname == '/favicon.ico') {
            return this.sendError('not favicon.ico',req,res)
        }
        // 獲取訪問的文件或文件夾的路徑
        let filePath = path.join(this.config.root, pathname);
        try{
            // 判斷訪問的路徑是文件夾 仍是文件
           let statObj = await stat(filePath);
           if (statObj.isDirectory()) {// 若是是目錄的話 應該顯示目錄下面的文件列表 不然顯示文件內容
               let files = await readdir(filePath);
               files = files.map(file => ({
                   name: file,
                   url: path.join(pathname, file)
               }))
                let html = this.list({
                    title: pathname,
                    files,
                });
               res.setHeader('Content-Type', 'text/html');
               res.end(html)
           } else {
            // 若是是文件的話顯示文件內容
               this.sendFile(req, res, filePath, statObj)
           }
        }catch (e) {
            debug(inspect(e)); // 把一個對象轉化爲字符串,由於有的tosting會生成object object
            this.sendError(e, req, res);
        }
    }

    // 接下來咱們來看訪問的路徑是文件的狀況。這時候咱們會根據文件的後綴的不一樣返回不一樣的響應類型,這時咱們就用到了咱們的mime模塊
  
      sendFile (req, res, filePath, statObj) {
        // 若是緩存存在的話走緩存
        if(this.isCache(req, res, filePath, statObj)){
            return;
        }
        res.statusCode = 200; // 能夠省略
        res.setHeader('Content-Type', mime.getType(filePath) + ';charset=utf-8');
        let encoding = this.compression(req,res);
        // 是否是須要壓縮
        if(encoding) {
            // 在這裏使用斷點續傳
            this.rangeTransfer(req, res, filePath, statObj).pipe(encoding).pipe(res)
        } else {
           
            this.rangeTransfer(req, res, filePath, statObj).pipe(res)
        }
    }

複製代碼

(3)緩存支持和控制

要想理解下面緩存是否存在,緩存是否有效來判斷要不要走緩存,你們能夠先看一下這篇文章node中的緩存機制能夠加深對緩存的理解

/*cache 是否走緩存*/
    isCache (req, res, filePath, statObj) {
        let ifNoneMatch = req.headers['if-none-match'];
        let ifModifiedSince = req.headers['if-modified-since'];
        res.setHeader('Cache-Control','private,max-age=10');
        res.setHeader('Expires',new Date(Date.now() + 10*1000).toGMTString);
        let etag = statObj.size;
        let lastModified = statObj.ctime.toGMTString();
        res.setHeader('Etag',etag)
        res.setHeader('Last-Modified',lastModified);
        if(ifNoneMatch && ifNoneMatch != etag) {
            return false
        }

        if(ifModifiedSince && ifModifiedSince != lastModified){
            return false
        }
        if(ifNoneMatch || ifModifiedSince) {
            res.writeHead(304);
            res.end();
            return true
        } else {
            return false
        }
    }

複製代碼

(4)Range支持,斷點續傳

該選項指定下載字節的範圍,常應用於分塊下載文件

range的表示方式有多種,如100-500,則指定從100開始的400個字節數據;-500表示最後的500個字節;5000-表示從第5000個字節開始的全部字節

另外還能夠同時指定多個字節塊,中間用","分開

服務器告訴客戶端可使用range response.setHeader('Accept-Ranges', 'bytes')

Server經過請求頭中的Range:bytes=0-xxx來判斷是不是作Range請求,若是這個值存在並且有效,則只發回請求的那部分文件內容,響應的狀態碼變成206,若是無效,則返回416狀態碼,代表Request Range Not Satisfiable

/*broken-point continuingly-transferring  斷點續傳*/
    rangeTransfer (req, res, filePath, statObj) {
        let start = 0;
        let end = statObj.size-1;
        let range = req.headers['range'];
        if(range){
            res.setHeader('Accept-range','bytes');
            res.statusCode=206// 返回整個內容的一塊
            let result = range.match(/bytes=(\d*)-(\d*)/);
            start = isNaN(result[1]) ? start : parseInt(result[1]);
            end = isNaN(result[2]) ? end : parseInt(result[2]) - 1
        }
        return fs.createReadStream(filePath, {
            start,
            end
        })
    }

複製代碼

(5)支持gzip和deflate壓縮

服務器會根據客戶端請求中的req.headers['accept-encoding']這個字段中的值來判斷客戶端支持哪一種解壓類型來進行壓縮的

/*compression 壓縮*/
    compression (req, res) {
        let acceptEncoding = req.headers['accept-encoding'];//163.com
        if(acceptEncoding) {
            if(/\bgzip\b/.test(acceptEncoding)){
                res.setHeader('Content-Encoding','gzip');
                return zlib.createGzip();
            } else if(/\bdeflate\b/.test(acceptEncoding)) {
                res.setHeader('Content-Encoding','deflate');
                return zlib.createDeflate();
            } else {
                return null
            }
        }
    }

複製代碼

(6)發佈爲可執行命令並能夠後臺運行,能夠在全局經過下面的命令來設置根目錄端口號和主機名:myselfsever -d指定靜態文件的根目錄 -p指定端口號 -o指定監聽的主機

這個功能是在咱們的bin目錄下面的command 文件中實現的。 文件開頭的#! /usr/bin/env node 這句命令是告訴該腳本用哪一種程序來執行,詳細可看這邊文章Node.js 命令行程序開發教程

#! /usr/bin/env node
	let yargs = require('yargs');
	let Server = require('../src/app.js')
	let argv = yargs.option('d', {
	    alias: 'root',
	    demand: false,
	    description: '請配置監聽靜態文件目錄',
	    default: process.cwd(),
	    type: 'string'
	}).option('o', {
	    alias: 'host',
	    demand: false,
	    description: '請配置監聽的主機',
	    default: 'localhost',
	    type: 'string'
	}).option('p', {
	    alias: 'port',
	    demand: false,
	    description: '請配置監聽的主機的端口號',
	    default: 8080,
	    type: 'number'
	}).usage('myselfsever -d / -p 8080 -o localhost')
	    .example(
	        'myselfsever -d / -p 9090 -o localhost', '在本機器的9090端口上監聽客戶端發來的請求'
	    ).help('h').argv;
	
	
	let server = new Server();
	server.start(argv);
	
複製代碼

天天都進步一點;不要畏懼陌生的事物,天天都學習一點;

相關文章
相關標籤/搜索