關於實現一個Node.js靜態服務器你所須要知道的ALL

閱前須知

previously:html

Node和http:一本通node

設計思路

當你輸入一個url時,這個url可能對應服務器上的一個資源(文件)也可能對應一個目錄。 So服務器會對這個url進行分析,針對不一樣的狀況作不一樣的事。 若是這個url對應的是一個文件,那麼服務器就會返回這個文件。 若是這個url對應的是一個文件夾,那麼服務器會返回這個文件夾下包含的全部子文件/子文件夾的列表。 以上,就是一個靜態服務器所主要乾的事。git

但真實的狀況不會像這麼簡單, 咱們所拿到的url多是錯誤的,它所對應的文件或則文件夾或許根本不存在, 又或則有些文件和文件夾是被系統保護起來的是隱藏的,咱們並不想讓客戶端知道。 所以,咱們就要針對這些特殊狀況進行一些不一樣的返回和提示。github

再者,當咱們真正返回一個文件前,咱們須要和客戶端進行一些協商。 咱們須要知道客戶端可以接受的語言類型、編碼方式等等以便針對不一樣瀏覽器進行不一樣的返回處理。 咱們須要告訴客戶端一些關於返回文件的額外信息,以便客戶端能更好的接收數據: 文件是否須要緩存,該怎樣緩存? 文件是否進行了壓縮處理,該以怎樣的方式解壓? 等等...npm

至此,咱們已經初步瞭解了一個靜態服務器所主要作的幾乎全部事情, let's go!json

實現

項目目錄

static-server/
|
| - bin/
|   | - www   # 批處理文件
|      
|
| - src/
|   | - App.js    # main文件
|   | - Config.js   # 默認配置
|
|
·- package.json
複製代碼

配置文件

要啓動一個服務器,咱們須要知道這個服務器的啓動時的端口號數組

而在拿到用戶的請求後咱們須要在咱們本身的服務器上去查找資源,so咱們須要配置一個工做目錄瀏覽器

let config = {
    host:'localhost' //提示用
    ,port:8080 //服務器啓動時候的默認端口號
    ,path:path.resolve(__dirname,'..','test-dir') //靜態服務器啓動時默認的工做目錄
}
複製代碼

總體框架

注意緩存

  • 事件函數中的this默認指向綁定的對象(這裏是小server),這裏修改爲了Server這個大對象,以便調用在回調函數中調用Server下的方法。
class Server(){
	constructor(options){
    	/* === 合併配置參數 === */
        
    	this.config = Object.assign({},config,options)
    }
    
    start(){
    	/* === 啓動http服務 === */
        
    	let server = http.createServer();
        server.on('request',this.request.bind(this));  
        server.listen(this.config.port,()=>{
    	    let url =  `${this.config.host}:${this.config.port}`;
            console.log(`server started at ${chalk.green(url)}`)
        })
    }
    
    async request(req,res){
    	/* === 處理客戶端請求,決定響應信息 === */
        // try
        //若是是文件夾 -> 顯示子文件、文件夾列表
        //若是是文件 -> sendFile()
        // catch
        //出錯 -> sendError()
    }
    
    sendFile(){
    	//對要返回的文件進行預處理併發送文件
    }
    
    handleCache(){
    	//獲取和設置緩存相關信息
    }
    
    getEncoding(){
    	//獲取和設置編碼相關信息
    }
    
    getStream(){
    	//獲取和設置分塊傳輸相關信息
    }
    
    sendError(){
    	//錯誤提示
    }
}

module.exports = Server;
複製代碼

request請求處理

獲取url的pathname,和服務器本地的工做根目錄地址進行拼接,返回一個filename 利用filename和stat方法檢測是文件仍是文件夾bash

  • 若是是文件夾, 利用readdir方法返回該文件夾下的列表,將列表包裝成一個對象組成的數組 而後結合handlebar將數組數據編譯到模板中,最後返回這個模板給客戶端

  • 若是是文件, 將req、res、statObj、filepath傳遞給sendFile,交由sendFile處理

async request(req,res){
    let pathname = url.parse(req.url);
    if(pathname == '/favicon.ico') return; //瀏覽器會自動向咱們索取網站圖標,這裏沒有準備,爲了防止報錯,返回便可
    let filepath = path.join(this.config.root,pathname);
   	try{
        let statObj = await stat(filepath);
        if(statObj.isDirectory()){
            let files = awaity readdir(filepath);
            files.map(file=>{
                name:file
                ,path:path.join(pathname,file)
            });
            // 讓handlebar 拿着數去編譯模板
            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){
    	this.sendError(e,req,res);
    }
}
複製代碼

[tip] 咱們將request方法async化,這樣咱們就能像寫同步代碼同樣寫異步

方法

sendFile

涉及緩存、編碼、分段傳輸等功能

sendFile(){
    if(this.handleCache(req,res,filepath,statObj)) return; //若是走緩存,則直接返回。
    res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
    let encoding = this.getEncoding(req,res); //獲取瀏覽器能接收的編碼並選擇一種
    let rs = this.getStream(req,res,filepath,statObj); //支持斷點續傳
    if(encoding){
        rs.pipe(encoding).pipe(res);
    }else{
        rs.pipe(res);
    }
}
複製代碼

handleCache

緩存處理時要注意的是,緩存分爲強制緩存和對比緩存,且強制緩存的優先級是高於相對緩存的。

也就是說,當強制緩存生效的時候並不會走相對緩存,不會像服務器發起請求。

但一旦強制緩存失效,就會走相對緩存,若是文件標識沒有改變,則相對緩存生效,

客戶端仍然會去緩存數據拿取數據,因此強制緩存和相對緩存並不衝突。

強制緩存和相對緩存一塊兒使用時,能在減小服務器的壓力的同時又保持請求數據的及時更新。

另外須要注意的是,若是同時設置了兩種相對緩存的文件標識,必需要兩種都沒有改變時,緩存才生效。

handleCache(req,res,filepath,statObj){
    let ifModifiedSince = req.headers['if-modified-since']; //第一次請求是不會有的
    let isNoneMatch = req.headers['is-none-match'];
    res.setHeader('Cache-Control','private,max-age=30');
    res.setHeader('Expires',new Date(Date.now()+30*1000).toGMTString()); //此時間必須爲GMT
    
    let etag = statObj.size;
    let lastModified = statObj.ctime.toGMTString(); //此時間格式可配置
    res.setHeader('Etag',etag);
    res.setHeader('Last-Modified',lastModified);
    
    if(isNoneMatch && isNoneMatch != etag) return false; //如果第一次請求已經返回false
    if(ifModifiedSince && ifModifiedSince != lastModified) return false;
    if(isNoneMatch || ifModifiedSince){
    // 說明設置了isNoneMatch或則isModifiedSince且文件沒有改變
    	res.writeHead(304);
        res.end();
        return true;
    }esle{
    	return false;
    }
}
複製代碼

若想更詳細的瞭解緩存相關的內容,能夠閱讀個人這篇文章

304與緩存

getEncoding

從請求頭中拿取到瀏覽器能接收的編碼類型,利用正則匹配匹配出最前面那個, 建立出對應的zlib實例返回給sendFile方法,以便在返回文件時進行編碼。

getEncoding(req,res){
    let acceptEncoding = req.headers['accept-encoding'];
    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;
    }
}
複製代碼

getStream

分段傳輸,主要利用的是請求頭中的req.headers['range']來確認要接收的文件是從哪裏開始到哪裏結束,然而真正拿到這部分數據是經過fs.createReadStream來讀取到的。

getStream(req,res,filepath,statObj){
    let start = 0;
    // let end = statObj.size - 1;
    let end = statObj.size;
    let range = req.headers['range'];
    if(range){
      let result = range.match(/bytes=(\d*)-(\d*)/); //不可能有小數,網絡傳輸的最小單位爲一個字節
      if(result){
        start = isNaN(result[1])?0:parseInt(result[1]);
        // end = isNaN(result[2])?end:parseInt(result[2]) - 1;
        end = isNaN(result[2])?end:parseInt(result[2]);
      }
      res.setHeader('Accept-Range','bytes');
      res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`)
      res.statusCode = 206; //返回整個數據的一塊
    }
    return fs.createReadStream(filepath,{
      start:start-1,end:end-1
    });
  }
複製代碼

包裝成命令行工具

咱們能夠像在命令行中輸入npm start啓動一個dev-server同樣自定義一個啓動命令來啓動咱們的靜態服務器。

#! /usr/bin/env node
// -d 靜態文件根目錄
// -o --host 主機
// -p --port 端口號
let yargs = require('yargs');
let Server = require('../src/app.js');
let argv = yargs.option('d',{
  alias:'root'
  ,demand:'false' //是否必填
  ,default:process.cwd()
  ,type:'string'
  ,description:'靜態文件根目錄'
}).option('o',{
  alias:'host'
  ,demand:'false' //是否必填
  ,default:'localhost'
  ,type:'string'
  ,description:'請配置監聽的主機'
}).option('p',{
  alias:'port'
  ,demand:'false' //是否必填
  ,default:8080
  ,type:'number'
  ,description:'請配置端口號'
})
//usage 命令格式
  .usage('static-server [options]')
// example 用法實例
  .example(
    'static-server -d / -p 9090 -o localhost'
    ,'在本機9090的端口上監聽客戶端的請求'
  )
  .help('h').argv;

//argv = {d,root,o,host,p,port}
let server = new Server(argv);
server.start();

let os = require('os').platform();
let {exec} = require('child_process');
let url = `http://${argv.hostname}:${argv.port}`
if(argv.open){
  if(os === 'win32'){
    exec(`start ${url}`);
  }else{
    exec(`open ${url}`);
  }
}
複製代碼

至於原理,限於篇幅,更多詳細信息請關注這篇個人這篇文章process.argv與命令行工具

下載安裝以及使用

經過npm

npm i static-server-study
複製代碼
let static = require('static-server-study');
let server = new static({
  port:9999
  ,root:process.cwd()
});
server.start();
複製代碼

經過github

我のgithub

clone後,執行如下命令

npm init
npm link
複製代碼

而後咱們就能將任意目錄當作一個靜態服務器的工做目錄, 只需在那個目錄下打開命令行窗口輸入static-server

相關文章
相關標籤/搜索