一、准备工作及流程说明

一看这标题,大家可能一下子没有反应过来,到底是要干什么?那么就先看一下实现效果吧~

项目目录结构:

.

└─my-http-server
├─node_modules
├─bin
├─config.js // 命令行配置文件
├─www.js // 可执行文件
├─src
├─index.js // 主要代码实现
├─template.html // 模板文件

初始化package.json文件,npm init -y

  1. 声明想要发包的名称

  2. 执行命令行工具,需要配置bin及需要执行的文件路径,并放在全局下(长命令和短命令)

   {
"name": "my-http-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin":{
"mhs":"./bin/www.js",
"my-http-server":"./bin/www.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
  1. 创建并编写执行文件

    www.js:

#! /usr/bin/env node 

// 以上代码为声明该文件的运行方式,使用node环境去运行

console.log('ok');
  1. 在当前目录下执行npm link ,就会在全局下产生一个对应的软链

  2. 终端输入命令测试是否成功:mhs,若演示代码和我的一样,打印出ok即成功

  3. 源码实现使用到了一些第三方的模块,就现在准备工作时安装了,代码实现时,就直接用了

    • commander chalk mime ejs
  4. 配置命令行工具,也就是我们常用的命令行指令的帮助文档及一些参数信息。所用模块:commander

  5. 配置命令行的参数的默认值

  6. 最后去实现创建服务及启动

二、配置命令行

  1. 将配置命令行的参数的默认值单独抽离,写入至config.js

    config.js

const options = {
'port':{ // 端口
option:'-p, --port <n>',
default: 8080,
usage:'mhs --port 3000',
description:'set mhs port'
},
'gizp':{ // 压缩
option:'-g, --gizp <n>',
default: 1,
usage:'mhs --gizp 0', // 禁用压缩
description:'set mhs gizp'
},
'cache':{ // 缓存
option:'-c, --cache <n>',
default: 1,
usage:'mhs --cache 0', // 禁用缓存
description:'set mhs cache'
},
'directory':{ // 设置服务启动目录
option:'-d, --directory <d>',
default: process.cwd(), // 表示当前的工作目录
usage:'mhs --directory C:',
description:'set mhs directory'
}
} module.exports = options
  1. 命令行的帮助文档,处理用户输入指令参数

    www.js

   #! /usr/bin/env node
// 以上代码为声明该文件的运行方式,使用node环境去运行 // console.log('ok'); // 可先行 测试 // 命令行的帮助文档
const program = require('commander')
const options = require('./config')
program.name('mhs')
program.usage('[option]') // 解析 当前运行进程 传递的参数
const examples = new Set();
const defaultMapping = {};
Object.entries(options).forEach(([key, value]) => { // 参数是个数组,需要解构
examples.add(value.usage)
defaultMapping[key] = value.default
program.option(value.option, value.description)
})
program.on('--help', function () {
console.log('\nExamples:');
examples.forEach(item => {
console.log(` ${item}`);
})
}) program.parse(process.argv) // 获取用户输入的参数
let userArges = program.opts() // console.log(defaultMapping); // 自定义的默认值 // 合并用户传递的参数及默认值,且默认值优先级最低
let serverOptions = Object.assign(defaultMapping,userArges) // 启动服务 此时只是入口,实际文件还并未编写,莫着急哈
const Server = require('../src/index');
let server = new Server(serverOptions);
server.start()

三、设置入口文件和渲染模板

template.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<%dirs.forEach(dir=>{%>
<li><a href="<%=dir.url%>"><%=dir.name%></a></li>
<%})%>
</body>
</html>

四、my-http-server源码

  1. 根据用户输入的参数,创建一个服务,获取本机IP地址,输出服务启动信息。
  2. 监听对应的端口,并处理端口被占用问题
  3. 获取请求路径,以当前目录为基准查找文件,解析路径 ,存在中文路径找不到问题,需要转义
  4. 请求路径不存在,设置响应状态码404,提示信息
  5. 若是文件夹:根据数据和模板渲染页面(纯服务端渲染),并且可点击查看详情,设置a 链接的跳转路径为相对路径,对资源发送请求,每点一次都会请求服务器
  6. 若是文件:则判断是否有缓存,是否需要压缩返回
const http = require('http');
const os = require('os');
const url = require('url');
const path = require('path');
const fs = require('fs').promises; // 将fs中的方法 全部转为promise 的形式
const crypto = require('crypto')
const zlib = require('zlib'); const chalk = require('chalk'); // 粉笔
const mime = require('mime'); // 头信息
const ejs = require('ejs'); // 模板解析 const { createReadStream, readFileSync } = require('fs')
const template = readFileSync(path.resolve(__dirname, 'template.html'), 'utf8'); class Server {
constructor(serverOptions) {
this.port = serverOptions.port;
this.gzip = serverOptions.gzip;
this.cache = serverOptions.cache;
this.directory = serverOptions.directory;
this.handleRequest = this.handleRequest.bind(this); // 第一种 指正this 指向
this.template = template;
}
async handleRequest(req, res) {
// 1. 获取请求路径,以当前目录为基准查找文件,如果文件存在,且不是文件夹则直接返回
let { pathname } = url.parse(req.url); // 获取解析路径
// 存在中文路径找不到问题,需要转义
pathname = decodeURIComponent(pathname)
let requestFile = path.join(this.directory, pathname);
try {
let statObj = await fs.stat(requestFile)
if (statObj.isDirectory()) {
const dirs = await fs.readdir(requestFile)
// 根据数据和模板渲染页面(纯服务端渲染),并且可点击查看详情,设置a 链接的跳转路径为相对路径
// 对资源发送请求,每点一次都会请求服务器
let fileContent = await ejs.render(this.template, {
dirs: dirs.map(dir => ({
name: dir,
url: path.join(pathname, dir)
}))
})
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end(fileContent)
} else {
this.sendFile(req, res, requestFile, statObj)
}
} catch (e) {
console.log(e);
this.sendError(req, res, e);
}
}
cacheFile(req, res, requestFile, statObj) {
// 第一次发送文件,先设置强制缓存,在执行强制缓存时,默认不会执行对比缓存,因为不走服务器
res.setHeader('Cache-Control', 'max-age=10');
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString()); // 每次强制缓存时间到了,就会走对比缓存,然后在变成强制缓存、
const lastModified = statObj.ctime.toGMTString();
const etag = crypto.createHash('md5').update(readFileSync(requestFile)).digest('base64'); res.setHeader('Last-Modified', lastModified);
res.setHeader('Etag', etag); let ifModifiedSince = req.headers['if-modified-since']
let ifNoneMatch = req.headers['if-none-match']
// 如果文件修改时间不一样,就直接返回最新的
if (lastModified !== ifModifiedSince) { // 有可能时间一样,但是内容不一样
return false;
}
if (etag !== ifNoneMatch) { // 一般情况下,指纹生成不会是根据文件全量生成,有可能只是根据文件大小
return false;
}
return true;
}
gzipFile(req, res, requestFile, statObj) {
// 浏览器会携带一个accept-encoding 字段,表示浏览器支持的压缩格式
let encodings = req.headers['accept-encoding'];
if (encodings) { // 浏览器支持压缩
if (encodings.includes('gzip')) {
res.setHeader('Content-Encoding', 'gzip') // 浏览器要知道服务器的压缩类型
return zlib.createGzip()
} else if (encodings.includes('deflate')) {
res.setHeader('Content-Encoding', 'deflate')
return zlib.createDeflate()
}
}
return false;
}
sendFile(req, res, requestFile, statObj) {
// 返回文件,需要给浏览器 提供内容类型及内容的编码格式
res.setHeader('Content-Type', mime.getType(requestFile) + ';charset=utf-8'); // 判断有没有缓存,如果有缓存,就使用对比缓存
if (this.cacheFile(req, res, requestFile, statObj)) {
res.statusCode = 304;
return res.end();
} // 判断是否支持压缩,如果支持返回一个压缩流
let createGzip;
if (createGzip = this.gzipFile(req, res, requestFile, statObj)) {
return createReadStream(requestFile).pipe(createGzip).pipe(res); // 转化流
}
// 根据文件生成一个可读流,而res 是可写流
createReadStream(requestFile).pipe(res);
}
sendError(req, res, e) {
res.statusCode = 404;
res.end('Not Found');
}
start() {
// const server = http.createServer(this.handleRequest.bind(this)) // 第二种 指正this 指向
// const server = http.createServer((req, res)=>this.handleRequest(req, res)) // 第三种 指正this 指向
const server = http.createServer(this.handleRequest)
server.listen(this.port, () => { // 订阅方法,监听成功后会触发
let WLAN = os.networkInterfaces().WLAN;
let IP = WLAN[1].address;
console.log(chalk.yellow(`Starting up http-server, serving`) + chalk.blue(' ./'));
console.log(chalk.yellow(`Available on:`));
console.log(`http://${IP}:${chalk.green(this.port)}`);
console.log(`http://127.0.0.1:${chalk.green(this.port)}`);
console.log(`Hit CTRL-C to stop the server`);
})
server.on('error', err => {
if (err.code = 'EADDRINUSE') { // 解决端口被占用的问题,被占用 累加1
server.listen(++this.port);
}
})
}
} module.exports = Server

my-http-server 静态服务器源码学习实现缓存及压缩的更多相关文章

  1. tinyhttpd服务器源码学习

    下载地址:http://sourceforge.net/projects/tinyhttpd/ /* J. David's webserver */ /* This is a simple webse ...

  2. ThinkPHP5.0源码学习之缓存Cache(二)

    一.使用Cache类 TP5.0框架默认使用的是File文件缓存驱动,可以修改全局配置文件convention.php中的type,将其改为Redis,这样使用的就是Redis缓存驱动了.

  3. ThinkPHP5.0源码学习之缓存Cache(一)

    一.文件 1.缓存配置文件:thinkphp\convention.php 2.缓存文件:thinkphp\library\think\Cache.php 3.驱动目录:thinkphp\librar ...

  4. 【mybatis源码学习】缓存机制

    一.mybatis的缓存 一级缓存:sqlsession级别,默认开启(一个事务内有效)二级缓存:  sqlsessionFactory级别,需要手动开启,在xml配置cache节点(依赖事务的执行结 ...

  5. Seata Server 1.5.2 源码学习

    Seata 包括 Server端和Client端.Seata中有三种角色:TC.TM.RM,其中,Server端就是TC,TM和RM属Client端.Client端的源码学习上一篇已讲过,详见 < ...

  6. Hadoop源码学习笔记(4) ——Socket到RPC调用

    Hadoop源码学习笔记(4) ——Socket到RPC调用 Hadoop是一个分布式程序,分布在多台机器上运行,事必会涉及到网络编程.那这里如何让网络编程变得简单.透明的呢? 网络编程中,首先我们要 ...

  7. tiny web服务器源码分析

    tiny web服务器源码分析 正如csapp书中所记,在短短250行代码中,它结合了许多我们已经学习到的思想,如进程控制,unix I/O,套接字接口和HTTP.虽然它缺乏一个实际服务器所具备的功能 ...

  8. MVC系列——MVC源码学习:打造自己的MVC框架(四:了解神奇的视图引擎)

    前言:通过之前的三篇介绍,我们基本上完成了从请求发出到路由匹配.再到控制器的激活,再到Action的执行这些个过程.今天还是趁热打铁,将我们的View也来完善下,也让整个系列相对完整,博主不希望烂尾. ...

  9. Redis源码学习:字符串

    Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...

  10. Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

    Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题 相关文章: Dubbo源码学习文章目录 前言 主要是前一阵子换了工作,第一个任务就是解决目前团队在 Dubbo 停机时产生的问题 ...

随机推荐

  1. 倒计时7天!AIRIOT新品发布会,6月6日北京见。

    随着物联网.大数据.AI技术的成熟和演进,智能物联网技术正在加速.深入渗透至各行业应用. AIRIOT物联网平台作为赋能数字经济发展和产业转型的数字基座,由航天科技控股集团股份有限公司(股票代码:00 ...

  2. 节能降耗 | AIRIOT智慧电力综合管理解决方案

      电力技术的发展推动各行各业的生产力,与此同时,企业中高能耗设备的应用以及输配电过程中的电能损耗,也在一定程度上加剧了电能供应压力.以工业制造业为例,企业的管理水平.能耗结构.生产组织方式都关系到能 ...

  3. Biwen.Settings添加对IConfiguration&IOptions的集成支持

    Biwen.Settings 是一个简易的配置项管理模块,主要的作用就是可以校验并持久化配置项,比如将自己的配置存储到数据库中,JSON文件中等 使用上也是很简单,只需要在服务中注入配置即可, 比如我 ...

  4. linux 自定义程序开机自启

    实现开机自启常见的有两种方法: /etc/init.d/下编写脚本命令(有些机子会有问题,比较麻烦) 利用定时任务crontab 本文介绍crontab现实程序开机自启 编写执行脚本run.sh #! ...

  5. RocketMQ主从同步原理

    一. 主从同步概述 主从同步这个概念相信大家在平时的工作中,多少都会听到.其目的主要是用于做一备份类操作,以及一些读写分离场景.比如我们常用的关系型数据库mysql,就有主从同步功能在. 主从同步,就 ...

  6. C# || 批量翻译工具 || 百度翻译api || 读取.cs文件内容 || 正则表达式筛选文件

    背景: 我们项目一开始的所有提示都是中文,后来要做国际化.发现项目中的带双引号的中文居然有 2.3 w 多条!!!简直让人欲哭无泪... 如果使用人工改的话,首先不说正确率了.光是效率都是难难难.所以 ...

  7. 使用 Hugging Face 推理终端搭建强大的“语音识别 + 说话人分割 + 投机解码”工作流

    Whisper 是当前最先进的开源语音识别模型之一,毫无疑问,也是应用最广泛的模型.如果你想部署 Whisper 模型,Hugging Face 推理终端 能够让你开箱即用地轻松部署任何 Whispe ...

  8. 判断是不是ie浏览器 加上ie11

    var b_version = navigator.appVersion; var version = b_version.split(";"); var trim_Version ...

  9. css做多列瀑布流

    <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8 ...

  10. Thread交互及interrupt示例

    package com.test.docxml; /** Thread交互及interrupt示例 * 线程模拟:一个在睡觉,一个在敲墙,敲墙完成之后,把睡觉的吵醒了. */ public class ...