Node手把手構(gòu)建靜態(tài)文件服務(wù)器

這篇文章主要將會通過node手把手的構(gòu)建一個靜態(tài)文件服務(wù)器嘴瓤,那么廢話不多說场梆,開發(fā)流程走起來摹芙,我們先看一下將要做的這個靜態(tài)文件服務(wù)器將會有哪些功能?

這個靜態(tài)文件服務(wù)器有哪些功能袱瓮?

  • 讀取靜態(tài)文件
  • MIME類型支持
  • 支持壓縮
  • 支持?jǐn)帱c續(xù)傳
  • 支持緩存與緩存控制
  • 實現(xiàn)命令行調(diào)用
  • 最后將代碼發(fā)布到npm缤骨,可通過npm install -g全局安裝

好了,通過以上的功能梳理尺借,那么我們需要實現(xiàn)的功能就很明確了绊起,也就相當(dāng)于我們項目開發(fā)過程中的需求現(xiàn)在已經(jīng)確定了(原諒我這些天被公司項目別急了),接下來就一步步開始實現(xiàn)功能吧褐望。

功能實現(xiàn)——讀取靜態(tài)文件+MIME類型支持

  1. 首先先構(gòu)建好項目目錄勒庄,項目目錄如下:

    project
     |---bin 命令行實現(xiàn)放置腳本
     |
     |---public 靜態(tài)文件服務(wù)器默認(rèn)靜態(tài)文件夾
     |
     |---src 實現(xiàn)功能的相關(guān)代碼
     |   |
     |   |__template 模板文件夾
     |   |
     |   |__app.js 主要功能文件
     |   |__config.js 配置文件
     |
     |---package.josn (這個不用多說了吧)
    
    
  2. 然后開始實現(xiàn)功能,我們將會通過node的http模塊來啟動一個服務(wù)瘫里,這里我先將功能(讀取靜態(tài)文件实蔽、MIME類型支持)的實現(xiàn)整體代碼貼出來,再慢慢道來:

const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
let chalk = require('chalk');
process.env.DEBUG = 'static:*';
let debug = require('debug')('static:app');//每個debug實例都有一個名字谨读,是否在控制臺打印取決于環(huán)境變量中DEBUG的值是否等于static:app
const mime = require('mime');
const {promisify} = require('util')
let handlebars = require('handlebars');

const config = require('./config')
const stat = promisify(fs.stat)
const readDir = promisify(fs.readdir)
//獲取編譯模板
function getTemplet() {
    let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
    return handlebars.compile(tmpl);
}
class Server {
    constructor(argv) {
        this.config = Object.assign({}, config, argv);
        this.list = getTemplet();
    }
    //啟動服務(wù)
    start() {
        let server = http.createServer();
        server.on('request', this.request.bind(this))
        server.listen(this.config.port);
        let url=`http://${this.config.host}:${this.config.port}`;
        debug(`靜態(tài)服務(wù)啟動成功${chalk.green(url)}`);
    }
    async request(req, res) {//服務(wù)監(jiān)聽函數(shù)
        let pathName = url.parse(req.url).path;
        let filePath = path.join(this.config.root, pathName);
        if (filePath.indexOf('favicon.ico') > 0) {
            this.sendError(req, res, 'not found');
            return
        }
        try {//在靜態(tài)服務(wù)文件夾存在訪問的路徑內(nèi)容
            let statObj = await stat(filePath);
            if (statObj.isDirectory()) {//是文件夾
                let directories = await readDir(filePath);
                let files = directories.map(file => {
                    return {
                        filename: file,
                        url: path.join(pathName, file)
                    }
                });
                let htmls = this.list({
                    title: pathName,
                    files
                });
                res.setHeader('Content-Type', 'text/html');
                res.end(htmls);
            } else {//是文件
                this.sendContent(req, res, filePath, statObj);
            }
        } catch (err) {//靜態(tài)服務(wù)器內(nèi)容不存在訪問內(nèi)容
            this.sendError(req, res, err);
        }
    }
    sendContent(req, res, filePath, statObj) {//向客戶端響應(yīng)內(nèi)容
        let fileType = mime.getType(filePath);
        res.setHeader('Content-Type', `${fileType};charset=UTF-8`);
        let rs = this.getStream(filePath);//獲取文件的可讀流
        rs.pipe(res);
    }
    getStream(filePath) {//返回一個可讀流
        return fs.createReadStream(filePath);
    }
    sendError(req, res, err) {//發(fā)送錯誤
        res.statusCode = 500;
        res.end(`${err.toString()}`)
    }
}
module.exports = Server;

通過以上的代碼局装,我們可以看出,我這里是創(chuàng)建了一個Server類劳殖,然后通過在調(diào)用Server類的start()方法來啟動這樣一個服務(wù)铐尚,在Server類當(dāng)中有以下方法:

  • start 用來啟動服務(wù)的——這個方法里面主要是通過node的http模塊來啟動一個服務(wù),并監(jiān)聽對應(yīng)的端口
  • request 服務(wù)監(jiān)聽函數(shù)——這個方法主要是對啟動服務(wù)的監(jiān)聽哆姻,具體邏輯這里還是在代碼中通過注釋來說明吧:
 async request(req, res) {//服務(wù)監(jiān)聽函數(shù)
         let pathName = url.parse(req.url).path;//獲取到客戶端要訪問的服務(wù)器路徑
         let filePath = path.join(this.config.root, pathName);//客戶端要訪問的路徑得到該路徑在服務(wù)器上的對應(yīng)服務(wù)器物理路徑
         if (filePath.indexOf('favicon.ico') > 0) {//這個判斷主要是為了去掉網(wǎng)站默認(rèn)favicon.ico的請求報錯
             this.sendError(req, res, 'not found');
             return
         }
         try {//在靜態(tài)服務(wù)器存在訪問路徑內(nèi)容
             let statObj = await stat(filePath);//通過node來獲取該路徑下的文件信息
             if (statObj.isDirectory()) {//如果該路徑是對應(yīng)的文件夾
                 let directories = await readDir(filePath);//讀取該文件夾里面的文件內(nèi)容宣增,readDir其實是我定義的const readDir = promisify(fs.readdir)

                 let files = directories.map(file => {//這里主要是為了生成返回html模板內(nèi)容的對應(yīng)數(shù)據(jù)結(jié)構(gòu)如: {title:'顯示的頁面標(biāo)題',files:[{filename:'1',url:'/1'}]};

                     return {
                         filename: file,
                         url: path.join(pathName, file)
                     }
                 });
                 let htmls = this.list({//調(diào)用模板引擎的渲染方法,這就不對模板引擎做過多說明了矛缨,會在最后附上模板引擎的相關(guān)連接爹脾,這里用的handlebars
                     title: pathName,
                     files
                 });
                 res.setHeader('Content-Type', 'text/html');//因為返回的是html頁面,所以需要設(shè)置請求頭箕昭,告訴客戶端如何來解析
                 res.end(htmls);//將讀取到的html發(fā)送給客戶端
             } else {
                 this.sendContent(req, res, filePath, statObj);//調(diào)用Server類的sendContent方法,向客戶端發(fā)送內(nèi)容
             }
         } catch (err) {//靜態(tài)服務(wù)器不存在訪問內(nèi)容
             this.sendError(req, res, err);//調(diào)用Server類的sendError方法灵妨,向客戶端發(fā)送錯誤信息
         }
     }

代碼的解讀我會根據(jù)上一個方法的調(diào)用來一個個的逐行解讀,那么接下來時sendContent

  • sendContent 向客戶端發(fā)送內(nèi)容落竹,代碼段如下:
 sendContent(req, res, filePath, statObj) {//向客戶端響應(yīng)內(nèi)容
         let fileType = mime.getType(filePath);//這里是為了實現(xiàn)對MIME類型的支持泌霍,所以這里需要判斷訪問路徑的文件的MIME類型,主要是通過npm上的mime包來獲取
         res.setHeader('Content-Type', `${fileType};charset=UTF-8`);//設(shè)置對應(yīng)MIME的http響應(yīng)頭述召,這樣客戶端才能對應(yīng)的解析
         let rs = this.getStream(filePath);//獲取對應(yīng)路徑文件的可讀流
         rs.pipe(res);//向客戶端發(fā)送內(nèi)容朱转,這主要是因為res本身就是一個流
     }

那么同樣逐行解讀Server類的getStream方法

  • getStream 獲取一個流對象,代碼如下:
 getStream(filePath) {
         return fs.createReadStream(filePath);//返回一個可讀流积暖,供sendContent方法使用
     }

那么以上就已經(jīng)完成了向客戶端返回對應(yīng)的訪問路徑信息了肋拔,最后還剩一個Server類的sendError方法,這個方法主要是向客戶端發(fā)送一個錯誤信息呀酸。

  • sendError 發(fā)送錯誤信息凉蜂,代碼段如下:
 sendError(req, res, err) {//發(fā)送錯誤
        res.statusCode = 500;//設(shè)置錯誤碼
        res.end(`${err.toString()}`)//向客戶端發(fā)送對應(yīng)的錯誤信息字符串
    }

那么以上的代碼就實現(xiàn)了一個這個靜態(tài)服務(wù)器的——1.讀取靜態(tài)文件。2.MIME類型支持。這樣兩個功能點窿吩,對應(yīng)的代碼文件app.js github地址

功能實現(xiàn)——支持壓縮

因為這個功能點的實現(xiàn)都是基于前面已實現(xiàn)的功能(讀取靜態(tài)文件茎杂、MIME類型支持)的基礎(chǔ)上來做的,所以前面那些基礎(chǔ)的就不再做說明纫雁,同樣的是先貼上完整代碼煌往,然后再講壓縮的實現(xiàn)思路、以及壓縮的功能實現(xiàn)的核心代碼轧邪。整體代碼如下:
```javascript
//添加上文件壓縮刽脖,實現(xiàn)功能有——讀取靜態(tài)文件、MIME類型支持忌愚,支持壓縮
const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const mime = require('mime')
var zlib = require('zlib');
let chalk = require('chalk');
process.env.DEBUG = 'static:app';
let debug = require('debug')('static:app');//每個debug實例都有一個名字曲管,是否在控制臺打印取決于環(huán)境變量中DEBUG的值是否等于static:app
const {promisify} = require('util')
let handlebars = require('handlebars');

const config = require('./config')
const stat = promisify(fs.stat)
const readDir = promisify(fs.readdir)

//獲取編譯模板
function getTemplet() {
    let tmpl = fs.readFileSync(path.resolve(__dirname, 'template', 'list.html'), 'utf8');
    return handlebars.compile(tmpl);
}
class Server {
    constructor(argv) {
        this.config = Object.assign({}, config, argv);
        this.list = getTemplet()
    }
    //啟動服務(wù)
    start() {
        let server = http.createServer();
        server.on('request', this.request.bind(this))
        server.listen(this.config.port);
        let url=`http://${this.config.host}:${this.config.port}`;
        debug(`靜態(tài)服務(wù)啟動成功${chalk.green(url)}`);
    }
    async request(req, res) {//服務(wù)監(jiān)聽函數(shù)
        let pathName = url.parse(req.url).path;
        let filePath = path.join(this.config.root, pathName);
        if (filePath.indexOf('favicon.ico') > 0) {
            this.sendError(req, res, 'not found',404);
            return
        }
        try {//在靜態(tài)服務(wù)文件夾存在訪問的路徑內(nèi)容
            let statObj = await stat(filePath);
            if (statObj.isDirectory()) {//是文件夾
                let directories = await readDir(filePath);
                let files = directories.map(file => {
                    return {
                        filename: file,
                        url: path.join(pathName, file)
                    }
                });
                let htmls = this.list({
                    title: pathName,
                    files
                });
                res.setHeader('Content-Type', 'text/html');
                res.end(htmls);
            } else {//是文件
                this.sendContent(req, res, filePath, statObj);
            }
        } catch (err) {//靜態(tài)服務(wù)器不存在訪問內(nèi)容
            this.sendError(req, res, err);
        }
    }
    sendContent(req, res, filePath, statObj) {//向客戶端響應(yīng)內(nèi)容
        let fileType = mime.getType(filePath);
        res.setHeader('Content-Type', `${fileType};charset=UTF-8`);
        let enCoding=this.sourceGzip(req,res);
        let rs = this.getStream(filePath);//獲取文件的可讀流
        if(enCoding){//開啟壓縮傳輸模式
            rs.pipe(enCoding).pipe(res);
        }else{
            rs.pipe(res);
        }

    }
    sourceGzip(req,res){//資源開啟壓縮傳輸
    //    Accept-Encoding:gzip, deflate, sdch, br
        let encoding=req.headers['accept-encoding'];
        if(/\bgzip\b/.test(encoding)){//gzip壓縮格式
            res.setHeader('Content-Encoding','gzip');
            return zlib.createGzip();
        }else if(/\bdeflate\b/.test(encoding)){//deflate壓縮格式
            res.setHeader('Content-Encoding','deflate');
            return zlib.createDeflate();
        }else{
            return null;
        }
    }
    getStream(filePath) {//返回一個可讀流
        return fs.createReadStream(filePath);
    }
    sendError(req, res, err,errCode) {//發(fā)送錯誤
        if(errCode){
            res.statusCode=errCode;
        }else{
            res.statusCode = 500;
        }
        res.end(`${err.toString()}`)
    }
}
module.exports = Server;
```
通過以上代碼我們會發(fā)現(xiàn),這里代碼只是對像客戶端發(fā)送內(nèi)容做的sendContent方法做了修改硕糊,所以院水,這里將會只講sendContent以及sendContent里面與壓縮相關(guān)的sourceGzip方法:
那么我們一起來看看sendContent和sourceGzip方法吧,代碼如下:
```javascript
    sendContent(req, res, filePath, statObj) {//向客戶端響應(yīng)內(nèi)容
            let fileType = mime.getType(filePath);
            res.setHeader('Content-Type', `${fileType};charset=UTF-8`);
            let enCoding=this.sourceGzip(req,res);//調(diào)用sourceGzip简十,來實現(xiàn)資源壓縮傳輸
            let rs = this.getStream(filePath);//獲取文件的可讀流
            if(enCoding){////如果客戶端支持壓縮格式傳輸檬某,那么就以壓縮方式傳輸數(shù)據(jù)
                rs.pipe(enCoding).pipe(res);//向客戶端發(fā)送壓縮格式數(shù)據(jù)
            }else{
                rs.pipe(res);
            }

        }
     sourceGzip(req,res){//資源開啟壓縮傳輸
         //    Accept-Encoding:gzip, deflate, sdch, br,客戶端會發(fā)送這樣的請求頭螟蝙,給服務(wù)器判斷
             let encoding=req.headers['accept-encoding'];//獲取客戶端發(fā)送的壓縮相關(guān)的請求頭信息恢恼,
             if(/\bgzip\b/.test(encoding)){//客戶端支持gzip壓縮格式
                 res.setHeader('Content-Encoding','gzip');//設(shè)置請求頭
                 return zlib.createGzip();//創(chuàng)建并返回一個Gzip流對象
             }else if(/\bdeflate\b/.test(encoding)){//客戶端支持deflate壓縮格式
                 res.setHeader('Content-Encoding','deflate');//設(shè)置請求頭
                 return zlib.createDeflate();//創(chuàng)建并返回一個Deflate流對象
             }else{//代表客戶端不支持壓縮格式數(shù)據(jù)傳輸,
                 return null;
             }
         }

```

以上就是對實現(xiàn)數(shù)據(jù)壓縮傳輸?shù)拇a實現(xiàn)說明胰默,那么到這里厅瞎,總共就已經(jīng)實現(xiàn)了三個功能(讀取靜態(tài)文件、MIME類型的支持初坠,支持壓縮),對應(yīng)的代碼文件appGzip.js github地址;

功能實現(xiàn)——斷點續(xù)傳(同樣是在appGzip.js的基礎(chǔ)上繼續(xù)開發(fā))

因為現(xiàn)在的完整代碼越來越多了彭雾,所以我這里就不再貼完整的代碼了碟刺,就貼對應(yīng)功能的核心代碼吧,最后再附上完整的文件鏈接地址薯酝。這個功能主要是在獲取文件流的方法getStream里面去擴展的半沽,斷點續(xù)傳的個核心功能如下:

  getStream(req,res,filePath,statObj) {//返回一個可讀流
          let start = 0;//可讀流的起司位置
          let end = statObj.size - 1;//可讀流的結(jié)束位置
          let range = req.headers['range'];//獲取客戶端的range請求頭信息,Server通過請求頭中的Range: bytes=0-xxx來判斷是否是做Range請求
          if (range) {//斷點續(xù)傳
              res.setHeader('Accept-Range', 'bytes');
              res.statusCode = 206;//返回指定內(nèi)容的狀態(tài)碼
              let result = range.match(/bytes=(\d*)-(\d*)/);//斷點續(xù)傳的分段內(nèi)容
              if (result) {
                  start = isNaN(result[1]) ? start : parseInt(result[1]);
                  end = isNaN(result[2]) ? end : parseInt(result[2]) - 1;
              }
          }
          return fs.createReadStream(filePath, {//返回一個指定起始位置和結(jié)束位置的可讀流
              start, end
          });
      }

那么上面的代碼就已經(jīng)實現(xiàn)了文件的斷點續(xù)傳了吴菠,對應(yīng)完整代碼文件github地址;接下來者填,將繼續(xù)實現(xiàn)【支持緩存與緩存控制】這樣一個功能點;

功能實現(xiàn)——斷點續(xù)傳(同樣是在前面所有已完成功能基礎(chǔ)上繼續(xù)開發(fā))

之所以要實現(xiàn)緩存的支持與控制做葵,主要是為了讓客戶端在訪問服務(wù)端時以最小的數(shù)據(jù)傳輸量得到服務(wù)端最新的資源占哟。其實現(xiàn)代碼如下:

sendContent(req, res, filePath, statObj) {//向客戶端響應(yīng)內(nèi)容
        if (this.checkCache(req, res, filePath, statObj)) return; //通過sendContent方法實現(xiàn)緩存校驗
        let fileType = mime.getType(filePath);
        res.setHeader('Content-Type', `${fileType};charset=UTF-8`);
        let enCoding=this.sourceGzip(req,res);
        let rs = this.getStream(req,res,filePath,statObj);//獲取文件的可讀流
        if(enCoding){//開啟壓縮傳輸模式
            rs.pipe(enCoding).pipe(res);
        }else{
            rs.pipe(res);
        }

    }
    checkCache(req,res,filePath,statObj){//校驗緩存
        let ifModifiedSince = req.headers['if-modified-since'];//當(dāng)資源過期時(使用Cache-Control標(biāo)識的max-age),發(fā)現(xiàn)資源具有Last-Modified聲明,則再次向服務(wù)器請求時帶上頭If-Modified-Since榨乎。
        let isNoneMatch = req.headers['is-none-match'];//客戶端想判斷緩存是否可用可以先獲取緩存中文檔的ETag怎燥,然后通過If-None-Match發(fā)送請求給Web服務(wù)器詢問此緩存是否可用。
        res.setHeader('Cache-Control', 'private,max-age=10');//Cache-Control private 客戶端可以緩存,max-age=10 緩存內(nèi)容將在10秒后失效
        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString());//服務(wù)器響應(yīng)消息頭字段蜜暑,在響應(yīng)http請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數(shù)據(jù)
        let etag = statObj.size;
        let lastModified = statObj.ctime.toGMTString();
        res.setHeader('ETag', etag);//ETag是實體標(biāo)簽的縮寫铐姚,根據(jù)實體內(nèi)容生成的一段hash字符串,可以標(biāo)識資源的狀態(tài)。當(dāng)資源發(fā)生改變時肛捍,ETag也隨之發(fā)生變化隐绵。 ETag是Web服務(wù)端產(chǎn)生的,然后發(fā)給瀏覽器客戶端拙毫。
        res.setHeader('Last-Modified', lastModified);//服務(wù)器文件的最后修改時間
        if (isNoneMatch && isNoneMatch != etag) {//緩存過期
            return false;
        }
        if (ifModifiedSince && ifModifiedSince != lastModified) {//換存過期
            return false;
        }
        if (isNoneMatch || ifModifiedSince) {//緩存有效
            res.writeHead(304);
            res.end();
            return true;
        } else {//緩存無效
            return false;
        }

    }

那么以上代碼就已經(jīng)把靜態(tài)服務(wù)器的【讀取靜態(tài)文件依许、MIME類型支持、支持壓縮恬偷、支持?jǐn)帱c續(xù)傳悍手、支持緩存與緩存控制】這些功能都已經(jīng)實現(xiàn)了,完整的代碼文件GitHub地址,接下來將要實現(xiàn)命令行調(diào)用我們的靜態(tài)文件服務(wù)器啟用袍患;

功能實現(xiàn)——命令行調(diào)用

命令行調(diào)用的功能主要是什么坦康?
如果沒有命令行調(diào)用,如果我們想要執(zhí)行我們這個app.js诡延,那么就只能是先cmd進入命令行面板滞欠,然后在里面輸入node app.js才能執(zhí)行app.js。如果我們做了命令行調(diào)用肆良,那么我們只需要自定義一個命令假如叫Myserver筛璧,這個命令主要功能主要就是執(zhí)行app.js,那么我們在cmd命令行里面就只要輸入Myserver就能實現(xiàn)了,而且還可以通過命令行來實現(xiàn)傳參惹恃。例如:我們平時看電腦的ip地址時夭谤,我們可以在命令行中輸入ipconfig,就會顯示信息巫糙,也可以通過ipconfig /all 這樣一個命令來顯示完整信息朗儒,那么后面的這個/all就相當(dāng)于一個篩選參數(shù)了,這樣子就想Linux里面的命令一樣了参淹,這里就不再做太多說明了醉锄,這里主要講一下如何將我們的靜態(tài)服務(wù)器通過命令行來調(diào)用;
首先在package.json中提供一個bin字段浙值,主要是將包里包含可執(zhí)行文件恳不,通過設(shè)置這個字段可以將它們包含到系統(tǒng)的PATH中,這樣直接就可以運行开呐。我這里添加的bin字段如下:
javascript "bin": { "rcw-staticserver": "bin/app" }
這里是主要是將rcw-staticserver這個字段設(shè)置到系統(tǒng)PATH當(dāng)中去烟勋,然后記得一定要運行一次npm link规求,從而將命令執(zhí)行內(nèi)容路徑改到,bin/app文件來神妹。那么我這里就能通過在命令行輸入rcw-staticserver來啟動我的靜態(tài)文件服務(wù)器了颓哮。那么bin文件夾下的app文件代碼內(nèi)容如下:

```javascript

    #! /usr/bin/env node     //這段代碼一定要寫在開頭,為了兼容各個電腦平臺的差異性
    const yargs = require('yargs');//yargs模塊鸵荠,主要是用它提供的argv對象冕茅,用來讀取命令行參數(shù)
    let Server = require('../src/appCache.js');
    const child = require('child_process');
    const path=require('path')
    const os = require('os');
    let argv = yargs.option('d', {//通過-d別名或者--root 文件夾名稱來指定對應(yīng)的靜態(tài)文件服務(wù)器的文件夾目錄
        alias: 'root',//指令變量名稱
        demand: 'false',//是否必傳字段
        type: 'string',//輸入值類型
        default: path.resolve(process.cwd(),'public'),//默認(rèn)值
        description: '靜態(tài)文件根目錄'//字段描述
    }).option('o', {
        alias: 'host',
        demand: 'false',
        default: 'localhost',
        type: 'string',
        description: '請配置監(jiān)聽的主機'
    }).option('p', {
        alias: 'port',
        demand: 'false',
        type: 'number',
        default: 9898,
        description: '請配置端口號'
    })
        .usage('rcw-staticserver [options]')//使用示例
        .example(
            'rcw-staticserver -d / -p 9898 -o localhost', '在本機的9898端口上監(jiān)聽客戶端的請求'
        ).help('h').argv;

    let server = new Server(argv).start();//啟動我的靜態(tài)文件服務(wù)器
```
這樣子的話我就能在命令行當(dāng)中通過輸入rcw-staticserver來直接啟動靜態(tài)文件服務(wù)器了,那么命令行調(diào)用的功能也就實現(xiàn)了蛹找。

功能實現(xiàn)——代碼發(fā)布到npm姨伤,可通過npm install -g全局安裝。

這個功能其實相對來說就很簡單了庸疾,首先要有個npm官網(wǎng)的賬號乍楚,沒有的請自覺注冊吧。命令行里通過npm login先登錄自己的npm賬號届慈,然后再運行npm publish徒溪,這個包就很輕松的發(fā)布到npm上面去了,也就可以通過npm install -g來進行全局安裝了金顿。

通過以上的操作我們一個靜態(tài)文件服務(wù)器就已經(jīng)實現(xiàn)了哦臊泌!有不好和錯誤的地方,請大家多多指教揍拆。

完整代碼GitHub地址

參考文獻:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末渠概,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嫂拴,更是在濱河造成了極大的恐慌播揪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筒狠,死亡現(xiàn)場離奇詭異猪狈,居然都是意外死亡,警方通過查閱死者的電腦和手機辩恼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門雇庙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人运挫,你說我怎么就攤上這事√赘” “怎么了谁帕?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長冯袍。 經(jīng)常有香客問我匈挖,道長碾牌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任儡循,我火速辦了婚禮舶吗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘择膝。我一直安慰自己誓琼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布肴捉。 她就那樣靜靜地躺著腹侣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪齿穗。 梳的紋絲不亂的頭發(fā)上傲隶,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音窃页,去河邊找鬼跺株。 笑死,一個胖子當(dāng)著我的面吹牛脖卖,可吹牛的內(nèi)容都是我干的乒省。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼胚嘲,長吁一口氣:“原來是場噩夢啊……” “哼作儿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起馋劈,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤攻锰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后妓雾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娶吞,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年械姻,在試婚紗的時候發(fā)現(xiàn)自己被綠了妒蛇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡楷拳,死狀恐怖绣夺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情欢揖,我是刑警寧澤陶耍,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站她混,受9級特大地震影響烈钞,放射性物質(zhì)發(fā)生泄漏泊碑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一毯欣、第九天 我趴在偏房一處隱蔽的房頂上張望馒过。 院中可真熱鬧,春花似錦酗钞、人聲如沸腹忽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽留凭。三九已至,卻和暖如春偎巢,著一層夾襖步出監(jiān)牢的瞬間蔼夜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工压昼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留求冷,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓窍霞,卻偏偏與公主長得像匠题,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子但金,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理韭山,服務(wù)發(fā)現(xiàn),斷路器冷溃,智...
    卡卡羅2017閱讀 134,661評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,167評論 25 707
  • 有些人出現(xiàn)在你的世界里钱磅,就成了短暫而美好的青春里最明媚的一道陽光。就算從此形同陌路似枕,相忘于江湖盖淡,那人也會以一種絕對...
    singlEyrie閱讀 474評論 0 0
  • 雨又在下了,看外面灰漾漾的天空凿歼,莫名的想起一段前緣往事褪迟,在這風(fēng)雨交加的暮色里,我還是渙然冰釋了答憔。想把這段銘心的往事...
    YY說閱讀 265評論 0 0
  • 媽的味赃。。實在不行虐拓,修改文件后綴為zip心俗,只要沒用動態(tài)庫,直接解壓出來放到lib里面侯嘀。
    gogoforit閱讀 2,034評論 0 1