手寫啟動一個本地伺服器的命令列工具

xlei1123發表於2018-07-25

本篇主要將 實現一個自己的命令列工具,web快取,以及web檔案壓縮融合到一起,如果有部分不是太清楚的地方可以檢視文末的連結。

實現本地伺服器命令列工具 xl-server

//package.json
{
  "name": "xl-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "bin": {
    "xl-server": "bin/xl-server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^2.4.1",
    "commander": "^2.16.0",
    "debug": "^3.1.0",
  }
}
複製程式碼

我們知道實現在本地命令列中執行 xl-server 必須有bin目錄 上面的bin目錄指向了 bin/xl-server.js

// ./bin/xl-server.js
commander.on('--help', () => {
    console.log('\r\n how to use:');
    console.log('    xl-server --port <val>');
    console.log('    xl-server --host <val>');
    console.log('    xl-server --dir <val>');
})

commander
    .version('1.0.0')
    .usage('[option]')
    .option('-p,--port <n>','server port')
    .parse(process.argv)

let Server = require('../index')   //引入index檔案匯出的類
let server = new Server(commander)  //例項
server.start();   //啟動

let {exec} = require('child_process');
if(process.platform === 'win32'){ //執行調起瀏覽器 localhost:port
    exec(`start http://localhost:${server.config.port}`);    
}else{
    exec(`open http://localhost:${server.config.port}`);
}
複製程式碼

上面的commander的是一個解析和配置命令列引數的包 具體用法可以去npm官網看看用法commander 我們在index.js裡面建立服務

// index.js
let http = require('http');
let util = require('util');
let mime = require('mime');   //第三方模組 用來獲取內容型別
let chalk = require('chalk'); // 粉筆

//初步最簡單的命令列工具
let config = require('./config');


class Server {
    constructor(options) {
        this.config = {...config, ...options};    //覆蓋預設配置例如埠號
    }

    start() {
        let server = http.createServer((req,res) => {
            res.end('hello')
        });
        let { port, host } = this.config;
        server.listen(port, host, function () {
            console.log(`server start http://${host}:${chalk.green(port)}`)
        });
    }

}
module.exports = Server;
複製程式碼

還缺少一個config.js的預設配置項

module.exports = {
    port: 3000,
    host:'localhost',
    dir:process.cwd()     //當前執行目錄
}
// 執行的配置 
複製程式碼

這樣一個最簡單的 好像什麼用都沒有的命令列工具就有了 我們在當前的根目錄下執行下面的命令就可以看到瀏覽器開啟了 localhost:3000

// npm link
// xl-server
複製程式碼

讀取本地檔案目錄或檔案內容

//index.js
// 複雜一點的命令列工具 展示目錄和檔案
let http = require('http');
let url = require('url');
let path = require('path');
let fs = require('fs');
let util = require('util');
let zlib = require('zlib');
let mime = require('mime');   //第三方模組 用來獲取內容型別
// let debug = require('debug')('env')  //列印輸出 會根據環境變數控制輸出
let chalk = require('chalk'); // 粉筆
let ejs = require('ejs')    //高效的 JavaScript 模板引擎。


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

let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);

let templateStr = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
class Server {
    constructor(options) {
        this.config = {...config, ...options};
        this.template = templateStr;
    }
    async handleRequest(req, res) {               //這裡根據請求的url來讀取目錄或者檔案內容
        let { pathname } = url.parse(req.url, true);
        let realPath = path.join(this.config.dir, pathname);
        try{
            let statObj = await stat(realPath)
            if(statObj.isFile()) {   //檔案
                this.sendFile(req, res, statObj, realPath)
            } else {   //資料夾
                let dirs = await readdir(realPath);
                dirs = dirs.map(dir => ({ name: dir, path: path.join(pathname, dir) }));
                let str = ejs.render(this.template, { dirs });
                res.setHeader('Content-Type', 'text/html;charset=utf-8');
                res.end(str);
            }
        } catch (e) {
            this.sendError(req, res, e);
        }
    }
    sendError(req, res, e) {
        console.log(e),
        res.end('404')
    }
    sendFile(req, res, statObj, realPath) {
        fs.createReadStream(realPath).pipe(res)
    }
    start() {
        let server = http.createServer(this.handleRequest.bind(this));    //這裡用handleRequest來執行
        let { port, host } = this.config;
        server.listen(port, host, function () {
            console.log(`server start http://${host}:${chalk.green(port)}`)
        });
    }

}
module.exports = Server;
複製程式碼

目錄結構的模板檔案如下:

//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>Document</title>
</head>
<body>
   <!-- 實現渲染列表 {dirs:[{name:'201804',path:'/201804'},{}]} -->
   <%dirs.forEach(item=>{%>
        <li><a href="<%=item.path%>"><%=item.name%></a></li>
   <%})%>
</body>
</html>
複製程式碼

實現檔案目錄的快取和檔案壓縮

這個操作是在讀取檔案的過程中實現,所以下面只給出index.js裡面的sendFile方法裡面增加快取和壓縮

sendFile(req, res, statObj, realPath) {
    if (this.cache(req, res, statObj, realPath)) {
        res.statusCode = 304;
        res.end();
        return;
    }
    res.setHeader('Content-Type', mime.getType(realPath) + ';charset=utf-8');
    let zip = this.compress(req, res, statObj, realPath);
    if(zip) {
        return fs.createReadStream(realPath).pipe(zip).pipe(res)
    }
    fs.createReadStream(realPath).pipe(res)
}
複製程式碼

接下來就是寫上面的cache方法和compress方法了

cache(req, res, statObj, realPath) {
    res.setHeader('Cache-control','max-age=100')    //強制快取  注意即使是強制快取也不會快取主網頁
    let etag = statObj.ctime.toGMTString() + statObj.size;
    let lastModified = statObj.ctime.toGMTString();    //atime建立時間 ctime --- change time 修改時間
    res.setHeader('Etag', etag);   //Etag -- if-none-match
    res.setHeader('Last-Modified', lastModified);  //Last-Modified --- if-none-match
    let ifNoneMatch = req.headers['if-none-match'];
    let ifModifiedSince = req.headers['if-modified-since'];
    if (etag != ifNoneMatch) {       //兩種方式 第一種就行
        return false
    }
    if (lastModified !=ifModifiedSince) {       //兩種方式 第一種就行,此種只是列出304快取的另一種方式
        return false
    }
    return true
}
compress(req, res, statObj, realPat) {    //實現壓縮功能
    let encoding = req.headers['accept-encoding'];
    if (encoding) {
        if (encoding.match(/\bgzip\b/)) {
            res.setHeader('content-encoding','gzip')
            return zlib.createGzip()
        } else if (encoding.match(/\bdeflate\b/)) {
            res.setHeader('content-encoding', 'deflate')
            return zlib.createDeflate();
        } else {
            return false
        }
    } else {
        return false
    }
}
複製程式碼

寫完之後 我們

npm link 
xl-server   
//然後我們就可以看到啟動了一個localhost:3000 並且展示了當前檔案目錄
複製程式碼

原始碼分享

另外本文的三個部分具體一些的例子可以檢視下面的:

以上就是完美實現本地伺服器命令列工具,不足之處歡迎各位提出寶貴的意見或建議,也希望能幫助到你從中獲得一些知識,謝謝大家的關注!

相關文章