Node.js 使用文件系统构建静态资源服务器
Node.js 文件系统模块基础
文件系统模块简介
在Node.js中,文件系统(fs
)模块是一个核心模块,它提供了与文件系统进行交互的丰富功能。通过fs
模块,开发者可以轻松地读取、写入、创建和删除文件与目录,这对于构建静态资源服务器至关重要。因为静态资源服务器的主要职责就是从文件系统中读取静态文件(如HTML、CSS、JavaScript、图片等),并将其发送给客户端。
fs
模块有两种使用方式:同步和异步。同步方法会阻塞Node.js事件循环,直到操作完成,这在处理I/O密集型任务时可能导致性能问题,但在某些简单场景或初始化阶段很有用。而异步方法不会阻塞事件循环,允许Node.js继续处理其他任务,更适合生产环境中的高并发场景。
常用的文件系统操作
- 读取文件
- 异步读取:使用
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);
}
- 写入文件
- 异步写入:
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);
}
- 创建目录
- 异步创建:
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);
}
- 读取目录
- 异步读取:
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
方法用于启动服务器并监听指定的端口。
构建静态资源服务器
服务器架构设计
- 请求处理流程
- 当客户端发送一个HTTP请求到静态资源服务器时,服务器首先解析请求URL。如果请求的是一个静态资源(如图片、CSS文件、JavaScript文件等),服务器需要从文件系统中读取相应的文件,并将其作为响应发送给客户端。
- 服务器需要处理不同类型的静态资源,为每种资源设置正确的
Content - Type
响应头。例如,对于HTML文件,Content - Type
应设置为text/html
;对于CSS文件,应设置为text/css
;对于JavaScript文件,应设置为application/javascript
;对于图片文件,根据图片格式设置为image/jpeg
、image/png
等。 - 如果请求的资源不存在,服务器应返回404状态码和相应的错误信息。
- 目录结构规划
- 通常,静态资源服务器会有一个根目录,所有的静态资源都存储在这个根目录及其子目录下。例如,可以有一个
public
目录作为静态资源的根目录,在public
目录下再细分html
、css
、js
、images
等子目录分别存放不同类型的资源。 - 服务器在处理请求时,需要根据请求URL将其映射到文件系统中的实际路径。例如,如果请求的URL是
/css/style.css
,服务器需要在public/css/style.css
路径下查找该文件。
- 通常,静态资源服务器会有一个根目录,所有的静态资源都存储在这个根目录及其子目录下。例如,可以有一个
代码实现
- 初始化项目
首先,创建一个新的目录用于项目,然后在该目录下初始化一个
package.json
文件。在终端中执行以下命令:
mkdir static - server
cd static - server
npm init -y
- 编写服务器代码
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}`);
});
在这段代码中:
- 引入了http
、fs
、path
和mime - 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状态码和错误信息。
- 安装依赖
由于代码中使用了
mime - types
模块,需要安装它。在终端中执行以下命令:
npm install mime - types
处理不同类型的静态资源
- HTML 文件
对于HTML文件,
Content - Type
设置为text/html
。在前面的代码中,通过mime - types
模块会自动根据文件扩展名识别并设置正确的Content - Type
。当客户端请求HTML文件时,服务器读取文件内容并发送给客户端,客户端的浏览器会解析并渲染HTML内容。 - CSS 文件
CSS文件的
Content - Type
为text/css
。同样,mime - types
模块会正确识别并设置。CSS文件通常用于为HTML页面提供样式,当浏览器加载HTML页面时,会根据HTML中的<link>
标签请求相应的CSS文件,服务器将CSS文件内容发送给浏览器,浏览器应用这些样式来美化页面。 - JavaScript 文件
JavaScript文件的
Content - Type
是application/javascript
。JavaScript文件用于为网页添加交互功能。浏览器在加载HTML页面时,会根据<script>
标签请求JavaScript文件,服务器发送文件内容,浏览器解析并执行JavaScript代码。 - 图片文件
图片文件有多种格式,如JPEG、PNG、GIF等。对于JPEG图片,
Content - Type
为image/jpeg
;对于PNG图片,Content - Type
为image/png
;对于GIF图片,Content - Type
为image/gif
。mime - types
模块能够准确识别这些格式并设置正确的Content - Type
。当HTML页面中有<img>
标签引用图片时,浏览器会向服务器请求图片文件,服务器发送图片内容,浏览器将其显示在页面中。
优化与扩展
- 缓存机制
为了提高性能,可以为静态资源添加缓存机制。可以通过设置
Cache - Control
和ETag
响应头来实现。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
字段,如果包含gzip
或deflate
,则使用相应的压缩方式对文件进行压缩,并设置Content - Encoding
响应头。
- 支持子目录和动态请求处理 为了支持更复杂的目录结构和动态请求处理,可以进一步扩展服务器逻辑。例如,可以添加对动态脚本的支持,通过解析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服务器原理,到具体的代码实现以及优化扩展,希望能够帮助开发者更好地理解和构建自己的静态资源服务器。