Skip to content
字数
438 字
阅读时间
3 分钟

实现一个静态资源服务器

可实现以下功能:

  • 命令行的方式,可传入端口号及目标目录
  • 处理 404
  • 流式返回资源,处理 Content-Length
  • 处理目录,如果请求路径是目录,则自动请求其目录中的 index.html

以下功能选做:

  • rewrites
  • redirects
  • cleanUrls
  • trailingSlash
  • etag
  • symlink
ts
import path from "node:path";
import http, { IncomingMessage, ServerResponse } from "node:http";
import fs from "node:fs";
import { URL } from "node:url";

import arg from "arg";

type Rewrite = {
  source: string;
  destination: string;
};

type Redirect = Rewrite;

interface Config {
  entry?: string;
  rewrites?: Rewrite[];
  redirects?: Redirect[];
  etag?: boolean;
  cleanUrls?: boolean;
  trailingSlash?: boolean;
  symlink?: boolean;
}

async function processDirectory(
  absolutePath: string
): Promise<[fs.Stats | null, string]> {
  const newAbsolutePath = path.join(absolutePath, "index.html");

  try {
    const newStat = await fs.promises.lstat(newAbsolutePath);
    return [newStat, newAbsolutePath];
  } catch (e) {
    return [null, newAbsolutePath];
  }
}

// 响应 404,此处可做一个优化,比如读取文件系统中的 404.html 文件
function responseNotFound(res: ServerResponse) {
  res.statusCode = 404;
  res.end("NNNNNNot Found");
}

// mime:
export default async function handler(
  req: IncomingMessage,
  res: ServerResponse,
  config: Config
) {
  const pathname = new URL("http://localhost:3000" + req.url ?? "").pathname;

  let absolutePath = path.resolve(config.entry ?? "", path.join(".", pathname));
  let statusCode = 200;
  let stat = null;

  try {
    stat = await fs.promises.lstat(absolutePath);
  } catch (e) {}

  if (stat?.isDirectory()) {
    // 如果是目录,则去寻找目录中的 index.html
    [stat, absolutePath] = await processDirectory(absolutePath);
  }

  if (stat === null) {
    return responseNotFound(res);
  }

  let headers = {
    // 取其文件系统中的体积作为其大小
    // 问: 文件的大小与其编码格式有关,那么文件系统的体积应该是如何确定的?
    "Content-Length": stat.size,
  };

  res.writeHead(statusCode, headers);

  fs.createReadStream(absolutePath).pipe(res);
}

const args = arg({
  "--port": Number,
  "-p": "--port",
});

function startEndpoint(port: number, entry: string) {
  const server = http.createServer((req, res) => {
    handler(req, res, { entry });
  });

  server.on("error", (err) => {
    // 表示端口号已被占用
    if ((err as any).code === "EADDRINUSE") {
      startEndpoint(port + 1, entry);
      return;
    }

    process.exit(1);
  });

  server.listen(port, () => {
    console.log(`Open http://localhost:${port}`);
  });
}

startEndpoint(args["--port"] ?? 3000, args._[0]);

贡献者

jiechen

文件历史