手写最简静态资源服务器
一个简单的网页
在开始部署一个静态资源服务器之前,我们需要先写一个简单的网页供浏览。
<!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>Hello World</title>
</head>
<body>
<h1> hello, world </h1>
</body>
</html>
HTTP
HTTP 即超文本传输协议,是一个用于传输超文本文档(例如 HTML)的应用层协议。客户端打开一个连接以发送请求,然后等待直到收到服务器响应。最简部署即客户端向服务器发送一个 HTML 文件请求,服务器响应一段 HTML 资源。
在浏览器中访问任意网页,可以自行查看网页的 HTTP 请求和响应报文。
最简静态资源服务器:字符串
作为前端,我们使用 node
来搭建静态资源服务。该服务中,监听 3000
端口,在响应体中返回上文的网页。
首先以字符串返回响应。
在 node
中,写服务端的最重要模块为 node:http
。通过 node:
前缀,可以指明其为内部模块,避免 node
内置模块与第三方模块的命名冲突。
const http = require('node:http')
通过 http.createServer
可以向外提供 HTTP 服务。res.end()
可以设置 HTTP 报文的响应体。我们先简单响应一个字符串。
const server = http.createServer((req, res) => {
res.end('hello world')
})
server.listen(3000, () => {
console.log('server running at 3000')
})
运行服务,在浏览器里打开 3000
端口。
将网页内容作为字符串传入,就可以使用 res.end()
来响应报文。
const 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>Hello World</title>
</head>
<body>
<h1> hello, world </h1>
</body>
</html>
`
const server = http.createServer((req, res) => {
res.end(html)
})
server.listen(3000, () => {
console.log('server running at 3000')
})
查看响应头及响应体。
至此,一个最简单的静态资源服务部署成功。
最简静态资源服务器:文件
然而,前端资源总是以文件而并非字符串的形式出现。接下来就使用文件系统读取资源并将数据返回。
使用 node:fs
内置模块来读取文件内容。
const fs = require('node:fs')
const html = fs.readFileSync('./index.html')
再使用 res.end()
响应该文件。
const http = require('node:http')
const fs = require('node:fs')
const html = fs.readFileSync('./index.html')
const server = http.createServer((req, res) => {
res.end(html)
})
server.listen(3000, () => {
console.log('server running at 3000')
})
启动服务,成功运行。
为什么需要专业的静态资源服务器
我们可以自己手写静态服务器,那为什么还需要如 nginx
之类的专业静态资源服务器呢?因为对于前端这类纯静态资源,自己写代码无论从开发还是性能而言都是极差的。
- 对开发而言,基本的
rewrite
、redirect
功能都需要重新开发。完备的静态资源服务器需要满足的功能都需要开发的情况下,增加了我们的开发负担。 - 对性能而言,单纯地使用文件系统读取文件后返回响应的性能并不高,我们需要使用其他方法来尽可能地提升静态服务器的性能。比如使用
ReadStream
读取文件流的方法。JavaScript
的性能始终有限,使用专业的静态资源服务器拥有更佳的性能。
nginx
也能实现很多必要的功能,如反向代理、TLS
、GZIP
、HTTP2
等;同时,因为 npm run dev
命令往往需要进行文件的监听并重启服务,会消耗较大的内存及 CPU,所以也不推荐使用这种方式来作为服务的部署。
部署的简单理解
简单来说,只要服务运行后能够通过 IP 加端口的方式进行访问那就是部署成功。如果有一台具备公网 IP 的服务器,那么部署在上面所有人都能访问到服务了。
进步演练
继续完善静态资源服务器
我们继续完善最简静态资源服务,优化性能。使其基于 stream
,并能给出正确的 content-length
。
首先,使用文件读取流读取文件并通过 pipe
响应。
const server = http.createServer((req, res) => {
fs.createReadStream('./index.html').pipe(res)
})
运行服务,查看响应头,可以看到该网页已经使用分块的形式进行发送。
读取文件信息后,再向响应头写入 Content-length
。
const http = require('node:http')
const fs = require('node:fs')
const fsp = require('node:fs/promises')
const server = http.createServer(async (req, res) => {
const fileStat = await fsp.stat('./index.html')
res.setHeader('Content-Length', fileStat.size)
fs.createReadStream('./index.html').pipe(res)
})
server.listen(3000, () => {
console.log('server running at 3000')
})
运行服务,查看响应头,已经成功设置 Content-length
。但发现 Transfer-Encoding
消失。难道 stream
已经不是通过分块的方式传输了吗?
答:其实 stream
还是以分块的形式传播,只不过不是 Transfer-Encoding: chunked
这种编码格式。由于 chunked
每次还要传输 length
和 \r\n
作为每一段的标识,因此 Content-Lengh
与其相比的编码格式更小。