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

Cris_冷崢子發表於2018-02-28

閱前須知

previously:

Node和http:一本通

設計思路

當你輸入一個url時,這個url可能對應伺服器上的一個資源(檔案)也可能對應一個目錄。So伺服器會對這個url進行分析,針對不同的情況做不同的事。如果這個url對應的是一個檔案,那麼伺服器就會返回這個檔案。如果這個url對應的是一個資料夾,那麼伺服器會返回這個資料夾下包含的所有子檔案/子資料夾的列表。以上,就是一個靜態伺服器所主要乾的事。

但真實的情況不會像這麼簡單,我們所拿到的url可能是錯誤的,它所對應的檔案或則資料夾或許根本不存在,又或則有些檔案和資料夾是被系統保護起來的是隱藏的,我們並不想讓客戶端知道。因此,我們就要針對這些特殊情況進行一些不同的返回和提示。

再者,當我們真正返回一個檔案前,我們需要和客戶端進行一些協商。我們需要知道客戶端能夠接受的語言型別、編碼方式等等以便針對不同瀏覽器進行不同的返回處理。我們需要告訴客戶端一些關於返回檔案的額外資訊,以便客戶端能更好的接收資料:檔案是否需要快取,該怎樣快取?檔案是否進行了壓縮處理,該以怎樣的方式解壓?等等…

至此,我們已經初步瞭解了一個靜態伺服器所主要做的幾乎所有事情,let’s go!

實現

專案目錄

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方法檢測是檔案還是資料夾

  • 如果是資料夾,利用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 initnpm link複製程式碼

然後我們就能將任意目錄當做一個靜態伺服器的工作目錄,只需在那個目錄下開啟命令列視窗輸入static-server

來源:https://juejin.im/post/5a9660fe6fb9a0634b4da9ae?utm_medium=be&utm_source=weixinqun

相關文章