手摸手带你实现一个静态服务器,超详细
liebian365 2024-11-09 13:44 7 浏览 0 评论
这是学习 Node.js 的第七篇,这一篇主要是了解 http,同时实现一个静态资源服务器。先看一下这个服务器有什么功能。
服务器功能展示
首先我们在命令行工具输入 ss (意为:super server),它会帮我们在当前目录启动一个静态资源服务器。服务器的地址为 「http://localhost:3000」。
当我们访问 「http://localhost:3000」时,它把我们当前目录的所有文件都罗列了出来。
我们点击一个文件,例如 pacakge.json,它会把当前文件的内容显示出来:
OK,主要功能就是这些,下面我们一起来实现一下。
可以通过 ss --port 3001 指定端口号,通过 ss --directory C:\foo\bar 指定服务器的工作目录,即静态资源的根目录。
http 模块
既然是服务器,那一定是使用了 Node 的 http 模块,我们先简单的了解下如何使用 http 创建一个服务器。
创建一个服务器
const http = require('http')
const server = http.createServer((req, res) => {
console.log('有请求过来了~~~')
})
let port = 3000
server.listen(port, () => {
console.log(`server start ${port}`)
})
使用 http.createServer 即可创建一个服务器,然后再调用 server.listen() 方法监听一个端口,就算正式创建成功了。这时我们直接访问 http://localhost:3000 即可在命令行看到打印 有请求过来了。
那么我们如何获得这个请求的具体信息,并给客户端做出相应呢?
其实,每次请求过来的时候都会执行 createServer(callback) 中传入的回调,回调内会传入两个参数:「req(request) 与 res(response)」。req 就代表请求信息与相关操作,res 代表响应信息与相关操作。
我们具体来使用一下这两个对象。
const http = require('http')
const url = require('url')
const server = http.createServer((req, res) => {
// 请求方法名
console.log(req.method)
// 请求url
console.log(req.url)
// 请求头
console.log(req.headers)
// req 是一个可读流
req.on('data', chunk => {
console.log(chunk)
})
req.on('end', () => {})
// 响应行->响应头->响应体顺序不能变
// 首先设置响应行(状态码与状态码描述)
res.statusCode = 200
res.statusMessage = 'success'
// 设置响应头
res.setHeader('name', 'superYue')
// 最后设置响应体
// res 是一个可写流
res.write('ok')
res.end('1')
})
let port = 3000
server.listen(port, () => {
console.log(`server start ${port}`)
})
此时,我们在浏览器访问 http://localhost:3000 就可以下看到如下内容:
「这里有一些点需要大家注意」
- req 是一个可读流,我们获取请求体的时候,必须要以流的形式获取,正如我上述代码所写的那样
- res 是一个可写流,所以我们设置响应体的时候,必须用 write() 方法,结束响应时,必须用 end() 方法
「对可读流、可写流不清楚的同学可以看下此系列文章下的《手写文件流》」
开始写代码
现在我们进入主题——「实现一个静态资源服务器」。
先看一下我们的目录结构。
bin 目录是命令行逻辑代码
src/satic-server.js 静态资源服务器
src/template.html html 模板
实现命令行功能
我们的服务器是在命令行内输入 ss 之后自动启动的,我们看一下这个功能是怎么实现的?
首先,我们要在 package.json 内增加一个 bin 字段,如下代码所示:
// pacakge.json
{
"bin": {
"ss": "./bin/www.js"
},
}
ss 是我们运行的命令,./bin/www.js 是运行 ss 后要被执行的 js 文件。
然后,./bin/www.js 内注意要添加这行代码 #! /usr/bin/env node,这行代码的意思是用 node 环境来执行以下代码,这样我们就可以尽情的去写 Node 代码了。
最后,我们要在当前的工作目录去执行 npm link,这样才能将 ss 命令注册到全局变量中去,不然系统是不认识 ss 的。
现在我们已经可以执行 ss 命令了,理论上就可以在 bin/www.js 内去实现一个静态服务器了,但是在真正实现之前,我想有一些定制化的功能,比如自定义启动服务的端口号,自定义静态服务器的工作目录。
要实现这样的定制化功能,那肯定是在命令行内去输入,例如:
ss --port 3000 启动一个 3000 端口的服务器
ss --directory C: 静态资源服务器的根目录是 C 盘。
然后我们要解析 ss 输入的参数,这些参数 Node 都帮我们保存在了 process.argv 属性里,打印出来的结果如下图所示。
如果我们想得到正确的结果,需要我们自己去解析。这里给大家推荐一个工具——commander,它是一个完整的 node.js 命令行解决方案,github链接点这。
我们来看一下示例:
const { program } = require('commander)
// 声明一个 prot 参数,要求必须有值,默认值是 3000
// 'set your server port' 是命令描述
program.option('-p, --port <v>', 'set your server port', 3000);
// 开始解析命令
program.parse(process.argv);
// 通过 program.port 拿到解析好参数
console.log(`port: ${program.port}`);
可以看到,最终我们输入的命令都会被解析到 program 内。
这只是 commander 一部分功能,完整功能可以看具体文档。
接下来把我 www.js 代码贴出来
#! /usr/bin/env node
const program = require('commander')
const StaticServer = require('../src/static-server')
console.log(process.argv)
program.name('ss')
program
.option('-p, --port <v>', 'set your server port', 3000)
.option('-d, --directory <v>', 'set your server start directory', process.cwd())
program.on('--help', () => {
console.log('\nExamples:')
console.log('ss -p 3000 / ss --port 3000')
console.log('ss -d C: / ss --directory C:')
})
program.parse(process.argv)
const config = {}
config.port = program.port ?? process.cwd()
new StaticServer(config).start()
program.on('--help') 的意思是监听 --help 命令。每当用户输入 ss --help 的时候,我们都把操作提示给打印出来。
静态资源服务器
在上段代码内,我们在 www.js 里执行了 new StaticServer(config).start(),这句代码的意思是启动一个静态资源服务器,接下来,我们就来实现一下这个。
初始化参数
首先,我们声明一个类,并初始化参数。
class StaticServer {
constructor(config) {
this.port = config.port
this.directory = config.directory
}
}
start()
然后,在调用 start 的时候,我们创建一个服务器。
start() {
const server = http.createServer(this.handleRequest.bind(this))
server.listen(this.port, () => {
console.log(`${chalk.yellow('Starting up super-server: ')}${this.directory}`)
console.log(`http://localhost:${chalk.green(this.port)}`)
})
}
为了更好的处理请求,我们把处理请求的逻辑全都放到了 handleRequest() 方法内。
chalk 中文意思为粉笔,是专门用来改变控制台输出颜色的第三方包。
handleRequest()
这个方法是专门用来处理请求的。
我们现在想一下,当一个静态资源的请求过来时,我们应该做什么操作?
- 判断请求的资源是一个文件还是一个文件夹。如果是文件,则直接返回文件内容;如果是文件夹,则返回文件夹内存在的资源列表。
- 对文件资源进行缓存。因为是静态资源服务器,所以肯定要有缓存功能,因为文件是不会经常变的。
- 响应文件内容。
我们看下具体代码
async handleRequest(req, res) {
// 获取请求路径
// url 为 Node 的核心模块
const { pathname } = url.parse(req.url)
// 工作目录与请求路径拼接,得到最终的静态资源地址
// 这里的工作目录默认是 process.cwd(),意思是当前代码启动的目录
// 可以通过 --directory 去指定
const filePath = path.join(this.directory, pathname)
try {
// 获取文件信息
const stat = await fs.stat(filePath)
if (stat.isFile()) {
// 如果是文件,则返回文件信息
this.sendFile(req, res, filePath, stat)
} else {
// 如果是文件夹,则返回资源列表
this.sendFolder(req, res, filePath, pathname)
}
} catch(e) {
// 返回错误信息
this.sendError(req, res, e)
}
}
代码注释非常详细,相信不用做过多的解释。
这里最终获取静态资源的地址是:请求的路径 + 服务器工作目录(默认是 process.cwd(),可以通过 --dircetory 去指定)
sendFile()
sendFile 对客户端响应文件信息,在响应之前,要做缓存相关的操作,这些操作都放在了 cache() 方法内。
缓存包括强缓存与协商缓存,强缓存取的是浏览器客户端内的内容,浏览器不会对服务器发起响应。协商缓存需要服务器判断文件是否发生了变化,如果未发生变化则返回 304。
在具体返回响应之前,要设置响应内容的 mime 格式,用来告诉客户端如何处理这段内容。例如,如果是 html 内容,那我们的 Content-Type 响应头必须是 text/html,不然浏览器不能正确的解析。这里我们使用了 mime 这个第三方包,它可以根据文件后缀得到正确的 mime 类型。
响应内容的时候,我们会以流的形式去响应,所以这里我们创建了一个文件可读流。
sendFile(req, res, filePath, stat) {
if (this.cache(req, res, filePath, stat)) {
res.statusCode = 304
res.end()
return;
}
res.setHeader('Content-Type', mime.getType(filePath))
createReadStream(filePath).pipe(res)
}
sendFolder()
返回文件夹内的文件列表。
将文件夹内的文件全部读取出来,并以 html 的形式返回给浏览器以供展示。这里使用了 ejs 模板引擎来渲染 html。
async sendFolder(req, res, filePath, pathname) {
let dirs = await fs.readdir(filePath)
dirs = dirs.map(item => ({
filename: item,
href: path.join(pathname, item)
}))
console.log(dirs)
const template = await fs.readFile(path.resolve(__dirname, './template.html'), 'utf-8')
const html = await ejs.render(template, { dirs }, { async: true })
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end(html)
}
我们将读取出来的文件列表传给 template 静态模板,然后利用 ejs 的得到渲染后的 html。
template.html 模板代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>zs</title>
</head>
<body>
<!-- 出路路径 尽量不要采用./ ../ 绝对路径 /a/a.js -->
<%dirs.forEach(item=>{%>
<li><a href="<%=item.href%>"><%=item.filename%></a></li>
<%})%>
</body>
</html>
cache()
cache() 方法封装了文件缓存操作。
首先对文件应用缓存,设置 Expires 与 Cache-Control 响应头,这两个字段设置任何一个字段都可以实现缓存,但为了最大的保证兼容性,我们这里都做了设置。
如果浏览器缓存失效,会重新发起请求,这时需要服务器判断资源是否真的被更改了,判断文件资源的缓存是否失效有两种方案。
- 通过判断文件的修改时间来确定是否失效。第一次请求时,将文件的修改时间通过 Last-Modified 响应头带给浏览器,浏览器下次请求时会将修改时间放入 if-modified-since 请求头内,这时我们就可以通过 if-modified-since 字段与当前文件的修改时间做比较,来判断文件是否被修改了。
但是这种做法有缺陷,假如我们将文件修改了,然后过一会又修改成原来的内容,这时最终的文件是没有变化的,但是文件的修改时间却变了,这样就导致缓存失效。
- 第二种解决了上述问题。第一次请求时,返回给浏览器的是一个文件摘要,以后请求时,根据文件摘要来判断资源是否过期。由于文件摘要是根据文件内容生成的,所以文件内容不变,摘要就不会变。用来控制缓存的字段分别是,浏览器:ifNoneMatch;客户端:Etag。
cache(req, res, filePath, stat) {
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())
res.setHeader('Cache-Control', `max-age=${10}`)
const ifModifiedSince = req.headers['if-modified-since']
const ctime = stat.ctime.toGMTString()
if (ifModifiedSince !== ctime) {
return false
}
const ifNoneMatch = req.headers['if-none-match']
// 利用 MD5 生成文件摘要
// crypto 为内置的加密算法
const etag = crypto.createHash('md5').update( readFileSync(filePath)).digest('base64')
if (ifNoneMatch !== etag) {
return false
}
res.setHeader('Last-Modified', ctime)
res.setHeader('Etag', etag)
return true;
}
完整代码
const http = require('http')
const url = require('url')
const fs = require('fs').promises
const path = require('path')
const { createReadStream, readFileSync } = require('fs')
const crypto = require('crypto')
const chalk = require('chalk')
const mime = require('mime')
const ejs = require('ejs')
class StaticServer {
constructor(config) {
this.port = config.port
this.directory = config.directory
}
start() {
const server = http.createServer(this.handleRequest.bind(this))
server.listen(this.port, () => {
console.log(`${chalk.yellow('Starting up super-server: ')}${this.directory}`)
console.log(`http://localhost:${chalk.green(this.port)}`)
})
}
async handleRequest(req, res) {
const { pathname } = url.parse(req.url)
const filePath = path.join(this.directory, pathname)
console.log(filePath)
try {
const stat = await fs.stat(filePath)
if (stat.isFile()) {
this.sendFile(req, res, filePath, stat)
} else {
this.sendFolder(req, res, filePath, pathname)
}
} catch(e) {
this.sendError(req, res, e)
}
}
cache(req, res, filePath, stat) {
res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())
res.setHeader('Cache-Control', `max-age=${10}`)
const ifModifiedSince = req.headers['if-modified-since']
const ctime = stat.ctime.toGMTString()
if (ifModifiedSince === ctime) {
return true
}
const ifNoneMatch = req.headers['if-none-match']
const etag = crypto.createHash('md5').update( readFileSync(filePath)).digest('base64')
if (ifNoneMatch === etag) {
return true
}
res.setHeader('Last-Modified', ctime)
res.setHeader('Etag', etag)
return false;
}
sendFile(req, res, filePath, stat) {
if (this.cache(req, res, filePath, stat)) {
res.statusCode = 304
res.end()
return;
}
res.setHeader('Content-Type', mime.getType(filePath))
createReadStream(filePath).pipe(res)
}
async sendFolder(req, res, filePath, pathname) {
let dirs = await fs.readdir(filePath)
dirs = dirs.map(item => ({
filename: item,
href: path.join(pathname, item)
}))
console.log(dirs)
const template = await fs.readFile(path.resolve(__dirname, './template.html'), 'utf-8')
const html = await ejs.render(template, { dirs }, { async: true })
res.setHeader('Content-Type', 'text/html;charset=utf-8')
res.end(html)
}
sendError(req, res, e) {
res.end(e.message)
}
}
module.exports = StaticServer
总结
可以看到,这个静态资源服务器并不是特别复杂,但是它却给我们带来了不少知识点。
- 命令行工具的使用
- 一个 http 请求与响应的具体过程
- http 缓存策略
- http、url、crypto 等核心模块的使用
希望这篇文章可以给大家带来一些收获~~~
也可以看下这一系列的其它文章~~~
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)