node--http-server

程淮晨
2023-12-01

模拟实现http-server

  1. 能够准确识别目标访问路径
  2. 判断目标路径是文件夹还是文件,前者读取目录并借助模板引擎返回文件列表,后者返回该文件
  3. 以pipe形式将目标文件输出,并指定Content-type
  4. 如果目标路径不存在,设置状态码为404,提示404 not found
  5. 压缩
  6. 缓存

启动命令

http-server/bin/www

  • 默认端口启动:server
  • 指定端口启动:server -p 8881
#! /usr/bin/env node

require('../dist/main')

es6转es5和import语法识别

  • 为方便编写,代码中使用import语法
  • 安装依赖@babel/cli @babel/core @babel/preset-env
  • 根目录下新建.babelrc文件,配置如下
{
    "presets": [
        ["@babel/preset-env",{
            "targets":{
                "node":"current"
            }
        }]
    ]
}

package.json配置

 "bin": {
    "server": "./bin/www"
  },
 "scripts": {
    "babel": "babel src -d dist --watch"
  },

npm link

将server 命令链接到全局

###识别命令行参数

  1. npm 安装 commander
  2. 新建src目录
  3. src下新建main.js ,server.js
  4. main.js写入以下内容

import program from 'commander'
import Server from './server'

program
    .option('-p ,--port <val>', 'set http-server port')
    .parse(process.argv)


const config = {
    port: 8080//默认端口
}

Object.assign(config, program)

//通过解析参数port 在指定端口启动服务

const app=new Server(config)
app.start()

###代码

//server.js
import http from 'http'
import fs from 'fs'
import path from 'path'
import mime from 'mime'
import chalk from 'chalk'
import url from 'url'
import artTemplate from 'art-template'

let template = fs.readFileSync(path.resolve(__dirname, '../template.html'), 'utf-8')


class Server {
    constructor(config) {
        this.port = config.port
        this.template = template
    }


    httpRequest(req, res) {
        try {

            let { pathname } = url.parse(req.url)
              //忽略小图标请求
            if (pathname === '/favicon.ico') return res.end('')

            //中文路径解析
            pathname = decodeURIComponent(pathname)
            // process.cwd() node命令执行的地址
            let filePath = path.join(process.cwd(), pathname)
            const stat = fs.statSync(filePath);
            if (stat.isDirectory(filePath)) {
                const dirs = fs.readdirSync(filePath);
                // pathname/dir  决定是否深层路径拼接 /bin/www   / 
                const deepPath = pathname === '/' ? '' : pathname
                let templateStr = artTemplate.render(this.template, { dirs, pathname: deepPath })
                res.setHeader('Content-type', 'text/html;charset=utf-8')
                res.end(templateStr)

            } else {
                this.sendFile(stat, filePath, req, res)
            }


        } catch (err) {
            console.log(err)
            this.sendError(res)
        }


    }

    start() {
        http.createServer(this.httpRequest.bind(this))
            .listen(this.port, () => {
//使用chalk为打印着色
                console.log(`
${chalk.yellow('Starting up http-server, serving')} ${chalk.blue('./')} 
${chalk.yellow('Available on:')} 
http://127.0.0.1:${chalk.green(this.port)}
Hit CTRL-C to stop the server
                `)
            })
    }


    sendFile(stat, filePath, req, res) {

        if (fs.existsSync(filePath)) {
            //通过管道形式返回 并指定响应头Content-type
            //通过第三方包mime获取文件mime类型
            const mimeType = mime.getType(filePath)
            res.setHeader('Content-type', `${mimeType};charset=utf-8`)
            fs.createReadStream(filePath).pipe(res)
        }
    }

    sendError(res) {
        //返回404 not found 
        res.statusCode = 404
        res.setHeader('Content-type', 'text/plain;charset=utf-8')
        res.end('404 not found')
    }
}


export default Server


模板引擎

根目录下新建template.html 用于渲染目录下文件列表

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>list</title>
</head>
<body>
    <ul>
        {{each dirs}}
        <li><a href="{{pathname}}/{{ $value }}"> {{ $value }} </a></li>
        {{/each}}

    </ul>
</body>
</html>

压缩

压缩和缓存是http性能优化的两个主要手段。
请求头Accept-Encoding表示客户端支持的压缩方式,如gzip, deflate, br。
响应头Content-Encoding表示服务端具体采用的压缩方式,会返回Accept-Encoding中的某一种。

zlib模块

nodejs的zlib模块专门用于压缩和解压,压缩本质是在读写流之间加一层转化流,内容重复度越高,压缩效果越明显

//压缩
const zlib = require('zlib')
const fs = require('fs')
fs.createReadStream('./a.txt')
    .pipe(zlib.createGzip())
    .pipe(fs.createWriteStream('./a.txt.gz'))

//解压
const zlib = require('zlib')
const fs = require('fs')
fs.createReadStream('./a.txt.gz')
    .pipe(zlib.createGunzip())
    .pipe(fs.createWriteStream('./a.txt'))


浏览器支持的压缩格式

如果浏览器支持压缩,借助zlib模块,我们可以在发送文件前进行一次压缩。
在server中定义一个gzip方法,根据请求头accept-encoding做不同的逻辑处理。

  gzip(req, res) {
    //获取请求头accept-encoding
    const encoding = req.headers['accept-encoding']
    if (encoding) {
        if (encoding.includes('gzip')) {
            //告知客户端服务端具体采用的压缩方式
            res.setHeader('Content-Encoding', 'gzip')
            return zlib.createGzip()
        } else if (encoding.includes('deflate')) {
            //告知客户端服务端具体采用的压缩方式
            res.setHeader('Content-Encoding', 'deflate')
            return zlib.createDeflate()
        } else {
            return false
        }
    } else {
        return false
    }

}

发送文件前调用gzip方法


    sendFile(stat, filePath, req, res) {
        if (fs.existsSync(filePath)) {
            //调用gzip方法
            const gzip = this.gzip(req, res)
            const mimeType = mime.getType(filePath) || 'text/plain'
            res.setHeader('Content-type', `${mimeType};charset=utf-8`)
            if (!gzip) {
                //不支持压缩的直接返回
                fs.createReadStream(filePath).pipe(res)
            } else {
                //支持压缩返回压缩后的文件
                fs.createReadStream(filePath).pipe(gzip).pipe(res)
            }
        }
    }

缓存

概述

  • 缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。
  • 同样的,缓存也是http层面重要的优化手段。

缓存分类

  • 缓存可分为强缓存和协商缓存。
  • 强缓存指的是缓存有效期内客户端无需二次向服务端发送请求,直接从缓存中获取,状态码为200。
  • 网络请求中的from disk cache ,from memory就表示命中强缓存
  • 协商缓存指的是强缓存失效后,客户端需要带上对应的缓存头向服务端发送请求进行协商,服务端对比后决定缓存是否可用。
  • 如命中协商缓存,则状态码为304,响应体无内容。更新缓存时间,继续使用,下一轮次将是强缓存(这是一个循环)
  • 如强缓存和协商缓存都未命中,客户端需要从服务端获取数据,此时状态码为200。

相关http头

  • 对于http头而言,除了方法区分大小写(GET,POST…),其他都是大小写不敏感的,建议首字母大写
  • 强缓存:Cache-Control ,Expires,二者同时出现Cache-Control优先生效
  • 协商缓存:Last-Modified If-Modified-Since Etag If-None-Match
  • 响应头:Last-Modified,Etag ,二者同时出现Etag优先生效
  • 请求头:If-Modified-Since,If-None-Match,前者用于和Last-Modified值比对,后者用于和Etag值比对
  • 出现类似功能的头是因为历史原因,Cache-Control基本上可以取代Expires,但是Etag无法完全取代Last-Modified
  • Etag根据文件内容摘要比对,Last-Modified根据修改时间比对,维度不同
  • Etag虽然更精准,但是比起Last-Modified要额外读取文件,进行摘要处理,如果是大文件会很耗时。
  • 一般情况下,会读取部分文件内容生成摘要。耗时和精准度上来讲,这也是一个取舍。

缓存策略

  • 缓存策略,其实就是通过合理的指定各种缓存相关头的值,从而达到性能优化的一种有效方法。
  • 对于网站logo这种近乎一两年,甚至十几年都不会变动的,可以直接强缓存Cache-Control设置max-age为年级别的数值
  • 涉及频繁读写的场景可以设置强缓存的时间短一些,或者直接Cache-Control:no-cache,进入协商缓存
  • 如果是实时读写,缓存可能不太实用,压缩会更有效一些。Cache-Control:no-store 不进行任何数据的缓存
  • 抛开实际场景谈缓存意义不大,要根据实际场景按需制定缓存策略

缓存使用


    sendFile(stat, filePath, req, res) {

        if (fs.existsSync(filePath)) {

            const gzip = this.gzip(req, res)
            const mimeType = mime.getType(filePath) || 'text/plain'
            res.setHeader('Content-type', `${mimeType};charset=utf-8`)

            //设置强缓存
            res.setHeader('Cache-Control', `max-age=10`)
            res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())

            //协商缓存
            const cache = this.cache(stat, filePath, req, res)

            //是否命中协商缓存
            if (cache) {
                res.statusCode = 304;
                return res.end()
            }

            if (!gzip) {
                //不支持压缩的直接返回
                fs.createReadStream(filePath).pipe(res)
            } else {
                //支持压缩返回压缩后的文件
                fs.createReadStream(filePath).pipe(gzip).pipe(res)
            }
        }
    }



协商缓存实现



    cache(stat, filePath, req, res) {

        //协商缓存:如果last-modified 和 etag 同时存在,etag优先生效
        //读取文件内容生成唯一标识
        const Etag = crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('base64')
        res.setHeader('Etag', Etag)
        const ifNoneMatch = req.headers['if-none-match']
        //返回比对结果
        if (ifNoneMatch) return ifNoneMatch === Etag
        //上次修改时间
        let lastModified = stat.ctime.toGMTString()
        res.setHeader('Last-Modified', lastModified)
        const ifModifiedSince = req.headers['if-modified-since']
        //返回比对结果
        if (ifModifiedSince) return ifModifiedSince === lastModified
        //首次没缓存 返回fasle
        return false
    }

再会


情如风雪无常,

却是一动既殇。

感谢你这么好看还来阅读我的文章,

我是冷月心,下期再见。

 类似资料: