手把手体验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文件内容改变了再去请求看看,就是下面这样

咱们来看看效果:

相关文章
相关标签/搜索