node--http-server
模拟实现http-server
- 能够准确识别目标访问路径
- 判断目标路径是文件夹还是文件,前者读取目录并借助模板引擎返回文件列表,后者返回该文件
- 以pipe形式将目标文件输出,并指定Content-type
- 如果目标路径不存在,设置状态码为404,提示404 not found
- 压缩
- 缓存
启动命令
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 命令链接到全局
###识别命令行参数
- npm 安装 commander
- 新建src目录
- src下新建main.js ,server.js
- 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
}
再会
情如风雪无常,
却是一动既殇。
感谢你这么好看还来阅读我的文章,
我是冷月心,下期再见。