手把手體驗http-server服務理解強緩存和協商緩存

前提:

咱們先來體驗下npm包http-server的功能 html

alt
訪問下試試,有點牛皮的樣子
訪問下html試試
直接展現出來,是否是有種後臺中出渲染的感受

實現

下面咱們來整一個吧node

咱們先來整理下步驟:webpack

  1. 建立一個http服務應用
  2. 可顯示文件直接顯示
  3. 目錄文件咱們要列出目錄裏面全部的目錄和文件 你們可能會有疑問,第3個怎麼展現呢,咱們可使用模板呀

開發第一步:開發語法選擇問題

  • 有的人會說,我不會node呀,我可使用es6語法麼,答案確定是能夠的
  • 下面我來介紹下,咱們可使用babel來轉義成node支持的commonjs語法呀

@babel/core 主要是babel的核心模塊
@babel/preset-env env這個預設是es6轉es5的插件集合
babel-loader 是webpack和loader的橋樑
說到用到babel,那確定少不了配置文件.babelrc文件啦,配置以下es6

{
  "presets": [
    ["@babel/preset-env", {
      "targets":{
        "node": "current"
      }
    }]
  ]
}
複製代碼

例如咱們使用es6編寫的代碼是放在src目錄下,咱們能夠寫一個npm scripts 經過babel轉成commonjsweb

"scripts": {
    "babel:dev": "babel ./src -d ./dist --watch",
    "babel": "babel ./src -d ./dist"
  },
複製代碼

這邊是將咱們的源碼babel轉移後到dist目錄,用戶使用到的其實就是咱們dist目錄內容了算法

npm包的使用習慣

正常咱們開發的npm包怎麼調試呢
答案可能有不少哈,我這邊的話主要是推薦使用npm link 或者是sync-files(只是同步文件,須要配置node_modules對應的文件目錄)
npm link是一種軟鏈的方式去把當前目錄 作一個軟鏈到node全局安裝的目錄下,這樣咱們不管在哪裏均可以使用了,其實真正訪問的仍是本地npm包的目錄 npm

sync-files模式

//scripts指令:
sync-files --verbose ./lib $npm_config_demo_path
// 這邊用到了npm的變量$npm_config_demo_path,須要配置.npmrc的文件
// demo_path = 實際要替換的依賴包地址
複製代碼

上面的npm link 還沒說完,npm包要告訴我當前的包要從哪裏開始執行怎麼整呢
配置bin或者main方法
promise

"bin": {
  "server": "./bin/www"
}, // 這邊的指令名稱能夠隨便起哈
複製代碼

第一步咱們都介紹完了,咱們要真正開始來實現了瀏覽器

第二步 代碼實現

模板文件template.html緩存

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>模板數據</title>
</head>
<body>
  <ul>
    <% dirs.forEach(dir => {%>
      <li><a href="<%=pathname%>/<%=dir%>"><%=dir%></a></li>
    <% }) %>
  </ul>
</body>
</html>
複製代碼

main.js文件主要是讓咱們作一個相似於http-server腳手架的一個展現信息,咱們接收參數,能夠引用commander這個包

import commander from "commander";
import Server from './server';

commander.option('-p, --port <val>', "please input server port").parse(process.argv);
let config = {
  port: 3000
}
Object.assign(config, commander);
const server = new Server(config);
server.start();
複製代碼

這個文件裏面咱們只是監聽了命令的入參,可使用-p 或者--p來傳一個參數
若是不傳,我這邊會有個初始的端口
而後咱們傳給了server.js

import fs from "fs";
import http from 'http';
import mime from 'mime';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
const { readdir, stat } = fs.promises;
// 同步讀取下template.html文件內容
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
  constructor(config){
    this.port = config.port;
    this.template = template;
  }
  /** * 處理請求響應 */
  async handleRequest(req, res){
    // 獲取請求的路徑和傳參 
    let {pathname, query} = url.parse(req.url, true);
    // 轉義中文文件名處理(解決中文字符轉義問題)
    pathname = decodeURIComponent(pathname);
    // 下面是解決 // 訪問根目錄的問題
    let pathName = pathname === '/' ? '': pathname;
    let filePath = path.join(process.cwd(), pathname);
    try {
      // 獲取路徑的信息 
      const statObj = await stat(filePath);
      // 判斷是不是目錄
      if(statObj.isDirectory()) {
        // 先遍歷出全部的目錄節點
        const dirs = await readdir(filePath);
        // 若是當前是目錄則經過模板來解析出來
        const content = ejs.render(this.template, {
          dirs,
          pathname:pathName
        });
        res.setHeader('Content-Type', 'text/html;charset=utf-8');
        res.statusCode = 200;
        res.end(content);
      } else {
        // 若是是文件的話要先讀取出來而後顯示出來
        this.sendFile(filePath, req, res, statObj);
      }
    }catch(e) {
      // 出錯了則拋出404
      this.handleErr(e, res);
    }
  }
  /** * 處理異常邏輯 * @param {*} e * @param {*} res */
  handleErr(e, res){
    console.log(e);
    res.setHeader('Content-Type', 'text/plain;charset=utf-8');
    res.statusCode = 404;
    res.end('資源未找到')
  }
  
  /** * 處理文件模塊 */
  sendFile(filePath, req, res, statObj){
    console.log(chalk.cyan(filePath));
    res.statusCode = 200;
    let type = mime.getType(filePath);
    // 當前不支持壓縮的處理方式
    res.setHeader('Content-Type', `${type};charset=utf-8`);
    fs.createReadStream(filePath).pipe(res);
  }
  start(){
    // 建立http服務 
    let server = http.createServer(this.handleRequest.bind(this));
    server.listen(this.port, () => {
      console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
    })
  }
}

export default Server;
複製代碼

總計下:

  1. 獲取到請求過來的路徑pathname,首先咱們要處理下中文兼容性的問題,瀏覽器會幫咱們urlEncode,因此咱們要decodeURIComponent一下
    還有pathname默認爲'/',咱們要判斷下,爲了模板裏面跳轉邏輯準備的, // 會指向到根目錄去
  2. 判斷當前的路徑對應的是文件夾仍是文件
  3. 文件的話,使用mime包查詢到當前文件的類型,設置Content-type,把文件讀出來的流給到響應流返回
  4. 若是是文件夾的話,咱們須要使用模板文件內容經過ejs傳入readdir的目錄結果和pathname去渲染頁面,res返回的Content-type設置爲text/html
  5. 若是沒有找到直接返回http 404 code碼

上面其實就實現了個簡單的http-server
那麼咱們想下咱們能作些什麼優化呢???

優化點

  1. 壓縮
  2. http緩存

優化點一:壓縮方案

怎麼壓縮呢,咱們來看下http請求內容吧,裏面可能會注意到

Accept-Encoding: gzip, deflate, br
複製代碼

瀏覽器支持什麼方式咱們就是什麼方式
咱們使用zlib包來作壓縮操做吧
代碼走一波

import fs from "fs";
import http from 'http';
import mime from 'mime';
import crypto from 'crypto';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
import zlib from 'zlib';
const { readdir, stat } = fs.promises;
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
  constructor(config){
    this.port = config.port;
    this.template = template;
  }
  /** * 壓縮文件處理 */
  zipFile(filePath, req, res){
    // 使用zlib庫去壓縮對應的文件
    // 獲取請求頭數據Accept-Encoding來識別當前瀏覽器支持哪些壓縮方式
    const encoding = req.headers['accept-encoding'];
    console.log('encoding',encoding);
    // 若是當前有accept-encoding 屬性則按照匹配到的壓縮模式去壓縮,不然不壓縮 gzip, deflate, br 正常幾種壓縮模式有這麼幾種
    if(encoding) {
      // 匹配到gzip了,就使用gzip去壓縮
      if(/gzip/.test(encoding)) {
        res.setHeader('Content-Encoding', 'gzip');
        return zlib.createGzip();
      } else if (/deflate/.test(encoding)) { // 匹配到deflate了,就使用deflate去壓縮
        res.setHeader('Content-Encoding', 'deflate');
        return zlib.createDeflate();
      }
      return false;
    } else {
      return false;
    }
  }
  /** * 處理請求響應 */
  async handleRequest(req, res){
    let {pathname, query} = url.parse(req.url, true);
    // 轉義中文文件名處理
    pathname = decodeURIComponent(pathname);
    let pathName = pathname === '/' ? '': pathname;
    let filePath = path.join(process.cwd(), pathname);
    try {
      const statObj = await stat(filePath);
      if(statObj.isDirectory()) {
        // 先遍歷出全部的目錄節點
        const dirs = await readdir(filePath);
        // 若是當前是目錄則經過模板來解析出來
        const content = ejs.render(this.template, {
          dirs,
          pathname:pathName
        });
        res.setHeader('Content-Type', 'text/html;charset=utf-8');
        res.statusCode = 200;
        res.end(content);
      } else {
        // 若是是文件的話要先讀取出來而後顯示出來
        this.sendFile(filePath, req, res, statObj);
      }
    }catch(e) {
      // 出錯了則拋出404
      this.handleErr(e, res);
    }
  }
  /** * 處理異常邏輯 * @param {*} e * @param {*} res */
  handleErr(e, res){
    console.log(e);
    res.setHeader('Content-Type', 'text/plain;charset=utf-8');
    res.statusCode = 404;
    res.end('資源未找到')
  }
  /** * 處理文件模塊 */
  sendFile(filePath, req, res, statObj){
    let zip = this.zipFile(filePath, req, res);
    res.statusCode = 200;
    let type = mime.getType(filePath);
    if(!zip) {
      // 當前不支持壓縮的處理方式
      res.setHeader('Content-Type', `${type};charset=utf-8`);
      fs.createReadStream(filePath).pipe(res);
    } else {
      fs.createReadStream(filePath).pipe(zip).pipe(res);
    }
  }
  start(){
    let server = http.createServer(this.handleRequest.bind(this));
    server.listen(this.port, () => {
      console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
    })
  }
}

export default Server;
複製代碼

這邊加個方法zipFile,來匹配瀏覽器支持的類型來作壓縮,壓縮也要給res告訴瀏覽器我服務端是根據什麼來壓縮的res.setHeader('Content-Encoding', '***');
壓縮以前:

壓縮以後:

優化點二:http緩存 強緩存和協商緩存

咱們整理下
強緩存是http 200

cache-control 能夠設置max-age 相對時間 幾秒

no-cache 是請求下來的內容仍是會保存到緩存裏,每次都仍是要請求數據
     no-store  表明不會將數據緩存下來 
複製代碼

Expires 絕對時間,須要給出一個特定的值 協商緩存是http 304
Etag 判斷文件內容是否有修改
Last-Modified 文件上一次修改時間 根據這個方案咱們來作優化

import fs from "fs";
import http from 'http';
import mime from 'mime';
import crypto from 'crypto';
import path from 'path';
import chalk from "chalk";
import url from 'url';
import ejs from 'ejs';
import zlib from 'zlib';
const { readdir, stat } = fs.promises;
const template = fs.readFileSync(path.join(process.cwd(), 'template.html'), 'utf8');
class Server {
  constructor(config){
    this.port = config.port;
    this.template = template;
  }
  /** * 壓縮文件處理 */
  zipFile(filePath, req, res){
    // 使用zlib庫去壓縮對應的文件
    // 獲取請求頭數據Accept-Encoding來識別當前瀏覽器支持哪些壓縮方式
    const encoding = req.headers['accept-encoding'];
    console.log('encoding',encoding);
    // 若是當前有accept-encoding 屬性則按照匹配到的壓縮模式去壓縮,不然不壓縮 gzip, deflate, br 正常幾種壓縮模式有這麼幾種
    if(encoding) {
      // 匹配到gzip了,就使用gzip去壓縮
      if(/gzip/.test(encoding)) {
        res.setHeader('Content-Encoding', 'gzip');
        return zlib.createGzip();
      } else if (/deflate/.test(encoding)) { // 匹配到deflate了,就使用deflate去壓縮
        res.setHeader('Content-Encoding', 'deflate');
        return zlib.createDeflate();
      }
      return false;
    } else {
      return false;
    }
  }
  /** * 處理請求響應 */
  async handleRequest(req, res){
    let {pathname, query} = url.parse(req.url, true);
    // 轉義中文文件名處理
    pathname = decodeURIComponent(pathname);
    let pathName = pathname === '/' ? '': pathname;
    let filePath = path.join(process.cwd(), pathname);
    try {
      const statObj = await stat(filePath);
      if(statObj.isDirectory()) {
        // 先遍歷出全部的目錄節點
        const dirs = await readdir(filePath);
        // 若是當前是目錄則經過模板來解析出來
        const content = ejs.render(this.template, {
          dirs,
          pathname:pathName
        });
        res.setHeader('Content-Type', 'text/html;charset=utf-8');
        res.statusCode = 200;
        res.end(content);
      } else {
        // 若是是文件的話要先讀取出來而後顯示出來
        this.sendFile(filePath, req, res, statObj);
      }
    }catch(e) {
      // 出錯了則拋出404
      this.handleErr(e, res);
    }
  }
  /** * 處理異常邏輯 * @param {*} e * @param {*} res */
  handleErr(e, res){
    console.log(e);
    res.setHeader('Content-Type', 'text/plain;charset=utf-8');
    res.statusCode = 404;
    res.end('資源未找到')
  }
  /** * 緩存文件 * @param {*} filePath * @param {*} req * @param {*} res */
  cacheFile(filePath, statObj, req, res) {
    // 讀出上一次文件中的變動時間
    const lastModified = statObj.ctime.toGMTString();
    const content = fs.readFileSync(filePath);
    // 讀取出當前文件的數據進行md5加密獲得一個加密串
    const etag = crypto.createHash('md5').update(content).digest('base64');
    res.setHeader('Last-Modified', lastModified);
    res.setHeader('Etag', etag);
    // 獲取請求頭的數據 If-Modified-Since 對應上面res返回的Last-Modified
    const ifLastModified = req.headers['if-modified-since'];
    // 獲取請求頭的數據 If-None-Match 對應上面res返回的Etag
    const ifNoneMatch = req.headers['if-none-match'];
    console.log(ifLastModified,lastModified);
    console.log(ifNoneMatch,etag);
    if(ifLastModified && ifNoneMatch) {
      if(ifLastModified === lastModified || ifNoneMatch === etag) {
        return true;
      }
      return false;
    }
    return false;
  }
  /** * 處理文件模塊 */
  sendFile(filePath, req, res, statObj){
    console.log(chalk.cyan(filePath));
    // 設置cache的時間間隔,表示**s內不要在訪問服務器
    res.setHeader('Cache-Control', 'max-age=3');
    // 若是強制緩存,首頁是不會緩存的 訪問的頁面若是在強制緩存,則會直接從緩存裏面讀取,不會再請求了
    // res.setHeader('Expires', new Date(Date.now()+ 3*1000).toGMTString())
    // res.setHeader('Cache-Control', 'no-cache'); // no-cache 是請求下來的內容仍是會保存到緩存裏,每次都仍是要請求數據
    // res.setHeader('Cache-Control', 'no-store'); // no-store 表明不會將數據緩存下來 
    // 在文件壓縮以前能夠先走緩存,查看當前的文件是不是走的緩存出來的數據
    const isCache = this.cacheFile(filePath, statObj, req, res);
    if(isCache) {
      res.statusCode = 304;
      return res.end();
    }
    let zip = this.zipFile(filePath, req, res);
    res.statusCode = 200;
    let type = mime.getType(filePath);
    if(!zip) {
      // 當前不支持壓縮的處理方式
      res.setHeader('Content-Type', `${type};charset=utf-8`);
      fs.createReadStream(filePath).pipe(res);
    } else {
      fs.createReadStream(filePath).pipe(zip).pipe(res);
    }
  }
  start(){
    let server = http.createServer(this.handleRequest.bind(this));
    server.listen(this.port, () => {
      console.log(`${chalk.yellow('Starting up http-server, serving')} ${chalk.cyan('./')} ${chalk.yellow('Available on:')} http://127.0.0.1:${chalk.green(this.port)} Hit CTRL-C to stop the server`)
    })
  }
}

export default Server;
複製代碼

總結下:
正常來講強緩存和協商緩存是一塊兒用的
強緩存設置cache-control 咱們設置下緩存時間是3S,這邊設置的是相對時間3S,不加協商緩存,咱們試下看看

首頁index.html入口文件是不會被緩存的,若是首頁被緩存了,那麼不少人斷網的時候還能訪問就是有問題了
Expires 使用是同樣的,這個是傳的絕對時間,過了這個時間就會失效的

下面咱們加下協商緩存試下吧

Last-Modified 這個正常來講這個值是放的文件的更新時間,咱們這邊使用stat獲取到文件的ctime
Etag 官方說這個是一個新鮮複雜度的算法,這邊爲了方便處理,個人Etag沒作什麼算法處理,只是用文件內容md5加密成base64,內容長度固定,不會太大
咱們第一次訪問會將這2個值塞入到res響應頭裏面去
咱們看來看下請求的內容

咱們來分析下:
第一次進來會是200,服務端響應頭塞入了 Etag,Last-Modified
在cache-control時間以內,咱們請求頭會有
If-Modified-Since -> Last-Modified
If-None-Match -> Etag
咱們在服務端能拿到請求頭,去跟讀取出來的文件進行比較,若是沒有改變會走 304協商緩存
全部說 304協商緩存必定會請求到服務端比較文件信息,200的話則不必定,有可能直接從緩存裏面讀取了
這樣的好處是什麼?
每次都去請求比較,沒有變化就無論,有變化了就去從新請求數據
咱們來試下看看 先請求,而後html文件內容改變了再去請求看看,就是下面這樣

咱們來看看效果:

相關文章
相關標籤/搜索