MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js 使用文件系统构建静态资源服务器

2024-12-101.6k 阅读

Node.js 文件系统模块基础

文件系统模块简介

在Node.js中,文件系统(fs)模块是一个核心模块,它提供了与文件系统进行交互的丰富功能。通过fs模块,开发者可以轻松地读取、写入、创建和删除文件与目录,这对于构建静态资源服务器至关重要。因为静态资源服务器的主要职责就是从文件系统中读取静态文件(如HTML、CSS、JavaScript、图片等),并将其发送给客户端。

fs模块有两种使用方式:同步和异步。同步方法会阻塞Node.js事件循环,直到操作完成,这在处理I/O密集型任务时可能导致性能问题,但在某些简单场景或初始化阶段很有用。而异步方法不会阻塞事件循环,允许Node.js继续处理其他任务,更适合生产环境中的高并发场景。

常用的文件系统操作

  1. 读取文件
    • 异步读取:使用fs.readFile方法,它接受三个参数:文件路径、编码格式(可选)和回调函数。回调函数的第一个参数是错误对象(如果操作失败),第二个参数是文件内容。
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});
- **同步读取**:`fs.readFileSync`方法接受文件路径和编码格式(可选)作为参数,并返回文件内容。如果操作失败,会抛出异常。
const fs = require('fs');
try {
    const data = fs.readFileSync('example.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error(err);
}
  1. 写入文件
    • 异步写入fs.writeFile方法用于异步写入文件。它接受四个参数:文件路径、要写入的数据、选项(可选,如编码格式、文件模式等)和回调函数。回调函数只有在操作失败时会传递错误对象。
const fs = require('fs');
const content = 'This is some sample content';
fs.writeFile('newFile.txt', content, 'utf8', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('File written successfully');
});
- **同步写入**:`fs.writeFileSync`方法接受文件路径、要写入的数据和选项(可选)作为参数,并在操作失败时抛出异常。
const fs = require('fs');
const content = 'This is some sample content';
try {
    fs.writeFileSync('newFile.txt', content, 'utf8');
    console.log('File written successfully');
} catch (err) {
    console.error(err);
}
  1. 创建目录
    • 异步创建fs.mkdir方法用于异步创建目录。它接受目录路径、选项(可选,如目录权限)和回调函数。回调函数在操作失败时传递错误对象。
const fs = require('fs');
fs.mkdir('newDirectory', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory created successfully');
});
- **同步创建**:`fs.mkdirSync`方法接受目录路径和选项(可选)作为参数,并在操作失败时抛出异常。
const fs = require('fs');
try {
    fs.mkdirSync('newDirectory');
    console.log('Directory created successfully');
} catch (err) {
    console.error(err);
}
  1. 读取目录
    • 异步读取fs.readdir方法用于异步读取目录内容。它接受目录路径和选项(可选,如编码格式)以及回调函数。回调函数的第一个参数是错误对象,第二个参数是目录中文件和子目录的名称数组。
const fs = require('fs');
fs.readdir('someDirectory', (err, files) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(files);
});
- **同步读取**:`fs.readdirSync`方法接受目录路径和选项(可选)作为参数,并返回目录中文件和子目录的名称数组。如果操作失败,会抛出异常。
const fs = require('fs');
try {
    const files = fs.readdirSync('someDirectory');
    console.log(files);
} catch (err) {
    console.error(err);
}

HTTP 服务器基础

HTTP 协议概述

HTTP(Hyper - Text Transfer Protocol)是一种应用层协议,用于在Web浏览器和Web服务器之间传输超文本。它基于请求 - 响应模型,客户端(通常是浏览器)向服务器发送HTTP请求,服务器根据请求返回相应的HTTP响应。

一个HTTP请求由请求行、请求头、空行和请求体组成。请求行包含请求方法(如GET、POST、PUT、DELETE等)、请求URL和HTTP版本。请求头包含关于请求的元数据,如客户端的浏览器信息、接受的数据类型等。请求体则包含实际发送的数据,通常在POST请求中使用。

HTTP响应同样由状态行、响应头、空行和响应体组成。状态行包含HTTP版本、状态码(如200表示成功,404表示未找到等)和状态消息。响应头提供关于响应的元数据,如内容类型、内容长度等。响应体则是服务器返回给客户端的实际数据,如HTML页面、JSON数据等。

Node.js 中的 HTTP 模块

Node.js的http模块是构建HTTP服务器的核心。通过http模块,我们可以创建一个简单的HTTP服务器,并定义如何处理客户端的请求。

以下是一个基本的HTTP服务器示例:

const http = require('http');
const server = http.createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader('Content - Type', 'text/plain');
    res.end('Hello, World!');
});
const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这个示例中,http.createServer方法创建了一个新的HTTP服务器实例。该方法接受一个回调函数,这个回调函数在每次有客户端请求到达时被调用。回调函数的两个参数req(请求对象)和res(响应对象)分别用于获取请求信息和发送响应。

res.statusCode设置响应的状态码,res.setHeader设置响应头,res.end方法用于结束响应并发送响应体。server.listen方法用于启动服务器并监听指定的端口。

构建静态资源服务器

服务器架构设计

  1. 请求处理流程
    • 当客户端发送一个HTTP请求到静态资源服务器时,服务器首先解析请求URL。如果请求的是一个静态资源(如图片、CSS文件、JavaScript文件等),服务器需要从文件系统中读取相应的文件,并将其作为响应发送给客户端。
    • 服务器需要处理不同类型的静态资源,为每种资源设置正确的Content - Type响应头。例如,对于HTML文件,Content - Type应设置为text/html;对于CSS文件,应设置为text/css;对于JavaScript文件,应设置为application/javascript;对于图片文件,根据图片格式设置为image/jpegimage/png等。
    • 如果请求的资源不存在,服务器应返回404状态码和相应的错误信息。
  2. 目录结构规划
    • 通常,静态资源服务器会有一个根目录,所有的静态资源都存储在这个根目录及其子目录下。例如,可以有一个public目录作为静态资源的根目录,在public目录下再细分htmlcssjsimages等子目录分别存放不同类型的资源。
    • 服务器在处理请求时,需要根据请求URL将其映射到文件系统中的实际路径。例如,如果请求的URL是/css/style.css,服务器需要在public/css/style.css路径下查找该文件。

代码实现

  1. 初始化项目 首先,创建一个新的目录用于项目,然后在该目录下初始化一个package.json文件。在终端中执行以下命令:
mkdir static - server
cd static - server
npm init -y
  1. 编写服务器代码
const http = require('http');
const fs = require('fs');
const path = require('path');
const mime = require('mime - types');

const server = http.createServer((req, res) => {
    // 获取请求路径并将其映射到文件系统路径
    let filePath = path.join(__dirname, 'public', req.url === '/'? 'index.html' : req.url);
    // 检查文件是否存在
    fs.access(filePath, fs.constants.F_OK, (err) => {
        if (err) {
            // 文件不存在,返回404
            res.statusCode = 404;
            res.setHeader('Content - Type', 'text/plain');
            res.end('404 Not Found');
        } else {
            // 文件存在,读取文件并设置正确的Content - Type
            const contentType = mime.lookup(filePath);
            res.setHeader('Content - Type', contentType);
            const readStream = fs.createReadStream(filePath);
            readStream.pipe(res);
            readStream.on('error', (err) => {
                res.statusCode = 500;
                res.setHeader('Content - Type', 'text/plain');
                res.end('Internal Server Error');
            });
        }
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这段代码中: - 引入了httpfspathmime - types模块。http用于创建HTTP服务器,fs用于文件系统操作,path用于处理文件路径,mime - types用于根据文件扩展名获取正确的Content - Type。 - 在http.createServer的回调函数中,首先将请求URL映射到文件系统路径。如果请求的是根路径/,则默认返回index.html。 - 使用fs.access方法检查文件是否存在。如果文件不存在,返回404状态码和错误信息。 - 如果文件存在,使用mime.lookup方法获取文件的Content - Type,设置到响应头中。然后使用fs.createReadStream创建一个可读流来读取文件,并通过pipe方法将读取的内容直接发送到响应流中。如果在读取文件过程中发生错误,返回500状态码和错误信息。

  1. 安装依赖 由于代码中使用了mime - types模块,需要安装它。在终端中执行以下命令:
npm install mime - types

处理不同类型的静态资源

  1. HTML 文件 对于HTML文件,Content - Type设置为text/html。在前面的代码中,通过mime - types模块会自动根据文件扩展名识别并设置正确的Content - Type。当客户端请求HTML文件时,服务器读取文件内容并发送给客户端,客户端的浏览器会解析并渲染HTML内容。
  2. CSS 文件 CSS文件的Content - Typetext/css。同样,mime - types模块会正确识别并设置。CSS文件通常用于为HTML页面提供样式,当浏览器加载HTML页面时,会根据HTML中的<link>标签请求相应的CSS文件,服务器将CSS文件内容发送给浏览器,浏览器应用这些样式来美化页面。
  3. JavaScript 文件 JavaScript文件的Content - Typeapplication/javascript。JavaScript文件用于为网页添加交互功能。浏览器在加载HTML页面时,会根据<script>标签请求JavaScript文件,服务器发送文件内容,浏览器解析并执行JavaScript代码。
  4. 图片文件 图片文件有多种格式,如JPEG、PNG、GIF等。对于JPEG图片,Content - Typeimage/jpeg;对于PNG图片,Content - Typeimage/png;对于GIF图片,Content - Typeimage/gifmime - types模块能够准确识别这些格式并设置正确的Content - Type。当HTML页面中有<img>标签引用图片时,浏览器会向服务器请求图片文件,服务器发送图片内容,浏览器将其显示在页面中。

优化与扩展

  1. 缓存机制 为了提高性能,可以为静态资源添加缓存机制。可以通过设置Cache - ControlETag响应头来实现。
    • Cache - Control:它用于指定缓存策略。例如,可以设置Cache - Control: public, max - age = 31536000表示资源可以被公共缓存(如CDN)缓存,缓存有效期为一年(31536000秒)。
    • ETag:它是资源的唯一标识符。服务器可以根据文件的内容生成一个ETag值,并在响应头中返回。客户端下次请求时,会在请求头中发送If - None - Match字段,其值为上次响应的ETag值。服务器可以比较这个值和当前资源的ETag值,如果相同,则返回304状态码,表示资源未修改,客户端可以使用缓存的资源。
const http = require('http');
const fs = require('fs');
const path = require('path');
const mime = require('mime - types');
const crypto = require('crypto');

const server = http.createServer((req, res) => {
    let filePath = path.join(__dirname, 'public', req.url === '/'? 'index.html' : req.url);
    fs.access(filePath, fs.constants.F_OK, (err) => {
        if (err) {
            res.statusCode = 404;
            res.setHeader('Content - Type', 'text/plain');
            res.end('404 Not Found');
        } else {
            const stats = fs.statSync(filePath);
            const etag = crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
            if (req.headers['if - none - match'] === etag) {
                res.statusCode = 304;
                res.end();
            } else {
                const contentType = mime.lookup(filePath);
                res.setHeader('Content - Type', contentType);
                res.setHeader('Cache - Control', 'public, max - age = 31536000');
                res.setHeader('ETag', etag);
                const readStream = fs.createReadStream(filePath);
                readStream.pipe(res);
                readStream.on('error', (err) => {
                    res.statusCode = 500;
                    res.setHeader('Content - Type', 'text/plain');
                    res.end('Internal Server Error');
                });
            }
        }
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这段代码中,通过crypto模块生成文件的MD5哈希值作为ETag。在每次请求时,检查If - None - Match头,如果匹配则返回304状态码。同时设置了Cache - Control头来指定缓存策略。 2. 压缩资源 为了减少网络传输的数据量,可以对静态资源进行压缩。Node.js可以使用zlib模块来实现压缩。

const http = require('http');
const fs = require('fs');
const path = require('path');
const mime = require('mime - types');
const zlib = require('zlib');

const server = http.createServer((req, res) => {
    let filePath = path.join(__dirname, 'public', req.url === '/'? 'index.html' : req.url);
    fs.access(filePath, fs.constants.F_OK, (err) => {
        if (err) {
            res.statusCode = 404;
            res.setHeader('Content - Type', 'text/plain');
            res.end('404 Not Found');
        } else {
            const contentType = mime.lookup(filePath);
            res.setHeader('Content - Type', contentType);
            const acceptEncoding = req.headers['accept - encoding'];
            let writeStream = res;
            if (acceptEncoding && acceptEncoding.match(/\b(gzip|deflate)\b/)) {
                if (acceptEncoding.match(/\bgzip\b/)) {
                    res.setHeader('Content - Encoding', 'gzip');
                    writeStream = zlib.createGzip().pipe(res);
                } else if (acceptEncoding.match(/\bdeflate\b/)) {
                    res.setHeader('Content - Encoding', 'deflate');
                    writeStream = zlib.createDeflate().pipe(res);
                }
            }
            const readStream = fs.createReadStream(filePath);
            readStream.pipe(writeStream);
            readStream.on('error', (err) => {
                res.statusCode = 500;
                res.setHeader('Content - Type', 'text/plain');
                res.end('Internal Server Error');
            });
        }
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这段代码中,检查客户端请求头中的Accept - Encoding字段,如果包含gzipdeflate,则使用相应的压缩方式对文件进行压缩,并设置Content - Encoding响应头。

  1. 支持子目录和动态请求处理 为了支持更复杂的目录结构和动态请求处理,可以进一步扩展服务器逻辑。例如,可以添加对动态脚本的支持,通过解析URL参数来处理不同的请求。
const http = require('http');
const fs = require('fs');
const path = require('path');
const mime = require('mime - types');
const url = require('url');

const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    let filePath = path.join(__dirname, 'public', parsedUrl.pathname === '/'? 'index.html' : parsedUrl.pathname);
    fs.access(filePath, fs.constants.F_OK, (err) => {
        if (err) {
            // 尝试处理动态请求,例如 /api/user?id = 1
            if (parsedUrl.pathname.startsWith('/api')) {
                // 这里可以根据URL参数进行数据库查询等操作
                res.statusCode = 200;
                res.setHeader('Content - Type', 'application/json');
                const responseData = { message: 'This is a dynamic response' };
                res.end(JSON.stringify(responseData));
            } else {
                res.statusCode = 404;
                res.setHeader('Content - Type', 'text/plain');
                res.end('404 Not Found');
            }
        } else {
            const contentType = mime.lookup(filePath);
            res.setHeader('Content - Type', contentType);
            const readStream = fs.createReadStream(filePath);
            readStream.pipe(res);
            readStream.on('error', (err) => {
                res.statusCode = 500;
                res.setHeader('Content - Type', 'text/plain');
                res.end('Internal Server Error');
            });
        }
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server running on port ${port}`);
});

在这段代码中,使用url模块解析URL。如果请求的路径不存在且以/api开头,则尝试处理动态请求,这里简单返回一个JSON格式的响应。这样服务器既可以处理静态资源请求,也能处理一些简单的动态请求。

通过以上步骤,我们详细介绍了如何使用Node.js的文件系统模块构建一个功能丰富且性能优化的静态资源服务器。从基础的文件系统操作和HTTP服务器原理,到具体的代码实现以及优化扩展,希望能够帮助开发者更好地理解和构建自己的静态资源服务器。