一、回顾与本篇目标
上一篇我们写了第一个 Node.js 后端程序,7 行代码启动了一个 HTTP 服务。但那 7 行代码全部挤在一个文件里。你可以想象,如果整个后端项目——处理用户登录、操作数据库、发送邮件、上传文件——全部写在一个文件里,这个文件会有几千行甚至上万行。改一个地方要翻半天,出了 bug 找不到源头,想复用一段逻辑只能复制粘贴。
解决这个问题的方法叫模块化:把代码按照功能拆成多个文件,每个文件负责一件事。需要的时候,通过 require 把别的文件加载进来。
本篇的目标:
- 彻底理解
require的用法和查找规则 - 学会用
module.exports导出自己的函数和数据 - 认识两个最常用的内置模块:
fs(文件系统)和path(路径处理) - 把上一篇的 HTTP 服务拆成多个模块
二、为什么需要模块
先看一个反面例子。假设你要写一个后端程序,功能包括:
- 启动 HTTP 服务
- 处理用户登录
- 读写文件
- 发送邮件
如果全部写在一个 app.js 里:
// app.js —— 所有功能混在一起(反面教材,不要这样写)
const http = require('http');
const fs = require('fs');
// 登录相关的函数
function checkPassword(username, password) { /* 50 行代码 */ }
function createToken(user) { /* 30 行代码 */ }
// 文件相关的函数
function readUserData() { /* 40 行代码 */ }
function writeUserData(data) { /* 40 行代码 */ }
// 邮件相关的函数
function sendWelcomeEmail(email) { /* 60 行代码 */ }
function sendResetPasswordEmail(email) { /* 60 行代码 */ }
// HTTP 服务
const server = http.createServer(function (request, response) {
// 处理各种路由,又是几百行
});
server.listen(3000);
这个文件很快就会变成一坨灾难:
- 想找到处理登录的那段逻辑,要在一大堆无关代码里翻。
- 改了一个函数,不小心影响了另一个函数,因为它们共享了一些变量名。
- 如果另一个项目也需要用邮件发送功能,你只能把这个文件里的相关代码复制过去——然后两个地方的代码逐渐变得不一样,修了这头的 bug,那头还有。
模块化的思路:把不同功能的代码拆到不同的文件里,每个文件只负责一件事。
项目文件夹/
├── app.js ← 只负责启动 HTTP 服务
├── auth.js ← 只负责登录验证
├── userData.js ← 只负责读写用户数据
├── email.js ← 只负责发送邮件
每个文件之间通过 require 和 module.exports 来互相调用。这样每个文件都不长,改什么功能就打开对应的文件,不会影响其他模块。
三、require:加载模块
require 是 Node.js 中最核心的函数。它的作用是:找到指定的模块文件,执行里面的代码,然后把模块导出的内容返回给你。
三种 require 的写法
1. 加载内置模块:直接写模块名
const http = require('http');
const fs = require('fs');
const path = require('path');
Node.js 自带了几个核心模块,直接写名字就能加载。常见的除了 http、fs、path,还有 os(操作系统信息)、url(URL 解析)、crypto(加密)等。
2. 加载自己写的模块:写相对路径
const auth = require('./auth.js'); // 同一文件夹下的 auth.js
const userData = require('./data/user.js'); // 子文件夹 data 里的 user.js
const config = require('../config.js'); // 上一级文件夹里的 config.js
注意:路径必须以 ./ 或 ../ 开头,否则 Node.js 会以为你要加载的是内置模块或第三方模块。
3. 加载第三方模块:写包名
const express = require('express');
const mysql = require('mysql2');
第三方模块是通过 npm install 安装的,后面我们会学。现在先聚焦前两种。
require 的内部行为
当你写 const xxx = require('./someModule.js'); 时,Node.js 做了这几件事:
- 找到
someModule.js这个文件。 - 从头到尾执行这个文件里的所有代码。
- 把这个文件里通过
module.exports导出的内容返回给你。 - 把返回的内容存到你指定的变量(这里是
xxx)里。 - 如果同一个模块被
require了多次,Node.js 会缓存它。只有第一次会真正执行文件,后续的require直接返回缓存的结果。这避免了重复执行的开销。
四、module.exports:导出功能
一个模块文件里定义了函数和变量,但默认情况下这些函数和变量是私有的——其他文件 require 了它也访问不到。要暴露出去给别人用,必须通过 module.exports。
基础用法
第一步:创建一个工具模块
新建一个文件叫 math.js:
// math.js —— 数学工具模块
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
// 把需要暴露的函数挂到 module.exports 上
module.exports.add = add;
module.exports.subtract = subtract;
module.exports.multiply = multiply;
第二步:在主文件里使用
新建一个文件叫 main.js(和 math.js 在同一文件夹):
// main.js
const math = require('./math.js');
console.log(math.add(10, 20)); // 30
console.log(math.subtract(50, 15)); // 35
console.log(math.multiply(3, 7)); // 21
在终端里执行 node main.js,输出三个计算结果。
发生了什么:
require('./math.js')执行了math.js里的代码,拿到了module.exports这个对象。- 这个对象上有三个方法:
add、subtract、multiply。 - 赋值给
math变量后,你就可以通过math.add()来调用这些方法。
更简洁的写法
上面的写法有点啰嗦——函数名写了两次。可以用对象简写:
// math.js —— 简洁版
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
function multiply(a, b) { return a * b; }
module.exports = {
add: add,
subtract: subtract,
multiply: multiply
};
或者更简洁(ES6 属性简写):
module.exports = {
add(a, b) { return a + b; },
subtract(a, b) { return a - b; },
multiply(a, b) { return a * b; }
};
导出单个函数
如果一个模块只导出一件事,可以直接给 module.exports 赋一个值(而不是对象):
// capitalize.js —— 只导出一个函数
module.exports = function (str) {
return str.charAt(0).toUpperCase() + str.slice(1);
};
// main.js
const capitalize = require('./capitalize.js');
console.log(capitalize('hello')); // 'Hello'
注意:这里 require 拿到的直接就是那个函数,不需要通过属性访问。
五、实战:把 HTTP 服务拆成两个模块
回顾上一篇的 app.js,它做了两件事:
- 启动 HTTP 服务(
createServer和listen)。 - 处理请求并返回内容(回调函数里的逻辑)。
现在我们把“处理请求”的逻辑拆到一个单独的模块里。
第 1 步:创建 handler.js——专门处理请求
// handler.js —— 请求处理模块
function handleRequest(request, response) {
// 不管请求什么路径,统一返回一段文字
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('这是从 handler 模块返回的内容。');
}
module.exports = handleRequest;
这个文件导出了一个函数 handleRequest。它接收 request 和 response 两个参数,设置状态码和响应体,然后结束响应。
第 2 步:修改 app.js——只负责启动服务
// app.js —— 主入口,只负责启动服务
const http = require('http');
const handleRequest = require('./handler.js');
const server = http.createServer(handleRequest);
server.listen(3000, function () {
console.log('服务已启动,访问 http://localhost:3000');
});
变化点:
- 原来
createServer的回调函数是一个完整的匿名函数,现在换成了从handler.js加载进来的handleRequest函数。 app.js现在只有 5 行有效代码,职责非常清晰:加载模块、创建服务、启动监听。
运行 node app.js,效果和之前完全一样。但代码已经拆成了两个文件。以后要修改请求处理逻辑,只需要打开 handler.js 改,不会碰到 app.js。
六、内置模块 fs:读写文件
fs 是 File System(文件系统)的缩写。这是 Node.js 中最常用的内置模块之一,让你能用 JavaScript 读写电脑上的文件。
读取文件:fs.readFileSync()(同步)
同步读取的意思是:读文件的时候,程序停下来等,直到文件读完才继续往下执行。
先在项目文件夹里新建一个 data.txt,写入任意几行文字。然后创建 readFileDemo.js:
// readFileDemo.js
const fs = require('fs');
// 同步读取文件,返回文件内容(字符串)
let content = fs.readFileSync('./data.txt', 'utf-8');
console.log('文件内容:');
console.log(content);
console.log('读取完毕。');
执行 node readFileDemo.js,终端会按顺序输出:“文件内容:”→ 文件的实际内容 →“读取完毕。”
参数解释:
'./data.txt':要读取的文件的路径。'utf-8':字符编码。如果不写这个参数,readFileSync返回的是 Buffer(二进制数据),人看不懂。加上'utf-8'后返回的就是可读的字符串。
写入文件:fs.writeFileSync()(同步)
// writeFileDemo.js
const fs = require('fs');
let data = '这是通过 Node.js 写入文件的内容。\n第二行文字。';
fs.writeFileSync('./output.txt', data, 'utf-8');
console.log('文件写入成功!检查 output.txt');
执行后,项目文件夹里会出现一个 output.txt,内容就是你写的那段字符串。
注意:如果 output.txt 已经存在,writeFileSync 会覆盖它的全部内容。如果文件不存在,会自动创建。
异步读写:fs.readFile() 和 fs.writeFile()
同步方法简单直观,但有一个致命问题:读大文件或者写大量数据的时候,程序会卡住,无法处理其他请求。所以实际项目中强烈推荐使用异步方法。
// asyncReadDemo.js
const fs = require('fs');
console.log('开始读文件...');
// 异步读取:不会阻塞后续代码
fs.readFile('./data.txt', 'utf-8', function (error, data) {
if (error) {
console.error('读文件出错:', error.message);
return;
}
console.log('文件内容:');
console.log(data);
});
console.log('这行会先打印出来,因为读文件是异步的。');
执行顺序:
开始读文件...
这行会先打印出来,因为读文件是异步的。
文件内容:
(data.txt 的实际内容)
回调函数的两个参数:
error:如果读文件过程中出错了(比如文件不存在、没权限),这个参数会包含错误信息。如果一切正常,它是null。data:读到的文件内容。
异步写文件同理:
fs.writeFile('./output.txt', '内容', 'utf-8', function (error) {
if (error) {
console.error('写文件出错:', error.message);
return;
}
console.log('写入成功');
});
七、内置模块 path:处理文件路径
在不同操作系统上,文件路径的写法不同。Windows 用反斜杠 \,Mac 和 Linux 用正斜杠 /。如果你在代码里写死路径(比如 './data/file.txt'),换一个操作系统可能会出问题。path 模块就是来解决这个问题的。
拼接路径:path.join()
const path = require('path');
let filePath = path.join(__dirname, 'data', 'users.json');
console.log(filePath);
// Windows 上输出:C:\Users\xxx\project\data\users.json
// Mac 上输出: /Users/xxx/project/data/users.json
__dirname:Node.js 提供的一个全局变量,它始终指向当前文件所在的文件夹的绝对路径。结合 path.join,就能拼出正确的完整路径,无论代码跑在什么操作系统上。
为什么不用字符串拼接:'./data/' + 'file.txt' 在 Windows 上可能变成 ./data/file.txt,在 Mac 上也是 ./data/file.txt。但绝对路径拼接时,Windows 是 C:\project\data + \file.txt,如果你写成 base + '/' + filename,在 Windows 上会出问题。用 path.join 自动处理这些差异。
获取文件扩展名、文件名等
let filePath = '/users/docs/report.pdf';
console.log(path.basename(filePath)); // 'report.pdf' —— 文件名
console.log(path.dirname(filePath)); // '/users/docs' —— 目录路径
console.log(path.extname(filePath)); // '.pdf' —— 扩展名
console.log(path.basename(filePath, path.extname(filePath))); // 'report' —— 不带扩展名的文件名
八、综合演示:用模块 + 文件系统做一个简单的接口
现在我们把本篇学的三个知识点——模块系统、fs 文件读写、path 路径处理——整合到一个完整的 HTTP 服务里。
需求:浏览器访问 http://localhost:3000,返回一个本地的 index.html 文件的内容。
项目结构:
项目文件夹/
├── app.js ← 主入口,启动服务
├── handler.js ← 处理请求,读取文件并返回
├── index.html ← 被返回的网页文件
第 1 步:创建 index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Node.js 返回的页面</title>
<style>
body { font-family: sans-serif; padding: 40px; background: #f0f4f8; }
h1 { color: #333; }
</style>
</head>
<body>
<h1>你好,这是 Node.js 返回的网页!</h1>
<p>这个页面的内容是从本地文件读取的,然后通过 HTTP 服务返回给浏览器。</p>
</body>
</html>
第 2 步:创建 handler.js
// handler.js —— 请求处理模块
const fs = require('fs');
const path = require('path');
function handleRequest(request, response) {
// 用 path.join 拼出 index.html 的完整路径
let filePath = path.join(__dirname, 'index.html');
// 异步读取 index.html 文件
fs.readFile(filePath, 'utf-8', function (error, data) {
if (error) {
// 如果读文件出错(比如文件不存在),返回 500
response.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('服务器内部错误:无法读取文件。');
return;
}
// 读取成功,返回文件内容
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(data);
});
}
module.exports = handleRequest;
第 3 步:创建 app.js
// app.js —— 主入口
const http = require('http');
const handleRequest = require('./handler.js');
const server = http.createServer(handleRequest);
server.listen(3000, function () {
console.log('服务已启动,访问 http://localhost:3000');
});
运行:在终端执行 node app.js,浏览器访问 http://localhost:3000。你会看到 index.html 的内容被渲染成网页显示出来。
这段代码的完整执行流程:
node app.js启动服务,监听 3000 端口。- 浏览器访问
http://localhost:3000,HTTP 请求到达服务。 app.js里createServer的回调是handleRequest,这个函数被调用。handleRequest用path.join拼出index.html的路径,用fs.readFile异步读取文件。- 文件读完之后,回调函数把文件内容写入响应体,设置
Content-Type为text/html,结束响应。 - 浏览器收到 HTML 内容,渲染成你看到的网页。
九、本篇小结
这一篇你学会了 Node.js 模块系统的完整用法:
require:加载模块。内置模块直接写名字,自定义模块写相对路径(./或../)。同一个模块被多次加载时会走缓存。module.exports:导出模块的功能。可以导出对象(多个函数)或单个值(单个函数)。其他文件通过require拿到这些导出的内容。fs模块:读写文件。readFileSync和writeFileSync是同步方法(简单但会阻塞);readFile和writeFile是异步方法(推荐,需要回调函数处理结果)。path模块:处理文件路径。path.join跨平台拼接路径;path.basename、path.extname等获取路径各部分信息。__dirname:当前文件所在文件夹的绝对路径,配合path.join使用。- 模块化的实际价值:代码按功能拆分,每个文件只负责一件事,方便维护和复用。
模块系统是 Node.js 后端的组织基础。把这一篇的内容练熟,你就能把任何复杂的功能拆成清晰的模块结构。
下一篇预告
下一篇——《HTTP 协议——浏览器和服务器怎么对话》:我们上一篇和这篇已经写了两个 HTTP 服务,但还没有真正理解 HTTP 协议本身。下一篇会拆开请求报文和响应报文,看看里面到底装了什么。你会学到 GET 和 POST 的区别、常见状态码的含义、请求头和响应头的作用。这之后,我们就能写出能区分不同 URL、处理不同请求方法的路由了。
后端零基础入门,每周更新。













暂无评论内容