三種快取方式,再也不用麻煩運維小哥哥了!!!

LeslieMay發表於2018-09-03

依然在學習node的艱辛過程中,最近學習了http相關的知識,學到了東西當然第一時間就來和大家分享分享,今天呢就教大家來看看利用node中的http模組去實現不同的快取策略!!!

我們都知道,對於我們前端開發來說,快取是一個十分重要的東西,即希望使用者不能每次請求過來都要重複下載我們的頁面內容,希望為使用者節省流量,並且能提高我們頁面的瀏覽流暢度,但是同時當我們修改了一個bug後,又希望線上能夠及時更新,這時候就要求爺爺告奶奶讓運維小哥哥幫我們重新整理一下快取了,那麼有沒有一些比較好的快取策略可以針對我們修改bug又能不麻煩運維及時更新呢,今天我們就利用node來看一下後端中的快取策略是如何設定的。

強制快取

通常我們對於強制快取的設定是服務端告訴客戶端你剛剛已經請求過一次了,我們約定好十分鐘內你再過來請求都直接讀取快取吧,意思也就是當客戶端在十分鐘內多次請求的話只有第一次會下載頁面內容,其他的請求都是直接走快取,不管我們頁面在這期間有沒有變化都不會影響客戶端讀取快取。 那我們來看一下程式碼的實現

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
// 建立一個服務
let server = http.createServer();
// 監聽請求
server.on('request',(req,res)=>{
    // 獲取到請求的路徑
    let {pathname,query} = url.parse(req.url,true);
    // 將路徑拼接成伺服器上對應得檔案路徑
    let readPath = path.join(__dirname, 'public',pathname);
    console.log(readPath)
    try {
        // 獲取路徑狀態
        let statObj = fs.statSync(readPath);
        // 服務端設定響應頭 Cache-Control 也就是快取多久以秒為單位
        res.setHeader('Cache-Control','max-age=10');
        // 伺服器設定響應頭Expires 過期時間 獲取當前時間加上剛剛設定的快取秒數
        res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
        //判斷如果路徑是一件資料夾 就預設查詢該檔案下的index.html
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            console.log(p);
            // 判斷是否有index.html 沒有就返回404
            fs.statSync(p);
            // 建立檔案可讀流 並且pipe到響應res可寫流中
            fs.createReadStream(p).pipe(res)
        }else{
            // 如果請求的就是一個檔案 那麼久直接返回
            fs.createReadStream(readPath).pipe(res)
        }
    } catch (error) {
        // 讀取不到 返回404 
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未發現檔案`)
    }
})
// 監聽3000埠
server.listen(3000)
複製程式碼

三種快取方式,再也不用麻煩運維小哥哥了!!!

通過上面程式碼測試我們會發現當我們在10秒內進行對同一檔案的請求,那麼我們瀏覽器就會直接走快取 通過上圖可以看到我們重複請求的時候我們會看到css變成from memory cache,我們也看到我們剛剛的響應頭也被設定上了

三種快取方式,再也不用麻煩運維小哥哥了!!!

協商快取

上面的強制快取我們就發現了 就是我們平時改完bug上線要苦苦等待的一個原因了,那麼有沒有其他的好的快取處理方法呢,我們設想一下 假如我們能夠知道我們檔案有沒有修改,假如我們修改了伺服器就返回最新的內容假如沒有修改 就一直預設快取 ,這樣是不是聽起來十分的棒!那我們就想如果我們能夠知道檔案的最後修改時間是不是就可以實現了!

通過檔案最後修改時間來快取

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
let server = http.createServer();
server.on('request',(req,res)=>{
    // 獲取到請求的路徑
    let {pathname,query} = url.parse(req.url,true);
    // 將路徑拼接成伺服器上對應得檔案路徑
    let readPath = path.join(__dirname, 'public',pathname);
    try {
        // 獲取路徑狀態
        let statObj = fs.statSync(readPath);
        // 為了方便測試 我們告訴客戶端不要走強制快取了
        res.setHeader('Cache-Control','no-cache');
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            let statObj = fs.statSync(p);
            // 我們通過獲取到檔案狀態來拿到檔案的最後修改時間 也就是ctime 我們把這個時間通過響應頭Last-Modified來告訴客戶端,客戶端再下一次請求的時候會通過請求頭If-Modified-Since把這個值帶給服務端,我們只要判斷這兩個值是否相等,假如相等那麼也就是說 檔案沒有被修改那麼我們就告訴客戶端304 你直接讀快取吧
            res.setHeader('Last-Modified',statObj.ctime.toGMTString());
            if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                res.statusCode = 304;
                res.end();
                return
            }
            // 修改了那麼我們就直接返回新的內容
            fs.createReadStream(p).pipe(res)
        }else{
            res.setHeader('Last-Modified',statObj.ctime.toGMTString());
            if(req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
                res.statusCode = 304;
                res.end();
                return
            }
            fs.createReadStream(readPath).pipe(res)
        }
    } catch (error) {
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未發現檔案`)
    }
})

server.listen(3000)
複製程式碼

三種快取方式,再也不用麻煩運維小哥哥了!!!
我們通過請求可以看到,當我們第一次請求過後,無論怎麼重新整理請求都是304 直接讀取的快取,假如我們在服務端把這個檔案修改了 那麼我們就能看到又能請求到最新的內容了,這就是我們通過協商快取來處理的,我們通過獲取到檔案狀態來拿到檔案的最後修改時間 也就是ctime 我們把這個時間通過響應頭Last-Modified來告訴客戶端,客戶端再下一次請求的時候會通過請求頭If-Modified-Since把這個值帶給服務端,我們只要判斷這兩個值是否相等,假如相等那麼也就是說 檔案沒有被修改那麼我們就告訴客戶端304 你直接讀快取吧

通過檔案內容來快取

再再再再再假如我們在檔案中刪除了字元a然後又還原了,那麼這時候儲存我們的檔案的修改時間其實也發生了變化,但是其實我們檔案的真正內容並沒有發生變化,所以這時候其實客戶端繼續走快取也是可以的 ,我們來看看這樣的快取策略如何實現。

let http = require('http');
let path = require('path');
let fs = require('fs');
let url = require('url');
let crypto = require('crypto');
let server = http.createServer();
server.on('request',(req,res)=>{
    // 獲取到請求的路徑
    let {pathname,query} = url.parse(req.url,true);
    // 將路徑拼接成伺服器上對應得檔案路徑
    let readPath = path.join(__dirname, 'public',pathname);
    try {
        // 獲取路徑狀態
        let statObj = fs.statSync(readPath);
        // 為了方便測試 我們告訴客戶端不要走強制快取了
        res.setHeader('Cache-Control','no-cache');
        if(statObj.isDirectory()){
            let p = path.join(readPath,'index.html');
            let statObj = fs.statSync(p);
            // 我們通過流把檔案讀取出來 然後對讀取問來的內容進行md5加密 得到一個base64加密hash值
            let rs = fs.createReadStream(p);
            let md5 = crypto.createHash('md5');
            let arr = [];
            rs.on('data',(data)=>{
                arr.push(data);
                md5.update(data);
            })
            rs.on('end',(data)=>{
                let r = md5.digest('base64');
                // 然後我們將這個hash值通過響應頭Etag傳給客戶端,客戶端再下一次請求的時候會把上一次的Etag值通過請求頭if-none-match帶過來,然後我們就可以繼續比對檔案生成的hash值和上次產生的hash是否一樣 如果一樣說明檔案內容沒有發生變化 就告訴客戶端304 讀取快取
                res.setHeader('Etag',r);
                if(req.headers['if-none-match']===r){
                    res.statusCode=304;
                    res.end();
                    return;
                }
                res.end(Buffer.concat(arr))
            })
        }else{
            let rs = fs.createReadStream(readPath);
            let md5 = crypto.createHash('md5');
            let arr = [];
            rs.on('data',(data)=>{
                arr.push(data);
                md5.update(data);
            })
            rs.on('end',(data)=>{
                let r = md5.digest('base64');
                res.setHeader('Etag',r);
                if(req.headers['if-none-match']===r){
                    res.statusCode=304;
                    res.end();
                    return;
                }
                res.end(Buffer.concat(arr))
            })
        }
    } catch (error) {
        console.log(error)
        res.setHeader('Content-Type','text/html;charset=utf8')
        res.statusCode = 404;
        res.end(`未發現檔案`)
    }
})

server.listen(3000)
複製程式碼

三種快取方式,再也不用麻煩運維小哥哥了!!!

通過控制檯我們可以看出來 請求頭和響應頭中都有我們上面所說的對應的值,但是從程式碼裡我們也能看出來,我們每次在請求到來的時候都會把檔案全部讀取出來並且進行加密生產hash然後再做對比,這樣其實十分的消耗效能,因此這種快取方式也有他自己的缺點

總結

我們通過node來親自實現了三種快取方式,我們可以總結出每種快取方式對應的實現:

  • 強制快取 服務端設定響應頭Cache-Control:max-age=xxx,並且設定Expires響應頭過期時間,客戶端自行判斷是否讀取快取
  • 協商快取 通過狀態碼304告訴客戶端該走快取
    • 修改時間:通過檔案的最後修改時間判斷該不該讀取快取,服務端設定響應頭Last-Modified,客戶端把上次服務端響應頭中的Last-modified值通過if-modified-since 傳遞給服務端 , 服務端通過比較當前檔案的修改時間和上次修改時間(上次傳給客戶端的值),如果相等那麼說明檔案修改時間沒變也就是沒變化
    • 檔案內容:通過檔案的內容來判斷該不該讀取快取,服務端通過把檔案內容讀取出來,通過md5進行base64加密得出hash值,把這個值設定響應頭Etag,客戶端下一次請求通過if-none-match帶過來,服務端再比對當前檔案內容加密得出的hash值和上次是否一樣,如果一樣說明檔案內容沒有發生改變,這種方式是最準確的方式,但是也是最耗效能

相關文章