第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\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 的时候你肯定知道 consolesetTimeoutMathJSON——这些在浏览器和 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 运行环境,有 __dirnameprocess 等浏览器没有的全局对象
2. requiremodule.exports 构成了 Node.js 的模块系统
3. fspath 模块让 JS 有了读写文件、操作路径的能力

延伸学习资源:
- Node.js 官方文档(英文,中文文档在底部链接)
- 《Node.js 实战》—— 进阶必读,讲得很细
- npm 官方文档—— 学完这章再看下一章「npm 与 yarn 包管理」正好

互动钩子:

「你有没有想过自己写一个命令行工具?用来干啥?比如自动备份文件、自动整理下载文件夹……评论区聊聊,下一章我们学完 npm 就可以开始搞大项目了!」


「下一章我们要学 npm 与 yarn 包管理——你现在用的 require('fs') 其实是 Node.js 内置的模块,下一章我们会学怎么从 npm 市场上安装别人写的模块,做真正的大项目!」

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。