您好,登錄后才能下訂單哦!
當你輸入一個url時,這個url可能對應服務器上的一個資源(文件)也可能對應一個目錄。 So服務器會對這個url進行分析,針對不同的情況做不同的事。 如果這個url對應的是一個文件,那么服務器就會返回這個文件。 如果這個url對應的是一個文件夾,那么服務器會返回這個文件夾下包含的所有子文件/子文件夾的列表。 以上,就是一個靜態服務器所主要干的事。
但真實的情況不會像這么簡單, 我們所拿到的url可能是錯誤的,它所對應的文件或則文件夾或許根本不存在, 又或則有些文件和文件夾是被系統保護起來的是隱藏的,我們并不想讓客戶端知道。 因此,我們就要針對這些特殊情況進行一些不同的返回和提示。
再者,當我們真正返回一個文件前,我們需要和客戶端進行一些協商。 我們需要知道客戶端能夠接受的語言類型、編碼方式等等以便針對不同瀏覽器進行不同的返回處理。 我們需要告訴客戶端一些關于返回文件的額外信息,以便客戶端能更好的接收數據: 文件是否需要緩存,該怎樣緩存? 文件是否進行了壓縮處理,該以怎樣的方式解壓? 等等...
至此,我們已經初步了解了一個靜態服務器所主要做的幾乎所有事情, let's go!
實現
項目目錄
static-server/ | | - bin/ | | - start # 批處理文件 | | | - src/ | | - App.js # main文件 | | - Config.js # 默認配置 | | ·- package.json
配置文件
要啟動一個服務器,我們需要知道這個服務器的啟動時的端口號和靜態服務器的工作目錄
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; } }
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 = startObj.size - 1; let range = req.headers['range']; if(range){ res.setHeader('Accept-Range','bytes'); res.statusCode = 206; //返回整個數據的一塊 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; //因為readstream的索引是包前又包后故要減去1 } } return fs.createReadStream(filepath,{ start,end }); }
包裝成命令行工具
我們可以像在命令行中輸入 npm start
啟動一個dev-server
一樣自定義一個啟動命令來啟動我們的靜態服務器。
大體實現的思路是: 在 packge.json 中的 bin 屬性下配置一個啟動命令和這個執行這個命令的文件的路徑。 然后我們需要準備一個批處理文件,在文件中引入我們的靜態服務器文件,讓我們的服務器跑起來 然后將這個文件 node link 即可。
總結
以上所述是小編給大家介紹的Node.js靜態服務器的實現方法,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。