Skip to content

Node.js HTTP 与文件操作

从文件 I/O 到 HTTP 服务,理解 Node 作为 BFF / 网关层的优势。

fs.readFile vs createReadStream

根据数据规模和处理方式选择:

readFile() 的场景

js
import { readFile } from 'fs/promises'

// 文件小、一次性读取配置/模板/小文本
const config = JSON.parse(await readFile('config.json', 'utf-8'))
const template = await readFile('template.html', 'utf-8')

createReadStream() 的场景

js
import { createReadStream } from 'fs'
import { pipeline } from 'stream/promises'
import { createGzip } from 'zlib'

// 大文件、要边读边传、要和压缩/网络/加密流串起来
await pipeline(
  createReadStream('large-video.mp4'),
  createGzip(),
  response          // HTTP 响应流
)

权衡:

对比readFilecreateReadStream
开发便利性✅ 简单稍复杂
内存占用文件大小固定缓冲区(KB 级)
适合文件大小小(< 几十 MB)任意大小
适合长期运行服务不推荐大文件✅ 推荐

HTTP 服务的本质

js
import http from 'http'

const server = http.createServer((req, res) => {
  // req 是 IncomingMessage(Readable 流)
  // res 是 ServerResponse(Writable 流)

  console.log(req.method, req.url)

  // 读取请求体(流式)
  let body = ''
  req.on('data', chunk => { body += chunk })
  req.on('end', () => {
    const data = JSON.parse(body)

    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ received: data }))
  })
})

server.listen(3000, () => console.log('Server running on :3000'))

理解要点:

  • 请求本质是字节流,不是天然 JSON
  • 请求头、方法、URL、body 都要分别处理
  • 响应必须显式结束(res.end()
  • 请求体很大时,应该按流处理,而不是默认一次性拼到内存

把 HTTP 理解成"协议 + 流 + 连接管理",而不是"框架帮我拿个 req/res"。


为什么 Node 适合做 BFF / 网关 / 中间层

这些场景通常不是超重 CPU,而是:

  • 聚合多个接口
  • 协议转换(REST → gRPC、GraphQL → REST)
  • 转发请求
  • 拼装/裁剪数据
  • 控制缓存
  • 管理登录态和鉴权

这些都偏 I/O 密集、编排型逻辑,正好符合 Node 的优势。

js
// BFF 典型模式:并发聚合多个下游接口
async function getProductDetail(id) {
  const [product, reviews, inventory] = await Promise.all([
    productService.getById(id),
    reviewService.getByProductId(id),
    inventoryService.getStock(id),
  ])

  return {
    ...product,
    reviews: reviews.slice(0, 5),
    inStock: inventory.quantity > 0,
  }
}

Node 适合做"离用户近、离 I/O 近、离前端协议近"的一层,尤其适合前端团队向服务端延伸时做 BFF。


流式处理:上传、代理、下载

js
// 文件上传:流式落盘(不缓存到内存)
app.post('/upload', (req, res) => {
  const dest = fs.createWriteStream('./uploads/file.bin')
  pipeline(req, dest)
    .then(() => res.json({ ok: true }))
    .catch(err => res.status(500).json({ error: err.message }))
})

// 代理转发:不缓存,直接透传
app.get('/proxy/:path', async (req, res) => {
  const upstream = await fetch(`https://api.upstream.com/${req.params.path}`)
  upstream.body.pipe(res)
})

// 大文件下载:带 Range 支持
app.get('/download/:file', (req, res) => {
  const filePath = path.join('./files', req.params.file)
  const stat = fs.statSync(filePath)

  const range = req.headers.range
  if (range) {
    const [start, end] = range.replace('bytes=', '').split('-').map(Number)
    const chunk = end - start + 1
    res.writeHead(206, {
      'Content-Range': `bytes ${start}-${end}/${stat.size}`,
      'Content-Length': chunk,
      'Content-Type': 'application/octet-stream',
    })
    fs.createReadStream(filePath, { start, end }).pipe(res)
  } else {
    res.writeHead(200, { 'Content-Length': stat.size })
    fs.createReadStream(filePath).pipe(res)
  }
})

为什么大数据量场景流式处理几乎是必选项:

如果总想"先全部收齐,再一次性处理",很容易带来:

  • 内存暴涨
  • 首字节延迟变长
  • 错误恢复差
  • 进程不稳定

网络和磁盘本来就是分段到达、分段写入的,流式处理更符合底层设备的工作方式。


path 模块常用 API

js
import path from 'path'

path.join('/usr', 'local', 'bin')    // '/usr/local/bin'
path.resolve('./config.json')        // 绝对路径
path.basename('/home/user/file.txt') // 'file.txt'
path.extname('index.html')           // '.html'
path.dirname('/home/user/file.txt')  // '/home/user'

// 解构路径
path.parse('/home/user/file.txt')
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

优雅退出

js
// HTTP server + 数据库连接的优雅退出
async function shutdown(signal) {
  console.log(`收到 ${signal},开始优雅退出...`)

  server.close(async () => {
    console.log('HTTP 服务已停止接受新请求')
    await db.pool.end()
    console.log('数据库连接已关闭')
    process.exit(0)
  })

  // 强制超时退出
  setTimeout(() => {
    console.error('优雅退出超时,强制退出')
    process.exit(1)
  }, 10_000)
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))