第8章 8.3 Node.js 基础(与浏览器 JS 对比)
「上一章我们学了泛型和高级类型,感觉 TypeScript 越来越像一门正经的编程语言了。但你有没有想过——JavaScript 还能在浏览器外面跑?」
没错,Node.js 就是让 JS 脱离浏览器的「任意门」。这一章我们来解决一个实际问题:浏览器 JS 能做的事很有限,不能读写文件、不能操作文件夹、不能当服务器。但你肯定见过很多工具(比如 Vue CLI、Webpack)说自己是「Node.js 程序」——这些是怎么做到的?
学完这章,你就能写出第一个跑在「命令行里的 JavaScript 程序」,而不是只能跑在网页上。
🎯 开场 3 分钟:为什么要学这个?
先说个真实场景。你肯定用过 Vue CLI(npm install -g @vue/cli)或者 Create React App,这些工具装完就能在命令行里跑 vue create my-app。但你有没有想过:
- 为什么它们能操作我的文件夹? 浏览器 JS 绝对做不到这件事。
-\n\n
\n\n
\n\n 为什么它们能安装 npm 包? 这背后的逻辑是什么? - 为什么
node -v能返回版本号,但浏览器里根本没有node这个东西?
Node.js 就是答案。 它是一台用 C++ 写的「JavaScript 虚拟机」,让 JS 获得了:
- 读写文件的能力(fs 模块)
- 操作文件夹的能力(path 模块)
- 充当服务器、接受网络请求的能力(http 模块)
简单说:浏览器 JS 是「网页里的工具人」,Node.js 是「电脑里的工具人」。 学会它,你就能写出命令行工具、脚本、自动化脚本、服务器……
🧱 基础 25 分钟:核心概念
第一个关键概念:Node.js 跟浏览器 JS 的区别
先来一个生活类比:
浏览器 JS 像是「外卖小哥」,只能在限定的范围(网页)里活动,送完一单就下班。
Node.js JS 像是「快递员」,能在整个城市(电脑)里跑来跑去,收件、派件、仓库管理都能干。
代码对比最清楚:
// 浏览器 JS —— 只能操作网页
document.querySelector('.btn').addEventListener('click', () => {
alert('点我了!');
});
// Node.js JS —— 能操作电脑
const fs = require('fs'); // 引入文件模块
fs.writeFileSync('note.txt', 'Hello Node.js'); // 写入一个文件
console.log('文件写好了!');
上面两段代码看起来差不多,但:
- 第一段只能在浏览器里跑
- 第二段只能在 Node.js 环境里跑
第二个关键概念:全局对象不一样
学 JS 的时候你肯定知道 console、setTimeout、Math、JSON——这些在浏览器和 Node.js 里都有,但有一些是各自的「私货」:
浏览器独有的:
- document —— 操作网页 DOM
- window —— 浏览器窗口对象
- location —— 网址信息
Node.js 独有的:
- process —— 进程信息,比如 process.env 能拿到环境变量
- __dirname —— 当前文件所在的文件夹路径
- __filename —— 当前文件的完整路径
// Node.js 独有的全局对象
console.log('当前文件夹:', __dirname);
console.log('当前文件:', __filename);
console.log('Node 版本:', process.version);
console.log('系统平台:', process.platform);
输出大概是:
当前文件夹: /Users/xiaoming/projects
当前文件: /Users/xiaoming/projects/test.js
Node 版本: v20.10.0
系统平台: darwin
第三个关键概念:模块系统——require 和 module
浏览器 JS 传统上是「一个 HTML 引入一个 JS」,所有变量都挂在一个全局作用域里。Node.js 引入了模块化的概念,每个文件是一个模块。
生活类比:
就像一个公司的不同部门,每个部门有自己的文件夹(模块),需要协作时通过「跨部门沟通」(require)来交流,而不是所有人都挤在一个大办公室里。
新建两个文件:
greet.js(模块文件):
// 导出函数
module.exports = function(name) {
return `你好,${name}!欢迎学习 Node.js!`;
};
main.js(主文件):
// 引入模块
const greet = require('./greet');
console.log(greet('小明'));
console.log(greet('小红'));
运行 node main.js,输出:
你好,小明!欢迎学习 Node.js!
你好,小红!欢迎学习 Node.js!
这就是 Node.js 的模块化——require 就像「点外卖」,module.exports 就像「接单」。
第四个关键概念:fs 模块——读写文件
这是 Node.js 最核心的能力之一。fs 是 "file system" 的缩写,让你读写文件、创建文件夹、删除文件……
读文件:
const fs = require('fs');
// 同步读取(等文件读完才继续往下走)
const content = fs.readFileSync('data.txt', 'utf-8');
console.log('文件内容:', content);
写文件:
const fs = require('fs');
// 写内容到文件(如果文件不存在会新建,存在会覆盖)
fs.writeFileSync('output.txt', '这是我用 Node.js 写的第一篇文章!', 'utf-8');
console.log('写入完成!');
检查文件/文件夹存不存在:
const fs = require('fs');
if (fs.existsSync('output.txt')) {
console.log('文件存在!');
const data = fs.readFileSync('output.txt', 'utf-8');
console.log('内容是:', data);
} else {
console.log('文件不存在,先创建一个吧');
fs.writeFileSync('output.txt', '新文件', 'utf-8');
}
第五个关键概念:path 模块——处理路径
读写文件经常要拼路径,但 Windows 和 Mac 的路径格式不一样(\ vs /),path 模块帮你统一处理。
const path = require('path');
// 拼接路径(自动适配不同系统)
const fullPath = path.join(__dirname, 'folder', 'file.txt');
console.log('完整路径:', fullPath);
// 获取文件扩展名
const ext = path.extname('photo.jpg');
console.log('扩展名是:', ext); // .jpg
// 获取文件名(不含扩展名)
const name = path.basename('photo.jpg', '.jpg');
console.log('文件名是:', name); // photo
第六个关键概念:process.argv——接收命令行参数
想让你的程序运行时接收用户输入的参数?用 process.argv。
// 保存为 args.js,运行:node args.js apple banana 100
console.log('所有参数:', process.argv);
console.log('第一个参数(Node 路径):', process.argv[0]);
console.log('第二个参数(脚本路径):', process.argv[1]);
console.log('用户传的参数们:', process.argv.slice(2));
运行 node args.js apple banana 100:
所有参数: ['/usr/local/bin/node', '/Users/xiaoming/args.js', 'apple', 'banana', '100']
第一个参数(Node 路径): /usr/local/bin/node
第二个参数(脚本路径): /Users/xiaoming/args.js
用户传的参数们: [ 'apple', 'banana', '100' ]
所以 process.argv.slice(2) 就是用户传的真实参数。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):Node.js 版「记事本」
需求: 让用户通过命令行写笔记,保存到文件里。
// 保存为 notes.js
// 运行:node notes.js "今天学了 Node.js,真香"
const fs = require('fs');
const path = require('path');
// 从命令行获取笔记内容
const noteContent = process.argv[2];
if (!noteContent) {
console.log('用法:node notes.js "你的笔记内容"');
process.exit(1);
}
// 文件名用日期命名,方便整理
const today = new Date();
const dateStr = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}`;
const fileName = `note_${dateStr}.txt`;
const filePath = path.join(__dirname, fileName);
// 追加模式(不覆盖之前的内容)
fs.appendFileSync(filePath, `[${today.toLocaleTimeString()}] ${noteContent}\n`, 'utf-8');
console.log(`✅ 笔记已保存到 ${fileName}`);
console.log(`📖 当前笔记内容:`);
// 读取并显示
const allNotes = fs.readFileSync(filePath, 'utf-8');
console.log(allNotes);
预期输出(首次运行):
✅ 笔记已保存到 note_2024-1-15.txt
📖 当前笔记内容:
[上午 10:30:00] 今天学了 Node.js,真香
解释: 这段代码展示了 Node.js 最核心的几个能力——接收命令行参数、读写文件、用日期生成文件名。
项目 2(15 分钟):读取 CSV 数据并统计
需求: 有一个 students.csv 文件,里面有学生姓名和分数,统计平均分、最高分、最低分。
先创建测试数据 students.csv:
name,score
小明,85
小红,92
小刚,78
小丽,88
小强,95
然后写分析脚本:
// 保存为 analyze.js
// 运行:node analyze.js
const fs = require('fs');
const path = require('path');
// 读取 CSV 文件
const csvPath = path.join(__dirname, 'students.csv');
const csvContent = fs.readFileSync(csvPath, 'utf-8');
// 解析 CSV(去掉第一行表头)
const lines = csvContent.trim().split('\n');
const header = lines[0]; // "name,score"
const dataLines = lines.slice(1);
// 解析每一行,提取姓名和分数
const students = dataLines.map(line => {
const [name, score] = line.split(',');
return { name, score: parseInt(score, 10) };
});
// 统计
const scores = students.map(s => s.score);
const average = scores.reduce((a, b) => a + b, 0) / scores.length;
const max = Math.max(...scores);
const min = Math.min(...scores);
// 输出结果
console.log('=== 学生成绩统计 ===');
console.log(`📊 总人数:${students.length} 人`);
console.log(`📈 平均分:${average.toFixed(1)} 分`);
console.log(`🏆 最高分:${max} 分(${students.find(s => s.score === max).name})`);
console.log(`📉 最低分:${min} 分(${students.find(s => s.score === min).name})`);
// 把统计结果也写入文件
const report = `
=== 学生成绩统计 ===
统计时间:${new Date().toLocaleString()}
总人数:${students.length} 人
平均分:${average.toFixed(1)} 分
最高分:${max} 分
最低分:${min} 分
`;
fs.writeFileSync('report.txt', report, 'utf-8');
console.log('\n📁 统计报告已保存到 report.txt');
预期输出:
=== 学生成绩统计 ===
📊 总人数:5 人
📈 平均分:87.6 分
🏆 最高分:95 分(小强)
📉 最低分:78 分(小刚)
📁 统计报告已保存到 report.txt
解释: 这段代码展示了 Node.js 处理真实数据的流程——读取文件 → 解析数据 → 计算统计 → 写回文件。
项目 3(15 分钟):命令行「待办清单」小工具
需求: 一个可以在命令行里添加待办、查看列表、标记完成的小工具,数据存在 JSON 文件里。
// 保存为 todo.js
// 支持的命令:
// node todo.js add "买牛奶"
// node todo.js list
// node todo.js done 1
// node todo.js del 2
const fs = require('fs');
const path = require('path');
const todoFile = path.join(__dirname, 'todos.json');
// 初始化:如果文件不存在,创建一个空列表
if (!fs.existsSync(todoFile)) {
fs.writeFileSync(todoFile, JSON.stringify([], null, 2), 'utf-8');
}
// 读取待办列表
function loadTodos() {
const data = fs.readFileSync(todoFile, 'utf-8');
return JSON.parse(data);
}
// 保存待办列表
function saveTodos(todos) {
fs.writeFileSync(todoFile, JSON.stringify(todos, null, 2), 'utf-8');
}
// 打印分隔线
function printLine() {
console.log('─'.repeat(30));
}
// 主逻辑
const command = process.argv[2];
const arg = process.argv[3];
switch (command) {
case 'add': {
// 添加待办
if (!arg) {
console.log('用法:node todo.js add "待办内容"');
break;
}
const todos = loadTodos();
const newTodo = {
id: Date.now(),
content: arg,
done: false,
createdAt: new Date().toLocaleString()
};
todos.push(newTodo);
saveTodos(todos);
console.log(`✅ 已添加:「${arg}」`);
break;
}
case 'list': {
// 查看列表
const todos = loadTodos();
printLine();
console.log('📋 我的待办清单');
printLine();
if (todos.length === 0) {
console.log('(空的,快去添加吧)');
} else {
todos.forEach((todo, index) => {
const status = todo.done ? '✅' : '⬜';
const content = todo.done ? `(已完成)${todo.content}` : todo.content;
console.log(`${index + 1}. ${status} ${content}`);
});
}
printLine();
break;
}
case 'done': {
// 标记完成
const index = parseInt(arg, 10) - 1;
const todos = loadTodos();
if (index < 0 || index >= todos.length) {
console.log('❌ 无效的序号');
break;
}
todos[index].done = true;
saveTodos(todos);
console.log(`✅ 已标记完成:「${todos[index].content}」`);
break;
}
case 'del': {
// 删除
const index = parseInt(arg, 10) - 1;
const todos = loadTodos();
if (index < 0 || index >= todos.length) {
console.log('❌ 无效的序号');
break;
}
const deleted = todos.splice(index, 1)[0];
saveTodos(todos);
console.log(`🗑️ 已删除:「${deleted.content}」`);
break;
}
default:
console.log('用法:');
console.log(' node todo.js add "内容" - 添加待办');
console.log(' node todo.js list - 查看列表');
console.log(' node todo.js done 序号 - 标记完成');
console.log(' node todo.js del 序号 - 删除');
}
预期输出:
$ node todo.js add "买牛奶"
✅ 已添加:「买牛奶」
$ node todo.js add "学 Node.js"
✅ 已添加:「学 Node.js」
$ node todo.js list
────────────────────────────
📋 我的待办清单
────────────────────────────
1. ⬜ 买牛奶
2. ⬜ 学 Node.js
────────────────────────────
$ node todo.js done 1
✅ 已标记完成:「买牛奶」
$ node todo.js list
────────────────────────────
📋 我的待办清单
────────────────────────────
1. ✅ (已完成)买牛奶
2. ⬜ 学 Node.js
────────────────────────────
解释: 这个项目组合了前面学的所有知识——文件读写、JSON 解析、命令行参数、数组操作。而且它有真实用途,你可以真的用它来管理自己的日常任务。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:路径拼接别用字符串拼接,用 path.join
// ❌ 错误方式:不同系统可能出问题
const path1 = __dirname + '/folder/' + 'file.txt';
// ✅ 正确方式:path.join 自动处理路径分隔符
const path2 = path.join(__dirname, 'folder', 'file.txt');
Mac/Linux 用 /,Windows 用 \,直接拼字符串在别的系统上会报错。
坑 2:fs.writeFileSync 会覆盖文件
// ❌ 错误:每次都覆盖之前的内容
fs.writeFileSync('log.txt', '新内容', 'utf-8');
// ✅ 正确:追加模式
fs.appendFileSync('log.txt', '新内容\n', 'utf-8');
如果想同时兼顾追加和覆盖,可以加个标志位。
坑 3:process.argv 拿到的都是字符串
// ❌ 错误:直接当数字用
const num = process.argv[2];
console.log(num + 1); // "101" 而不是 11!
// ✅ 正确:先转换类型
const num = parseInt(process.argv[2], 10);
console.log(num + 1); // 11
坑 4:JSON.parse 报错会导致整个程序崩溃
// ❌ 错误:文件损坏时程序直接崩溃
const data = JSON.parse(fs.readFileSync('data.json', 'utf-8'));
// ✅ 正确:用 try-catch 捕获错误
let data = {};
try {
data = JSON.parse(fs.readFileSync('data.json', 'utf-8'));
} catch (e) {
console.log('文件解析失败,使用默认空对象');
}
坑 5:Node.js 是单线程的,不要用它做 CPU 密集型任务
Node.js 擅长 I/O 操作(读写文件、网络请求),但不擅长大规模计算。如果你要处理一个大文件做复杂计算,可能会卡住整个进程。
调试技巧:用 console.log 配合 JSON.stringify
// 打印复杂对象时,加格式化的 JSON 字符串更容易看
const obj = { name: '小明', scores: [85, 92, 78] };
console.log('对象内容:', JSON.stringify(obj, null, 2));
// 输出:
// 对象内容: {
// "name": "小明",
// "scores": [85, 92, 78]
// }
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):改日期格式
- 输入:修改项目 1 的 notes.js,让文件名包含小时分钟秒
- 预期输出:文件名变成 note_2024-1-15_10-30-00.txt 这种格式
- 提示:参考 toLocaleTimeString() 的用法,或者用 getHours()、getMinutes() 手动拼接
练习 2(2 分钟):增加判断
- 输入:在项目 1 的 notes.js 里加一个判断,如果笔记内容少于 5 个字,提示「内容太短了」
- 预期输出:输入 node notes.js "你好" 时输出提示语
- 提示:用 if (noteContent.length < 5) 判断
练习 3(3 分钟):处理新数据
- 输入:创建一个新的 CSV 文件 grades.csv,内容是:name,grade\n张三,88\n李四,92\n王五,76,然后改写 analyze.js 来分析它
- 预期输出:能正确统计平均分、最高分、最低分
- 提示:只需要改文件名引用,把 students.csv 改成 grades.csv
练习 4(5 分钟):串两个项目
- 输入:把项目 2(CSV 分析)的统计结果,自动添加到项目 1(笔记)的文件里
- 预期输出:运行后既有统计报告,又有笔记记录
- 提示:用 fs.appendFileSync 把统计报告追加到笔记文件
练习 5(5 分钟):分析报错
- 输入:运行下面的代码会出现什么错误?
const fs = require('fs');
const content = fs.readFileSync('not_exist.txt', 'utf-8');
console.log(content);
- 预期输出:应该看到什么错误信息?如何修复?
- 提示:
existsSync可以先检查文件存不存在
作业题(30 分钟 - 2 小时)
作业:做一个「个人支出记录工具」
-
需求描述: 用命令行管理你的日常支出,每次消费后记一笔,月底可以统计花了多少钱。
-
功能点:
1.node expense.js add "午饭" 25- 记录一笔支出
2.node expense.js list- 查看所有支出
3.node expense.js total- 统计总支出
4.node expense.js month 2024-1- 查看某月支出 -
加分项:
1. 用filter按月份筛选支出
2. 支持删除某条记录(node expense.js del 序号) -
验收标准:
- 能跑起来,不报错
- add 能保存数据到 JSON 文件
- list 能显示所有支出和日期
-
total 能正确计算总和
-
提交方式: 评论区贴代码或 GitHub 链接
📚 总结 + 资源
这一章学了 3 个核心点:
1. Node.js 是运行在浏览器外的 JavaScript 运行环境,有 __dirname、process 等浏览器没有的全局对象
2. require 和 module.exports 构成了 Node.js 的模块系统
3. fs 和 path 模块让 JS 有了读写文件、操作路径的能力
延伸学习资源:
- Node.js 官方文档(英文,中文文档在底部链接)
- 《Node.js 实战》—— 进阶必读,讲得很细
- npm 官方文档—— 学完这章再看下一章「npm 与 yarn 包管理」正好
互动钩子:
「你有没有想过自己写一个命令行工具?用来干啥?比如自动备份文件、自动整理下载文件夹……评论区聊聊,下一章我们学完 npm 就可以开始搞大项目了!」
「下一章我们要学 npm 与 yarn 包管理——你现在用的 require('fs') 其实是 Node.js 内置的模块,下一章我们会学怎么从 npm 市场上安装别人写的模块,做真正的大项目!」

评论(0)