Skip to content

手写最简静态资源服务器

一个简单的网页

在开始部署一个静态资源服务器之前,我们需要先写一个简单的网页供浏览。

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>

HTTP

HTTP 即超文本传输协议,是一个用于传输超文本文档(例如 HTML)的应用层协议。客户端打开一个连接以发送请求,然后等待直到收到服务器响应。最简部署即客户端向服务器发送一个 HTML 文件请求,服务器响应一段 HTML 资源。

在浏览器中访问任意网页,可以自行查看网页的 HTTP 请求和响应报文。

最简静态资源服务器:字符串

作为前端,我们使用 node 来搭建静态资源服务。该服务中,监听 3000 端口,在响应体中返回上文的网页。

首先以字符串返回响应。

node 中,写服务端的最重要模块为 node:http。通过 node: 前缀,可以指明其为内部模块,避免 node 内置模块与第三方模块的命名冲突。

js
const http = require('node:http')

通过 http.createServer 可以向外提供 HTTP 服务。res.end() 可以设置 HTTP 报文的响应体。我们先简单响应一个字符串。

js
const server = http.createServer((req, res) => {
  res.end('hello world')
})

server.listen(3000, () => {
  console.log('server running at 3000')
})

运行服务,在浏览器里打开 3000 端口。

将网页内容作为字符串传入,就可以使用 res.end() 来响应报文。

js
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 内置模块来读取文件内容。

js
const fs = require('node:fs')

const html = fs.readFileSync('./index.html')

再使用 res.end() 响应该文件。

js
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 之类的专业静态资源服务器呢?因为对于前端这类纯静态资源,自己写代码无论从开发还是性能而言都是极差的。

  1. 对开发而言,基本的 rewriteredirect 功能都需要重新开发。完备的静态资源服务器需要满足的功能都需要开发的情况下,增加了我们的开发负担。
  2. 对性能而言,单纯地使用文件系统读取文件后返回响应的性能并不高,我们需要使用其他方法来尽可能地提升静态服务器的性能。比如使用 ReadStream 读取文件流的方法。JavaScript 的性能始终有限,使用专业的静态资源服务器拥有更佳的性能。

nginx 也能实现很多必要的功能,如反向代理、TLSGZIPHTTP2 等;同时,因为 npm run dev 命令往往需要进行文件的监听并重启服务,会消耗较大的内存及 CPU,所以也不推荐使用这种方式来作为服务的部署。

部署的简单理解

简单来说,只要服务运行后能够通过 IP 加端口的方式进行访问那就是部署成功。如果有一台具备公网 IP 的服务器,那么部署在上面所有人都能访问到服务了。

进步演练

继续完善静态资源服务器

我们继续完善最简静态资源服务,优化性能。使其基于 stream,并能给出正确的 content-length

首先,使用文件读取流读取文件并通过 pipe 响应。

js
const server = http.createServer((req, res) => {
  fs.createReadStream('./index.html').pipe(res)
})

运行服务,查看响应头,可以看到该网页已经使用分块的形式进行发送。

读取文件信息后,再向响应头写入 Content-length

js
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 与其相比的编码格式更小。

Released under the MIT License.