一,什么是NodeJs
Node.js 是一个让 JavaScript 在浏览器之外运行的环境(Runtime)。
它基于 Chrome V8 引擎,让 JS 代码可以像 Python、Java 一样直接运行在操作系统上,而不是依赖网页。
核心本质:Node.js = V8 + libuv
你可以这样理解 Node.js 的构成:
| 组件 | 作用 | 类比 |
|---|---|---|
| V8引擎 | 解析和执行JS代码 | 浏览器里的JS引擎 |
| libuv | 提供异步I/O和事件循环 | Node独有的"后台管家" |
| bindings | 让JS调用C++写的底层功能 | 胶水层 |
在浏览器里能用的console、setTimeout、Promise,都是 V8 提供的。而 Node.js 额外给你加了一堆浏览器里没有的东西:
fs读写文件http创建服务器child_process执行系统命令os获取系统信息path处理文件路径
所以:在浏览器里用 JS 控制网页 DOM,在 Node 里用 JS 控制操作系统资源。
二,什么是前端工程化?
前端工程化 = 用软件工程的方法,解决前端开发中的 "规模" 和 "效率" 问题。
说白了就是:当项目变大、人变多、需求变快时,怎么保证代码还能写得爽、跑得稳、上线快。
从 "原始社会" 到 "工程化"
-
阶段 1:原始时代(2010 年前)
你写一个网页:
<!DOCTYPE html> <html> <head> <script src="jquery.js"></script> <script src="utils.js"></script> <script src="app.js"></script> </head> <body> <!-- 页面内容 --> </body> </html>痛点:
- 所有变量都是全局的(
window.$、window.app),命名冲突家常便饭 - 引用的JS顺序不能错,
utils.js必须在app.js前面 - 要压缩代码?手动去网站粘贴压缩再粘回来
- 要兼容IE?手动加一堆
if判断 - 上线前要把所有HTML里的图片路径改成CDN?手动改
这时候你还不需要工程化,因为项目就几个页面、两个人开发。
- 所有变量都是全局的(
-
阶段 2:混乱时代(2013-2016 年)
项目变大了:
- 用了React/Vue,每个组件一个
.js文件 - 团队20个人同时开发
- 需要用到Sass写样式、ES6+语法、TypeScript
- 代码要压缩、要tree-shaking、要按需加载
你发现:浏览器根本看不懂这些!
- 浏览器不认识
.vue文件 - 浏览器不认识
import语法(当时) - 浏览器不认识
.scss文件 - 需要把ES6转成ES5兼容IE
于是你急需一个 "翻译官",把开发时写的 "现代代码" 翻译成浏览器能跑的 "古董代码"——这就是前端工程化的起点。
- 用了React/Vue,每个组件一个
-
阶段 3:工程化时代(现在)
你现在使用的工具链:
工具 解决什么问题 类比 Vite/Webpack 把 .vue、.jsx、.ts、.scss转成浏览器认识的HTML/CSS/JS翻译官 ESLint 代码上线前自动检查语法错误和风格问题 校对员 Prettier 自动格式化代码,团队统一风格 保洁阿姨 TypeScript 在写代码时提前发现类型错误 质检员 Jest/Vitest 自动测试你的代码是否跑得通 测试员 Git + CI/CD 多人协作、自动部署 流程管理 npm/yarn/pnpm 管理第三方库的版本和依赖 仓库管理员 这整套 "工具 + 流程 + 规范" 的体系,就是前端工程化。
三,前端工程化的四大核心目标
3.1 模块化(代码组织)
问题:几百个 JS 文件怎么组织?怎么避免命名冲突?怎么复用代码?
工程化方案:
- Node.js的CommonJS规范(
require/module.exports) - 浏览器的ES Module规范(
import/export) - 通过打包工具(Vite/Webpack)把零散的模块打包成几个文件
3.2 编译/转译(代码转换)
问题:浏览器不认识 TypeScript、Sass、JSX,怎么让它们跑起来?
工程化方案:
- TypeScript → JS:
tsc编译器 - Sass → CSS:
sass编译器 - JSX → JS:
babel编译器 - 这些编译器都是用Node.js写的,通过
fs模块读写文件
3.3 开发效率(开发体验)
问题:每次改代码都要手动刷新浏览器?手动打包?太慢了!
工程化方案:
- HMR(热模块替换):改代码后自动更新页面,保留状态
- Dev Server:本地起一个开发服务器,自动打开浏览器
- 文件监听:保存文件自动触发重新编译
- 这些功能底层都是Node.js的
fs.watch+http模块实现的
3.4 质量保障(代码可靠)
问题:团队 20 个人,怎么保证代码不坏?怎么保证风格统一?
工程化方案:
- ESLint:代码提交前自动检查
- Prettier:保存时自动格式化
- Git Hooks(husky):在
git commit前自动跑测试 - CI/CD:代码合并到主分支后自动构建、自动部署
3.5 一个"工程化"的完整工作流
把上面的串起来:
你写代码(.vue / .ts / .scss)
↓
ESLint 自动检查语法(Node脚本)
↓
Prettier 自动格式化(Node脚本)
↓
Vite 监听文件变化(fs.watch)
↓
Vite 把 .vue 转成 .js(用Node读取文件→转换字符串→写入内存)
↓
Dev Server 把编译后的代码发给浏览器(http模块)
↓
你修改代码 → Vite重新编译 → HMR更新页面(循环)
↓
git commit → husky触发(child_process)
↓
自动跑单元测试(Jest)
↓
推送代码 → CI服务器拉取 → 自动构建 → 部署上线
每一步都有 Node.js 的身影! 但它不是在跑后端业务,而是在跑各种工具脚本
四,NodeJs快速入门
4.1 执行NodeJs
安装 Nvm-DeskTop

然后随便新建个 Test.js 文件,内容举例
console.log('hello world')

4.2 fs 模块
FS两种API风格
同步 API
const fs = require('fs');
// 读文件(同步)
const content = fs.readFileSync('./src/app.js', 'utf-8');
// 写文件(同步)
fs.writeFileSync('./dist/app.js', '新内容', 'utf-8');
// 检查文件是否存在
const exists = fs.existsSync('./src/app.js');
// 删除文件
fs.unlinkSync('./temp.js');
工程化中同步是主力
- 构建过程需要按顺序执行:先读A,处理完,再写B
- 脚本是运行在开发环境,不需要考虑并发性能
- 同步代码更清晰,出错了堆栈更容易追踪
典型场景:Vite/Webpack 的构建流程、CLI 工具的文件操作
Promise 风格
const fs = require('fs').promises;
async function build() {
const content = await fs.readFile('./src/app.js', 'utf-8');
await fs.writeFile('./dist/app.js', content);
}
什么时候用?
- 需要并发处理多个文件(比如同时读取10个文件)
- 你在写一个需要高性能的工具(比如文件监听器)
注意:如果你的工具只是简单的顺序处理,用同步就够了,别为了 "异步高级" 而用异步。
回调风格
fs.readFile('./file.txt', 'utf-8', (err, data) => {
// 回调地狱,别用了!
});
工程化中几乎不用,除非你在维护 10 年前的旧代码。
常用的FS API
fs.readFileSync() —— 读文件
// 最常用:读取文本文件
const content = fs.readFileSync('./src/index.js', 'utf-8');
// 读取二进制文件(图片等)
const buffer = fs.readFileSync('./assets/logo.png');
工程化场景:读取源码文件、读取配置文件(package.json、vite.config.js)
fs.writeFileSync() —— 写文件
// 写入文本
fs.writeFileSync('./dist/index.js', 'console.log("hello")', 'utf-8');
// 创建目录时自动补全路径(需要先创建目录)
// 注意:writeFileSync不会自动创建不存在的目录!
工程化场景:输出构建产物到dist目录、生成manifest.json
fs.existsSync() —— 检查文件 / 目录是否存在
if (fs.existsSync('./node_modules')) {
console.log('依赖已安装');
}
// 注意:在Node.js官方文档中,existsSync被认为"过时",但工程化中依然广泛使用
// 替代方案:fs.accessSync(),但用法更繁琐,个人推荐existsSync
工程化场景:检查配置文件是否存在、判断是否要创建目录
fs.mkdirSync() —— 创建目录
// 创建单层目录
fs.mkdirSync('./dist');
// 递归创建多层目录(重要!)
fs.mkdirSync('./dist/js/components', { recursive: true });
工程化场景:构建前创建输出目录、创建临时文件夹
fs.readdirSync() —— 读取目录内容
// 读取目录下所有文件和文件夹
const files = fs.readdirSync('./src');
// 返回:['index.js', 'utils.js', 'components/']
// 读取并获取详细信息
const filesWithStats = fs.readdirSync('./src', { withFileTypes: true });
filesWithStats.forEach(item => {
if (item.isDirectory()) {
console.log('文件夹:', item.name);
} else {
console.log('文件:', item.name);
}
});
工程化场景:遍历源码目录、扫描所有.vue文件、查找所有测试文件
fs.statSync() —— 获取文件 / 目录信息
const stats = fs.statSync('./src/index.js');
console.log(stats.isFile()); // true
console.log(stats.isDirectory()); // false
console.log(stats.size); // 文件大小(字节)
console.log(stats.mtime); // 最后修改时间
工程化场景:判断是文件还是文件夹、文件监听时判断是否修改
fs.watch() —— 监听文件变化
// 监听整个目录
fs.watch('./src', (eventType, filename) => {
if (eventType === 'change') {
console.log(`${filename} 发生了变化,重新构建...`);
// 重新执行构建逻辑
build();
}
});
工程化场景:Vite/Webpack 的 HMR、nodemon自动重启、开发模式下的文件监听
注意要点
-
总是处理编码
// ❌ 错误:不指定编码会返回Buffer const data = fs.readFileSync('./file.txt'); console.log(data); // <Buffer 68 65 6c 6c 6f> // ✅ 正确:指定utf-8 const data = fs.readFileSync('./file.txt', 'utf-8'); console.log(data); // 'hello' -
总是先创建目录再写文件
const fs = require('fs'); const path = require('path'); function writeFileSafe(filePath, content) { const dir = path.dirname(filePath); // 如果目录不存在,递归创建 if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content, 'utf-8'); } // 使用:即使dist/js目录不存在,也会自动创建 writeFileSafe('./dist/js/app.js', 'console.log("hello")');
4.3 path模块
为什么需要Path模块?
-
错误的做法(硬编码路径分隔符)
// 这样写,在Windows上会炸! const filePath = 'src/components/App.js'; // Mac/Linux用 / // Windows上路径应该是:src\components\App.js // 手动拼接字符串 const filePath = __dirname + '/src/' + 'index.js'; // 在Windows上可能变成:C:\project/src/index.js(混用/和\)__dirname 内置变量(获取当前模块目录 - 绝对路径)
-
正确的做法
const path = require('path'); // ✅ 自动适配操作系统 const filePath = path.join('src', 'components', 'App.js'); // Mac/Linux: src/components/App.js // Windows: src\components\App.js
核心认知:Path 模块帮你屏蔽操作系统差异,让路径处理在任何系统上表现一致。
常用Path API
按使用频率从高到低排列:
-
path.join()—— 拼接路径最常用的 API,没有之一。用来拼接多个路径片段。
const path = require('path'); // 基础用法 const fullPath = path.join('src', 'components', 'App.js'); // 结果:'src/components/App.js' (Mac/Linux) // 结果:'src\components\App.js' (Windows) // 配合 __dirname 使用(最常用) const filePath = path.join(__dirname, 'src', 'index.js'); // 结果:'/Users/xxx/project/src/index.js' (Mac) // 结果:'C:\Users\xxx\project\src\index.js' (Windows) // 自动处理多余的斜杠 path.join('src/', '/components', 'App.js'); // 结果:'src/components/App.js' (会自动归一化)工程化场景:构建时拼接输入 / 输出路径
const fs = require('fs'); const path = require('path'); // 读取源码 const srcFile = path.join(process.cwd(), 'src', 'index.js'); const content = fs.readFileSync(srcFile, 'utf-8'); // 输出到dist const distFile = path.join(process.cwd(), 'dist', 'index.js'); fs.writeFileSync(distFile, content); -
path.resolve()—— 解析为绝对路径resolve和join经常被混淆,它们的区别非常关键:特性 path.join()path.resolve()作用 拼接路径片段 将路径解析为绝对路径 是否从根目录开始 ❌ 只拼接,不处理绝对路径 ✅ 总是返回绝对路径 处理 ..保留 ..解析 ..为上一级// 看区别: console.log(path.join('src', 'index.js')); // 输出:'src/index.js' (相对路径) console.log(path.resolve('src', 'index.js')); // 输出:'/Users/xxx/current/working/directory/src/index.js' (绝对路径) // 处理 .. 的区别: console.log(path.join('src/..', 'index.js')); // 输出:'src/../index.js' (保留了..) console.log(path.resolve('src/..', 'index.js')); // 输出:'/Users/xxx/current/working/directory/index.js' (解析了..)什么时候用哪个?
// 用 path.join:拼接相对路径 const relativePath = path.join('src', 'components', 'App.js'); // 用 path.resolve:获取绝对路径(最常见) const absolutePath = path.resolve('src/index.js'); // 相当于:path.join(process.cwd(), 'src/index.js') // 最推荐的写法:明确告诉别人你基于哪个目录 const filePath = path.resolve(__dirname, '../src/index.js');工程化场景:几乎所有的文件操作都用
path.resolve得到绝对路径后再操作// 不推荐:直接用相对路径(容易出错) const content = fs.readFileSync('./src/index.js'); // 推荐:解析为绝对路径再操作 const filePath = path.resolve(__dirname, 'src/index.js'); const content = fs.readFileSync(filePath); -
path.basename()—— 获取文件名const filePath = '/src/components/App.vue'; path.basename(filePath); // 输出:'App.vue' path.basename(filePath, '.vue'); // 输出:'App' (去掉扩展名) // 从完整路径中提取文件名(工程化常用) const files = fs.readdirSync('./src'); files.forEach(file => { const fileName = path.basename(file); const nameWithoutExt = path.basename(file, path.extname(file)); console.log(`完整文件名:${fileName},文件名:${nameWithoutExt}`); });工程化场景:处理文件列表时提取文件名、生成路由表
// 自动生成路由入口文件 const files = fs.readdirSync('./src/pages'); files.forEach(file => { const name = path.basename(file, '.vue'); // 生成 import Home from './pages/Home.vue' }); -
path.dirname()—— 获取目录路径const filePath = '/src/components/App.vue'; path.dirname(filePath); // 输出:'/src/components' // 配合 __dirname 获取当前文件所在目录 console.log(__dirname); // 当前文件目录 console.log(path.dirname(__dirname)); // 上一级目录工程化场景:从文件路径中提取目录、处理输出路径
function copyFile(sourcePath, targetDir) { const fileName = path.basename(sourcePath); const targetPath = path.join(targetDir, fileName); // 确保目标目录存在 fs.mkdirSync(targetDir, { recursive: true }); fs.copyFileSync(sourcePath, targetPath); } -
path.extname()—— 获取文件扩展名const filePath = 'src/components/App.vue'; path.extname(filePath); // 输出:'.vue' (注意包含点) // 工程化常用:根据扩展名决定如何处理 function processFile(filePath) { const ext = path.extname(filePath); switch(ext) { case '.js': case '.ts': return compileJavaScript(filePath); case '.vue': return compileVue(filePath); case '.scss': return compileSass(filePath); default: return copyFile(filePath); } } -
path.parse()—— 解析路径为对象const filePath = '/Users/xxx/project/src/components/App.vue'; const parsed = path.parse(filePath); console.log(parsed); // { // root: '/', // dir: '/Users/xxx/project/src/components', // base: 'App.vue', // ext: '.vue', // name: 'App' // } // 方便地提取各种信息 const { dir, base, name, ext } = path.parse(filePath);
"路径三剑客"
这三个变量在工程化中经常一起出现
-
__dirname—— 当前文件所在目录// 文件位置:/Users/xxx/project/scripts/build.js console.log(__dirname); // 输出:'/Users/xxx/project/scripts' // 永远不变,始终是当前JS文件的位置 -
process.cwd()—— 当前工作目录// 你在哪个目录执行 `node build.js` console.log(process.cwd()); // 输出:'/Users/xxx/project' (取决于你在哪个目录执行命令) // 可以变化!如果用户从不同目录启动,值不同 -
path.resolve()—— 基于当前工作目录解析// 基于 process.cwd() 解析 path.resolve('src/index.js'); // 等同于:path.join(process.cwd(), 'src/index.js')
区别
// ❌ 错误:用 __dirname 代替 process.cwd()
const config = require(path.join(__dirname, '..', 'vite.config.js'));
// 如果 scripts/build.js 位置变了,这个路径就错了
// ✅ 正确:用 process.cwd() 获取项目根目录
const config = require(path.resolve('vite.config.js'));
// 始终基于用户当前执行命令的目录
// ✅ 更严谨:从当前文件位置向上找
const config = require(path.resolve(__dirname, '../vite.config.js'));
// 假设 scripts 在项目根目录下,向上找一级
工程化中的黄金法则:
- 访问项目文件(源码、配置)→ 用
process.cwd() - 访问工具自身的文件(npm包里的资源)→ 用
__dirname
4.4 结合FS和Path写一个"文件遍历器"
这是工程化中最常见的任务,把所有知识串起来:
const fs = require('fs');
const path = require('path');
// 递归遍历目录,找所有 .vue 文件
function findVueFiles(dir, result = []) {
// 1. 读取目录
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
// 2. 如果是目录,递归遍历
findVueFiles(fullPath, result);
} else if (stats.isFile() && path.extname(item) === '.vue') {
// 3. 如果是 .vue 文件,记录
result.push(fullPath);
}
});
return result;
}
// 使用:从 src 目录开始查找
const srcDir = path.resolve('src');
const vueFiles = findVueFiles(srcDir);
console.log(`找到 ${vueFiles.length} 个 Vue 文件`);
vueFiles.forEach(file => {
const relativePath = path.relative(process.cwd(), file);
const fileName = path.basename(file);
console.log(`- ${relativePath} (${fileName})`);
});
"路径原则"
- 永远使用
path.join()或path.resolve(),别用字符串拼接 - 文件操作前先用
path.resolve()转成绝对路径 - 区分
__dirname(文件位置)和process.cwd()(执行位置)
4.5 http模块
Http模块是什么?
Node.js 的http模块是用来创建 HTTP 服务器或发起 HTTP 请求的核心模块。它让你能用 JavaScript 实现 HTTP 协议的全部功能。
const http = require('http');
核心能力:
- 做服务器:接收浏览器的请求,返回响应
- 做客户端:向其他服务器发起请求(类似浏览器里的
fetch)
Http模块的核心概念
请求(Request)和响应(Response)
HTTP 协议就是 "一问一答":
浏览器 ---------- 请求(Request) ----------> 服务器
浏览器 <--------- 响应(Response) ----------- 服务器
在 Http 模块中,这两个对象是核心:
req(请求对象):包含浏览器发送的所有信息(URL、方法、请求头、数据)res(响应对象):用来设置返回给浏览器的所有信息(状态码、响应头、数据)
创建服务器(最基础的用法)
const http = require('http');
// 创建服务器
const server = http.createServer((req, res) => {
// 这个回调函数会在每次有请求时执行
// 1. 设置响应头(状态码 + 内容类型)
res.writeHead(200, { 'Content-Type': 'text/plain' });
// 2. 返回响应内容
res.end('Hello World');
});
// 监听端口
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
运行后:浏览器访问http://localhost:3000,会看到 "Hello World"。
深入理解请求对象(req)
req包含了浏览器发送的所有信息:
const server = http.createServer((req, res) => {
// 1. 请求方法(GET、POST、PUT等)
console.log('方法:', req.method);
// 2. 请求URL(路径 + 查询参数)
console.log('URL:', req.url);
// 访问 http://localhost:3000/user?id=123
// 输出:'/user?id=123'
// 3. 请求头(浏览器信息、Cookie等)
console.log('请求头:', req.headers);
// 输出:{ host: 'localhost:3000', 'user-agent': '...' }
// 4. 请求体(POST/PUT时携带的数据)
let body = '';
req.on('data', chunk => {
body += chunk.toString(); // 接收数据片段
});
req.on('end', () => {
console.log('请求体:', body);
res.end('收到数据');
});
});
深入理解响应对象(res)
res用来控制返回给浏览器的内容:
const server = http.createServer((req, res) => {
// 1. 设置状态码和响应头(两种方式)
// 方式一:writeHead一次性设置
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
// 方式二:分开设置
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
// 2. 返回JSON数据
const data = JSON.stringify({ name: 'Node.js', version: '20' });
res.end(data);
// 3. 返回HTML
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello</h1>');
// 4. 返回文件(结合FS模块)
const fs = require('fs');
const content = fs.readFileSync('./index.html');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
});
处理不同的请求路径(路由)
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
// 解析URL,获取路径和查询参数
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
// 路由处理
if (pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>首页</h1>');
} else if (pathname === '/user') {
// 获取查询参数:/user?id=123
const userId = query.id;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: userId, name: '张三' }));
} else if (pathname === '/api/data' && req.method === 'POST') {
// 处理POST请求
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 }));
});
} else {
// 404
res.writeHead(404);
res.end('404 Not Found');
}
});
server.listen(3000);
解析请求体的多种格式
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
// 根据Content-Type解析不同格式
const contentType = req.headers['content-type'];
if (contentType === 'application/json') {
const jsonData = JSON.parse(body);
console.log('JSON数据:', jsonData);
} else if (contentType === 'application/x-www-form-urlencoded') {
// 表单数据:name=张三&age=25
const params = new URLSearchParams(body);
console.log('表单数据:', Object.fromEntries(params));
} else if (contentType === 'text/plain') {
console.log('纯文本:', body);
}
res.end('收到数据');
});
}
});
发送HTTP请求(作为客户端)
Http 模块也可以主动请求其他服务器:
const http = require('http');
// 方式1:使用 http.request
const options = {
hostname: 'api.example.com',
port: 80,
path: '/users',
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
};
const request = http.request(options, (response) => {
let data = '';
response.on('data', chunk => {
data += chunk.toString();
});
response.on('end', () => {
console.log('响应数据:', JSON.parse(data));
});
});
request.on('error', (error) => {
console.error('请求失败:', error);
});
request.end(); // 发送请求
简单版(使用http.get):
http.get('http://api.example.com/users', (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => console.log(JSON.parse(data)));
});
常见状态码和Content-Type
常用状态码
res.writeHead(200); // OK - 成功
res.writeHead(201); // Created - 创建成功
res.writeHead(301); // Moved Permanently - 永久重定向
res.writeHead(302); // Found - 临时重定向
res.writeHead(400); // Bad Request - 请求错误
res.writeHead(401); // Unauthorized - 未认证
res.writeHead(403); // Forbidden - 禁止访问
res.writeHead(404); // Not Found - 未找到
res.writeHead(500); // Internal Server Error - 服务器错误
常用 Content-Type
// 文本
'text/plain'
'text/html'
'text/css'
'text/javascript'
// 应用数据
'application/json'
'application/xml'
'application/x-www-form-urlencoded'
'application/octet-stream' // 二进制
// 图片
'image/jpeg'
'image/png'
'image/gif'
// 文件下载
'application/zip'
'application/pdf'
注意事项
- 总是处理错误
server.on('error', (error) => { if (error.code === 'EADDRINUSE') { console.error('端口被占用,请换一个端口'); } }); - 设置超时
server.timeout = 30000; // 30秒超时 - 限制请求体大小(防止攻击)
let body = ''; req.on('data', chunk => { body += chunk; if (body.length > 1e6) { // 1MB限制 res.writeHead(413); res.end('请求体太大'); req.destroy(); } }); - 使用
url.parse()解析查询参数const url = require('url'); const parsed = url.parse(req.url, true); console.log(parsed.query.id); // 获取查询参数
五,NodeJs模块化
5.1 什么是模块化?
一句话定义:模块化就是把一个大的程序拆分成独立、可复用的小文件,每个文件负责一个特定的功能。
没有模块化的时代(痛点)
<!-- 2010年前的写法 -->
<script src="jquery.js"></script>
<script src="utils.js"></script>
<script src="app.js"></script>
问题:
- 所有变量都是全局的,
utils.js里的formatDate会污染全局 - 脚本加载顺序必须正确,
app.js依赖utils.js,顺序错了就报错 - 无法复用:想在其他项目用
utils.js?只能复制粘贴
有了模块化之后
// utils.js - 导出功能
function formatDate(date) {
return date.toLocaleDateString();
}
module.exports = { formatDate };
// app.js - 导入使用
const { formatDate } = require('./utils.js');
console.log(formatDate(new Date()));
优势:
- 每个文件有独立作用域,不会污染全局
- 显式声明依赖关系(谁导入谁)
- 一个文件可以被多个地方复用
5.2 Node.js的两种模块系统
Node.js 同时支持两套模块规范,这是历史原因造成的,也是新手最容易混淆的地方。
| 特性 | CommonJS | ES Module |
|---|---|---|
| 标准 | Node.js原生 | ECMAScript官方标准 |
| 语法 | require() / module.exports | import / export |
| 文件扩展名 | .js (默认) | .mjs 或 .js (配置"type":"module") |
| 加载时机 | 运行时加载 | 编译时静态分析 |
| 是否异步 | 同步 | 异步 |
5.3 CommonJS
导出
-
方式 1:
module.exports(最常用,最推荐)你可以把
module.exports理解为一个空盒子,你把东西放进去,别人就能拿走。放函数
// 文件:math.js // 放一个函数进去 module.exports = function add(a, b) { return a + b; };放对象
// 文件:person.js // 放一个对象进去 module.exports = { name: '张三', age: 25, sayHello: function() { console.log('你好,我是' + this.name); } };放多个东西
// 文件:utils.js // 放多个东西进去 module.exports = { formatDate: function(date) { return date.toLocaleDateString(); }, generateId: function() { return Math.random().toString(36).substr(2, 9); } };关键点:
module.exports是一个容器,你把什么赋给它,别人就只能拿到什么。 -
方式 2:
exports(简写方式,有陷阱)exports是module.exports的 "快捷方式",你可以往里面添加属性。// 文件:math.js // 往 exports 里添加属性 exports.add = function(a, b) { return a + b; }; exports.subtract = function(a, b) { return a - b; }; // 最终效果等同于: // module.exports = { // add: function(a, b) { return a + b; }, // subtract: function(a, b) { return a - b; } // };注意要点:
// ❌ 错误写法:给 exports 重新赋值 exports = { add: function(a, b) { return a + b; } }; // 这样不行!exports 现在指向了一个新对象,但 module.exports 还是原来的空盒子 // ✅ 正确写法:修改 exports 的属性 exports.add = function(a, b) { return a + b; }; // 这样可以!因为 exports 和 module.exports 指向同一个对象 // ✅ 正确写法:直接改 module.exports module.exports = { add: function(a, b) { return a + b; } }; // 这样可以!直接替换了盒子里的东西记住:
exports只是module.exports的 "别名",最终真正导出的永远是module.exports。
导入
require()就是用来拿别人导出的东西。
-
基础用法
// 文件:app.js // 导入 math.js 导出的内容 const math = require('./math.js'); console.log(math.add(1, 2)); // 3 console.log(math.subtract(5, 3)); // 2 -
拿到不同的导出类型
// math.js - 导出一个函数 module.exports = function add(a, b) { return a + b; }; // app.js const add = require('./math.js'); console.log(add(1, 2)); // 3(直接调用函数)// person.js - 导出一个对象 module.exports = { name: '张三', age: 25 }; // app.js const person = require('./person.js'); console.log(person.name); // '张三' console.log(person.age); // 25// utils.js - 导出多个函数 module.exports = { formatDate: function(date) { /* ... */ }, generateId: function() { /* ... */ } }; // app.js const utils = require('./utils.js'); // 方式1:直接用 utils.formatDate(new Date()); // 方式2:解构赋值(更方便) const { formatDate, generateId } = require('./utils.js'); formatDate(new Date());
导入不同类型的内容
// 1. 导入核心模块(Node.js自带的)
const fs = require('fs');
const path = require('path');
// 2. 导入第三方包(npm安装的)
const lodash = require('lodash');
const express = require('express');
// 3. 导入自己写的文件(注意路径)
const utils = require('./utils.js'); // 同一目录
const config = require('../config.js'); // 上级目录
const helper = require('/absolute/path/helper.js'); // 绝对路径
// 4. 导入JSON文件(自动解析为对象)
const config = require('./config.json');
console.log(config.port); // 3000
// 5. 导入目录(自动找 index.js)
const routes = require('./routes');
// 等价于:require('./routes/index.js')
缓存机制
同一个文件多次导入,只会执行一次,返回同一个对象。
// counter.js
console.log('counter.js 被执行了');
let count = 0;
module.exports = {
increment: function() {
count++;
return count;
}
};
// app.js
const counter1 = require('./counter.js');
const counter2 = require('./counter.js');
console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 2(注意是2,不是1)
console.log(counter1 === counter2); // true(同一个对象)
输出:
counter.js 被执行了 (只打印一次!)
1
2
true
5.4 ES Module
ES Module 是 JavaScript 语言自带的模块化规范,用import和export关键字实现模块导入导出。
对比 CommonJS(Node.js 自创的),ES Module 是官方标准,未来所有 JavaScript 环境都会统一用这个。
如何启用 ES Module?
Node.js 默认使用 CommonJS,想用 ES Module 有三种方式:
-
文件扩展名用
.mjs# 文件命名 utils.mjs # ES Module app.mjs # ES Module # 运行 node app.mjs -
在
package.json中配置{ "type": "module" // 所有 .js 文件都按ES Module处理 } -
同时支持两种(不推荐)
{ "type": "module", "exports": { "import": "./index.mjs", // import时使用 "require": "./index.cjs" // require时使用 } }
导出
ES Module 有两种导出方式:命名导出和默认导出。
-
命名导出(Named Export)
可以导出多个,导入时需要用相同的名字。// utils.js // 方式1:分别导出 export const name = '工具库'; export function formatDate(date) { return date.toLocaleDateString(); } export const add = (a, b) => a + b; // 方式2:统一导出(先定义,最后一起导出) const name = '工具库'; function formatDate(date) { return date.toLocaleDateString(); } const add = (a, b) => a + b; export { name, formatDate, add }; // 方式3:导出时重命名(对外暴露的名字变了) const name = '工具库'; export { name as libName }; // 导入时只能使用 libName,不能使用 name -
默认导出(Default Export)
每个文件只能有一个默认导出,导入时可以任意取名。
// utils.js // 方式1:导出函数 export default function add(a, b) { return a + b; } // 方式2:导出对象 export default { name: '工具库', formatDate: function(date) { /* ... */ }, add: function(a, b) { return a + b; } }; // 方式3:导出类 export default class Calculator { add(a, b) { return a + b; } }
命名导出 vs 默认导出
| 特性 | 命名导出 | 默认导出 |
|---|---|---|
| 每个文件数量 | 多个 | 最多1个 |
| 导入时的名字 | 必须和导出时相同 | 可以任意取名 |
| 语法 | export const name = ... | export default ... |
| 导入语法 | import { name } from ... | import anyName from ... |
| 使用场景 | 工具函数库 | 主要功能/组件 |
导入
-
导入命名导出
// app.js // 导入单个 import { formatDate } from './utils.js'; // 导入多个 import { formatDate, add } from './utils.js'; // 导入时重命名(避免命名冲突) import { formatDate as dateFormat } from './utils.js'; // 导入全部(不推荐,会失去tree-shaking优化) import * as utils from './utils.js'; utils.formatDate(); utils.add(1, 2); -
导入默认导出
// app.js // 默认导出可以随便取名 import add from './utils.js'; import myAdd from './utils.js'; // 换个名字也行 import anything from './utils.js'; // 完全不同的名字也行 // 调用 console.log(add(1, 2)); // 3 console.log(myAdd(1, 2)); // 3 console.log(anything(1, 2)); // 3 -
混合导入(默认 + 命名)
// utils.js export default function add(a, b) { return a + b; } export const version = '1.0.0'; export const name = '工具库'; // app.js // 默认导入放前面,命名导入放后面 import add, { version, name } from './utils.js'; console.log(add(1, 2)); // 3 console.log(version); // '1.0.0' console.log(name); // '工具库'
ES Module的独特特性
-
静态加载(编译时)
ES Module 的导入导出在代码执行前就已经确定,所以必须写在顶层。
// ✅ 正确:import必须写在顶层 import { add } from './math.js'; // ❌ 错误:不能写在条件语句里 if (true) { import { add } from './math.js'; // 报错! } // ❌ 错误:不能写函数里 function test() { import { add } from './math.js'; // 报错! }好处:可以进行静态分析,实现 tree-shaking(打包时去掉没用到的代码)。
-
导入是只读的
// utils.js export let count = 0; export function increment() { count++; } // app.js import { count, increment } from './utils.js'; console.log(count); // 0 increment(); console.log(count); // 1 // ❌ 不能直接修改导入的变量 count = 10; // 报错!TypeError -
动态导入(运行时加载)
当需要按需加载时,可以使用
import()函数。// app.js // 动态导入返回Promise const mathModule = await import('./math.js'); console.log(mathModule.add(1, 2)); // 3 // 条件加载 if (condition) { const module = await import('./moduleA.js'); } else { const module = await import('./moduleB.js'); } // 路由懒加载(Vue/React中常见) const Home = () => import('./pages/Home.vue');
ES Module vs CommonJS 详细对比
| 特性 | CommonJS | ES Module |
|---|---|---|
| 标准 | Node.js私有 | JavaScript官方 |
| 语法 | require() / module.exports | import / export |
| 加载时机 | 运行时加载 | 编译时加载(静态) |
| 是否异步 | 同步 | 异步 |
| 是否支持tree-shaking | ❌ | ✅ |
| 导入是否只读 | 否(可修改) | 是(只读) |
| 在浏览器使用 | ❌ | ✅ |
__dirname / __filename | ✅ 有 | ❌ 没有 |
| 支持动态导入 | ❌ | ✅(import()) |
ES Module中的路径处理
在 ES Module 中,__dirname和__filename不可用,需要自己构造:
// CommonJS(可以直接用)
console.log(__dirname); // /Users/xxx/project
console.log(__filename); // /Users/xxx/project/app.js
// ES Module(需要构造)
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__dirname); // /Users/xxx/project
console.log(__filename); // /Users/xxx/project/app.js
import.meta.url 是 ES Module 中获取当前文件路径的标准方式。
5.5 CommonJS vs ES Module
-
加载时机不同
// CommonJS - 运行时加载 const fs = require('fs'); // 代码执行到这里才会加载 console.log('这行先执行'); // ES Module - 编译时加载 import { readFile } from 'fs'; // 代码执行前就已经加载完成 console.log('这行后执行'); // 实际上import会提升到顶部 -
循环依赖的处理
// a.js const b = require('./b.js'); console.log('a.js 加载完成'); module.exports = { name: 'A' }; // b.js const a = require('./a.js'); console.log('b.js 加载完成'); module.exports = { name: 'B' }; // 执行顺序: // 1. 加载a.js,发现require('b'),暂停a的执行 // 2. 加载b.js,发现require('a'),但a正在加载中,返回不完整的a // 3. b继续执行,导出 // 4. a继续执行,导出 // 结果:b中获取的a是不完整的!ES Module 的循环依赖(有保护):
// a.mjs import { b } from './b.mjs'; export const a = 'A'; // b.mjs import { a } from './a.mjs'; export const b = 'B'; // 不会报错,但a在b中可能是undefined(直到a完全加载) -
this 的指向不同
// CommonJS console.log(this); // 指向当前模块的exports对象 // ES Module console.log(this); // undefined -
文件扩展名规则
// CommonJS (.js默认) require('./file'); // 自动找 file.js require('./file.json'); // 自动解析JSON // ES Module (.mjs或配置后.js) import './file.mjs'; // 必须写完整扩展名 // 如果package.json中设置 "type": "module",.js文件也按ES Module处理
5.6 Node.js中两种模块的互操作
这是实际项目中最常遇到的坑。
1. ES Module 导入 CommonJS
// commonjs-module.js
module.exports = { name: 'CommonJS' };
// es-module.mjs
import cjs from './commonjs-module.js';
console.log(cjs.name); // 'CommonJS'
// 导入整个对象
import * as cjs from './commonjs-module.js';
console.log(cjs.default.name); // 'CommonJS'
2. CommonJS 导入 ES Module
// es-module.mjs
export default { name: 'ES Module' };
export const version = '1.0';
// commonjs.js
const es = require('./es-module.mjs');
// ❌ 报错!CommonJS不能直接require ES Module
// 只能用动态import
async function load() {
const es = await import('./es-module.mjs');
console.log(es.default.name); // 'ES Module'
console.log(es.version); // '1.0'
}
3. package.json中的配置
{
"type": "commonjs", // 默认,.js文件按CommonJS处理
// 或
"type": "module" // .js文件按ES Module处理
}
最佳实践:
- 新项目用ES Module(
"type": "module") - 旧项目保持CommonJS
- 工具类脚本用
.mjs扩展名明确标识
5.7 模块解析路径的规则
当你说require('xxx')或import 'xxx'时,Node.js 按以下顺序查找:
1. 核心模块
require('fs'); // Node.js内置模块,直接返回
2. 相对路径模块
require('./utils.js'); // 相对当前文件
require('../lib/index.js');
3. 绝对路径模块
require('/Users/xxx/project/utils.js');
4. 第三方模块(从node_modules查找)
require('lodash');
// Node.js会从当前目录开始,逐级向上查找node_modules
// /project/node_modules/lodash
// /node_modules/lodash
// /Users/node_modules/lodash
5. 目录自动查找index
require('./routes');
// 依次查找:
// ./routes.js
// ./routes.json
// ./routes/index.js
// ./routes/index.json
5.8 常见问题和陷阱
-
循环依赖导致的 undefined
// a.js const b = require('./b.js'); console.log(b); // 可能是不完整的 // 解决方案:延迟require module.exports = { run: () => { const b = require('./b.js'); // 使用时再加载 } }; -
导出对象的修改
// utils.js exports.count = 0; exports.increment = function() { this.count++; }; // app.js const utils = require('./utils.js'); utils.increment(); console.log(utils.count); // 1 // 共享状态!多个地方修改会影响彼此 -
__dirname 和 __filename 在 ES Module 中不可用
// CommonJS console.log(__dirname); // 当前文件目录 console.log(__filename); // 当前文件路径 // ES Module import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);