node中的http會了嗎? 來手寫一個屬於本身的'cgp-server'靜態服務

序言

手寫一個靜態服務能夠對node中http模塊有更深的理解,這是咱們的初衷。http-server相信你們都用過,這裏咱們要實現相似個功能。功能以下css

  • 啓動咱們寫好的模塊後,輸入localhost:3000打開咱們public目錄下的文件(默認打開index.html)
  • 用到debug插件,主要用於在命令行輸出一些日誌,咱們只用基本的功能,因此沒有難點。不會戳這裏
  • 可能用到chalk插件,就是把命令行輸出的日誌五光十色,變得好看,沒什麼太大做用,用法戳這裏

準備工做

咱們的目錄解構以下

  • 你們應該一看就懂啦,啓動咱們服務,自動打開public/index.html
  • bin/www.js 是咱們後面用命令行啓動服務的配置
  • public是咱們的靜態目錄
  • app.js 主文件
  • config.js 配置文件
  • tmpl.html是咱們用ejs編譯的模板,後面講到

先寫最簡單的config.js

let path = require('path');
let config = {
    hostname:'127.0.0.1', //默認主機
    port:3000,  //默認端口
    dir:path.join(__dirname,'','public') //默認打開的目錄(絕對路徑)
};
module.exports = config;
複製代碼

以上代碼都能看得懂,下面開始寫咱們主文件html

核心代碼 app.js

一、引入所需的依賴包
let http = require('http');
let url = require('url');
let path = require('path');
let util = require('util');
let fs = require('fs');
let zlib = require('zlib');
let mime = require('mime'); // 獲得內容類型
let debug = require('debug')('*'); // 打印輸出 會根據環境變量控制輸出
let chalk = require('chalk'); // 粉筆
let ejs = require('ejs'); // 模板引擎

//先聲明好,下面解釋
let config = require('./config');
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
let readdir = util.promisify(fs.readdir);
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //讀取ejs的模板文件
複製代碼
  • mime解析文件給你內容類型,用法
  • ejs渲染引擎,咱們用最簡單功能,不會也能看懂
二、http模塊開啓服務
/*運行的條件 指定主機名
* 指定啓動的端口號
* 指定運行的目錄
 */
let config = require('./config'); //引入配置文件
class Server { //聲明類
    constructor() {
        this.config = config; //講配置掛載再咱們的實例上
    }
    handleRequest(req,res){ //確保這裏的this都是實例

    }
    start(){//服務開始的方法
        let server =http.createServer(this.handleRequest.bind(this));
        let {hostname,port} = this.config; //解構主機名和端口
        server.listen(port,hostname);
        debug(`http://${hostname}:${port} start`) //命令行中打印
    }
}
//開啓一個服務
let server = new Server();
server.start(); //調用start方法

複製代碼

截至到目前位置,簡單的服務已經開啓了,先來測試下效果吧node

完美,控制檯打印出了內容,git

三、實現handleRequest方法,即處理請求邏輯

列出咱們要作什麼github

  • 解析url的路徑名
  • 與默認配置中路徑(G://cgp-server/public)拼接
  • 判斷是文件仍是文件夾仍是404
let stat = util.promisify(fs.stat);//promise化 fs.stat方法
 async  handleRequest(req,res){ //確保這裏的this都是實例
        let {pathname} = url.parse(req.url,true); //獲取url的路徑
        let p = path.join(this.config.dir,pathname); // 多是G:/cgp-server/public 多是G://cgp-server/public/index.html
        //一、根據路徑 返回不一樣結果 若是是文件夾 顯示文件夾裏的內容
        //二、若是是文件 顯示文件的內容
        try{
            let statObj=await stat(p);
        }catch (e) {
            //文件不存在狀況
            this.sendError(req,res,e)
        }
    }
複製代碼

try catch用於捕獲錯誤,當文件不存在,調用sendError方法,先來實現這個錯誤的處理方法npm

四、文件不存在的邏輯,sendError()
sendError(req,res,e){
        debug(util.inspect(e)); //輸出錯誤,util模塊提供方法
        res.statusCode = 404;
        res.end('Not Found');
    }
複製代碼

寫了這麼多了,測試下錯誤文件可否打印錯誤 json

測試完美,此時咱們應該判斷打開的是文件仍是目錄,並給對應的方法,下面咱們開始目錄的渲染方法api

五、ejs渲染目錄列表

  • 先聲明一個template模板,掛載到實例上
let template = fs.readFileSync(path.join(__dirname,'tmpl.html'),'utf8'); //讀取ejs的模板文件
class Server{
    constructor(){
        this.template = template //掛載到實例上
    }
}
複製代碼
  • 若是是目錄,渲染出一個html頁展現目錄結構
if(statObj.isDirectory()){
                //若是是目錄 列出目錄內容能夠點擊
                let dirs = await readdir(p); //public下面的目錄結構=>[index.html,style.css]
                dirs =dirs.map(dir=>{
                    return {
                        filename:dir,
                        path:path.join(pathname,dir)
                    }
                });
                //dirs就是要渲染的數據 
                //格式以下[{filename:index.html,path:'/index.html'},{{filename:style.css,path:''/style.css}}]
                let str =ejs.render(this.template,{dirs}); //ejs渲染方法
                // console.log(str);
                res.setHeader('Content-Type', 'text/html;charset=utf-8');
                res.end(str);
            }
複製代碼
  • 咱們來看下tmpl.html模板是怎麼寫的,ejs用法,咱們只用最簡單的,因此應該能看懂
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
//循環dirs中的內容到頁面中
<% dirs.map(item=>{%>
    <li><a href="<%=item.path%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>

複製代碼

渲染目錄結構,咱們已經寫完了,測試下看能不能運行promise

目前來看,無bug,接下來實現若是是文件的話,直接把文件內容渲染出來瀏覽器

六、文件的渲染方法,即this.sendFile()方法

sendFile(req,res,p,statObj){
     res.setHeader('Content-Type', mime.getType(p) + ';charset=utf-8');
        fs.createReadStream(p).pipe(res);//可讀流pipe到可寫流
}
複製代碼

功能已經實現拉,不信咱們測下

  • 功能已經實現,但咱們要求再增長三個功能
  • 一、檢測是否支持緩存
  • 二、檢測是否支持壓縮
  • 三、檢測是否支持範圍請求
6.1增長緩存功能
  • 修改下sendFile()方法添加三個功能
sendFile(req,res,p,statObj){
        // 一、檢測是否有緩存
        if(this.cache(req,res,p,statObj)){ //若是有緩存
            res.statusCode = 304;
            res.end();
            return
        }
        //二、檢測是否支持壓縮
            ....
        //三、檢測是否有範圍請求
            ....
        
    }
複製代碼
  • cache()緩存方法

緩存有兩種方式,強制緩存和協商緩存

  • 強制緩存 服務端Catch-Control 、 Expires
  • 協商緩存 服務端Last-Modified 、Etag
  • 協商緩存 客戶端if-modified-since if-none-match 與服務端對應
  • 貼下百度緩存的解構給你們看下

看完這個圖,相信你們應該懂啦。下面開始寫緩存方法

cache(req,res,p,statObj){  //實現緩存
        /* 強制緩存 服務端 Cache-Control Expires
            協商緩存  服務端 Last-Modified Etag
            協商緩存  客戶端 if-modified-since  if-none-match
            etag ctime + 文件的大小
            Last-modified ctime
            強制緩存
            */
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString());//10秒後從新發請求
        let etag = statObj.ctime.toGMTString() + statObj.size; //文件修改時間和文件大小
        let lastModified = statObj.ctime.toGMTString(); //文件的修改時間
        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) { //不相等,不走緩存
            return false;
        }
        if (lastModified != ifModifiedSince) { //同理
            return false;
        }
        return true; //不然走緩存
    }

複製代碼

緩存功能寫完了,咱們測試下設置的頭有沒有添加上

緩存咱們就已經實現啦

6.2 實現壓縮功能
  • node中zlib提供壓縮功能,這裏就不講怎麼用啦,用法戳官網
gzip(req,res,p,statObj){
        // 客戶端 Accept-Encoding: gzip, deflate, br
        // 服務端 Content-Encoding: gzip
        let encoding = req.headers['accept-encoding']; //獲取請求頭的接收的壓縮格式
        if (encoding) {
            if (encoding.match(/\bgzip\b/)) {
                res.setHeader('Content-Encoding', 'gzip')
                return zlib.createGzip();//返回一個gzip的壓縮流
            } else if (encoding.match(/\bdeflate\b/)) {
                res.setHeader('content-encoding', 'deflate');
                return zlib.createDeflate(); //返回createDeflate的壓縮流
            } else {
                return false; //不然不支持壓縮
            }
        } else {
            return false;//不然不支持壓縮
        }
    }
複製代碼
  • 修改下sendFile()方法
sendFile(req,res,p,statObj){
        // 一、檢測是否有緩存
        if(this.cache(req,res,p,statObj)){ //若是有緩存
            res.statusCode = 304;
            res.end();
            return
        }
        //二、檢測是否支持壓縮
        res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
        let compress =this.gzip(req,res,p,statObj);
        if(compress){ //檢測是否壓縮。返回的是壓縮流
           return fs.createReadStream(p).pipe(compress).pipe(res);
        }else{ //不支持壓縮直接把文件讀出來便可
           return fs.createReadStream(p).pipe(res)
        }
        //三、檢測是否有範圍請求
            ....
        
    }
複製代碼

用1.txt文件測試下

目前來看都還ok,還剩最後一個功能,實現範圍請求

6.3 實現範圍請求功能
  • 客戶端發送Range:bytes=0-3
  • 服務端對應Accept-Range:bytes Content-Range:bytes 0-3/xxx Content-Length:xxx

因爲可能同時會有壓縮和範圍請求,咱們稍微改下前面的代碼

sendFile(req,res,p,statObj){
        // 一、檢測是否有緩存
            ....
        //二、檢測是否支持壓縮同時加上範圍請求
        res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
        let compress =this.gzip(req,res,p,statObj);
        let {start,end} = this.range(req,res,p,statObj); //解構開始和結束的位置
        if(compress){ //檢測是否壓縮。返回的是壓縮流
           return fs.createReadStream(p,{start,end}).pipe(compress).pipe(res);
        }else{
            // res.setHeader("Content-Type",mime.getType(p)+";charset=utf8");
           return fs.createReadStream(p,{start,end}).pipe(res)
        }

    }
複製代碼
  • range()範圍請求的方法
range(req, res, statObj, p) {
        //客戶端 Range:bytes=0-3
        //服務端 Accept-Range:bytes Content-Range:bytes 0-3/8777

        let range = req.headers['range']; //若是有範圍請求
        if (range) {
            let [, start, end] = range.match(/(\d*)-(\d*)/); //解構出開始和結束的位置
            start = start ? Number(start) : 0; //start設置默認值
            end = end ? Number(end) : statObj.size - 1; //end設置默認值
            res.statusCode = 206; //狀態碼 206範圍請求
            res.setHeader('Accept-Ranges',"bytes");
            res.setHeader('Content-Length',end-start+1);
            res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`);
            return {start,end};
        }else {
            return {start:0, end:statObj.size};
        }
    }
複製代碼

基本功能已經實現,測試下代碼,咱們用curl工具發送請求,用法

  • 1.txt的內容123456789。咱們只想要前4個字符

測試完美,接下來咱們還想實現輸入cgp-server ,自動開啓瀏覽器,打開目錄。咱們須要引用一個模塊 yargs

七、yargs模塊配置命令行的輸入

  • yargs配置用法`
  • 這裏咱們只用最基本用法,一看就懂,詳細瞭解請看官網
7.1 npm link做用
  • npm link命令能夠將一個任意位置的npm包連接到全局執行環境,從而在任意位置使用命令行均可以直接運行該npm包。

7.2 修改下package.json文件

7.3 www.js的配置
7.3.1咱們把app.js主文件導出給www.js使用

7.3.2修改www.js文件
#! /usr/bin/env node //執行命令後會執行 bin/www.js
const yargs = require('yargs');
let argv = yargs.option('port',{ //yargs的基礎用法
    alias: 'p', //別名
    default: 3000, //默認值
    description:'this is port',  //描述
    demand:false // 是否必須
}).option('hostname',{
    alias: 'h',
    default: 'localhost',
    description:'this is hostname',
    demand:false
}).option('dir',{
    alias: 'd',
    default: process.cwd(),
    description:'this is cwd',
    demand:false
}).usage('cgp-server [options]' ).argv;

//開啓服務
let Server = require('../src/app.js');
new Server(argv).start();

// 判斷是win仍是mac平臺
let platform = require('os').platform();
//開啓子進程
let {exec} = require('child_process');
//win系統   win32
if(platform==="win32"){
    exec(`start http://${argv.hostname}:${argv.port}`)
}else {
    exec(`open http://${argv.hostname}:${argv.port}`)
}
複製代碼
  • 簡單介紹yargs用法。而後咱們輸入 cgp-server --help看效果
yargs.option('port',{ //yargs的基礎用法
    alias: 'p', //別名
    default: 3000, //默認值
    description:'this is port',  //描述
    demand:false // 是否必須
})
複製代碼

  • 解釋下流程,先開啓服務,而後判斷系統,再而後根據不一樣的平臺執行自動打開瀏覽器

測試下看能不能啓動

結尾

若是你能看到這裏,真的不容易,點個贊再走吧,源碼分享給你

相關文章
相關標籤/搜索