這篇文章主要介紹了Node.js靜態伺服器的實作方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下
當你輸入一個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); } }
方法
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如何實作靜態伺服器的詳細內容。更多資訊請關注PHP中文網其他相關文章!