Node.js入门必看:一篇搞懂基础 – wiki基地


Node.js 入门必看:一篇搞懂基础

欢迎来到 Node.js 的世界!如果你是一名 Web 开发者,或者对后端开发、构建高性能应用感兴趣,那么 Node.js 绝对是一个值得深入学习的技术。它让 JavaScript 这门原本运行在浏览器前端的语言,也具备了在服务器端运行的能力,并且凭借其独特的优势,成为了现代后端开发、构建 API、命令行工具乃至桌面应用的热门选择。

这篇文章将作为你的 Node.js 入门指南,带你从零开始,全面理解 Node.js 的核心概念、环境搭建、基本模块使用、包管理以及最重要的异步编程模型。读完这篇文章,你将能够掌握 Node.js 的基础,并为后续更深入的学习打下坚实的基础。

大纲:

  1. 什么是 Node.js?它为什么如此受欢迎?
  2. Node.js 的核心概念:V8 引擎、非阻塞 I/O 与事件循环
    • V8 引擎:JavaScript 的心脏
    • 非阻塞 I/O 与单线程模型
    • 事件循环 (Event Loop) 深度解析
  3. 搭建你的 Node.js 开发环境
    • 下载与安装 Node.js
    • 验证安装
    • Node 版本管理工具 (NVM)
  4. 运行你的第一个 Node.js 程序
    • 使用 Node.js REPL
    • 执行 JavaScript 文件
    • “Hello, World!” 示例
  5. Node.js 模块系统:requiremodule.exports
    • 什么是模块?
    • 引入模块:require()
    • 导出模块:module.exports / exports
  6. Node.js 内建核心模块
    • fs 模块:文件系统操作 (同步与异步)
    • http 模块:构建 Web 服务器
    • path 模块:处理文件路径
    • 其他常用核心模块
  7. 掌握 NPM:Node.js 的包管理器
    • NPM 是什么?
    • package.json 文件:项目的心脏
    • 安装依赖包:npm install
    • 全局安装与本地安装
    • node_modules 目录与 package-lock.json
    • 运行脚本:npm run
  8. Node.js 中的异步编程:回调、Promise 与 Async/Await
    • 为什么异步编程如此重要?
    • 回调函数 (Callbacks):Node.js 的传统异步模式
    • 回调地狱 (Callback Hell) 问题
    • Promise:异步操作的优雅解决方案
    • Async/Await:让异步代码看起来像同步
    • 错误处理机制
  9. 构建一个简单的 Web 服务器示例 (综合运用)
  10. Node.js 的常见应用场景
  11. 总结与后续学习方向

1. 什么是 Node.js?它为什么如此受欢迎?

简单来说,Node.js 是一个开源、跨平台的 JavaScript 运行时环境。注意这里的关键词是“运行时环境”,这意味着它不是一门新的语言,也不是一个框架,而是一个让 JavaScript 代码可以在 浏览器之外 运行的平台。

在 Node.js 出现之前,JavaScript 主要用于浏览器前端,负责网页的交互和动态效果。但有了 Node.js,开发者可以使用 JavaScript 来编写服务器端代码、命令行工具、桌面应用(通过 Electron 等框架)等等,极大地拓展了 JavaScript 的应用范围。

Node.js 由 Ryan Dahl 于 2009 年创建,它诞生之初的目标就是为了构建高性能、可扩展的网络应用。

Node.js 为什么如此受欢迎?

  • 统一语言: 最大的优势之一是前后端都可以使用 JavaScript。这使得开发者可以复用知识、代码,减少了学习成本,也方便了全栈开发团队的协作。
  • 高性能: Node.js 基于 Google Chrome 的 V8 JavaScript 引擎,该引擎以其快速的代码执行而闻名。同时,Node.js 采用了非阻塞 I/O 和事件驱动的模型,非常适合处理大量并发连接(例如 Web 服务器)。
  • 巨大的生态系统 (NPM): Node.js 拥有目前世界上最大的开源库生态系统 NPM (Node Package Manager)。几乎你能想到的任何功能,都能在 NPM 上找到现成的轮子,这极大地提高了开发效率。
  • 活跃的社区: Node.js 拥有一个庞大且活跃的社区,提供了丰富的文档、教程和支持。
  • 适合 I/O 密集型应用: Node.js 的非阻塞特性使其在处理 I/O 密集型任务(如文件读写、网络请求、数据库交互)时表现出色,这正是 Web 服务器和 API 服务的主要工作。

虽然 Node.js 也有其局限性(例如不适合 CPU 密集型任务),但其优势使得它在构建现代 Web 应用和微服务领域占据了重要地位。

2. Node.js 的核心概念:V8 引擎、非阻塞 I/O 与事件循环

理解这三个概念是理解 Node.js 工作原理的关键。

2.1 V8 引擎:JavaScript 的心脏

Node.js 的核心是 Google Chrome 的 V8 JavaScript 引擎。V8 是一个高性能的开源 JavaScript 引擎,它能将 JavaScript 代码直接编译成机器码,而不是解释执行,这大大提高了 JavaScript 的运行速度。Node.js 利用 V8 引擎来执行 JavaScript 代码。

2.2 非阻塞 I/O 与单线程模型

与许多传统的服务器端技术(如 Java 的 Servlets 或 Ruby on Rails)不同,它们通常为每个客户端连接创建一个新的线程来处理请求。这种多线程模型在处理大量并发连接时,会因为线程创建、销毁和切换的开销而变得效率低下,占用大量内存。

Node.js 采用了 单线程模型(主要用于执行 JavaScript 代码)和 非阻塞 I/O。这意味着:

  • 单线程: Node.js 主进程只有一个线程负责执行 JavaScript 代码。这简化了开发,避免了多线程编程中的锁、同步等复杂问题。
  • 非阻塞 I/O: 当 Node.js 需要执行一个 I/O 操作(如读取文件、访问数据库、发起网络请求)时,它不会等待这个操作完成。相反,它会将这个操作交给底层的操作系统或线程池去处理,然后立即继续执行后续的 JavaScript 代码。当 I/O 操作完成后,系统会通知 Node.js,Node.js 再通过 回调函数 来处理操作的结果。

一个类比:

想象你去餐厅点餐。

  • 阻塞模型: 你点完餐后,必须坐在那里一直等到你的菜做好并端上来,期间你不能做任何其他事情。服务员也得等你点完并付款才能去服务下一桌。
  • 非阻塞模型 (Node.js): 你点完餐后,服务员给你一个叫号器 (代表一个回调函数),然后他可以去服务其他顾客了。你则可以去旁边休息、聊天或做其他事情。当你的菜做好了,叫号器响了 (代表 I/O 操作完成),你回到窗口取餐 (执行回调函数)。

这种非阻塞模型使得 Node.js 在等待 I/O 完成期间不会闲着,可以处理其他客户端的请求,因此在处理大量并发 I/O 密集型任务时,相比阻塞模型能显著提高吞吐量和性能。

2.3 事件循环 (Event Loop) 深度解析

既然 Node.js 是单线程且非阻塞的,那么它是如何管理这些非阻塞操作和回调函数的呢?这就是 事件循环 (Event Loop) 的作用。

事件循环是 Node.js 实现非阻塞 I/O 的核心机制。它是一个持续运行的循环,负责监听程序中各种事件(如 I/O 完成、定时器到期、新的请求到来等),并在事件发生时触发相应的回调函数执行。

事件循环的简化流程:

  1. Node.js 程序启动,执行顶层的同步 JavaScript 代码。
  2. 当遇到一个异步操作(如 fs.readFile()http.createServer()),Node.js 将这个操作交给底层的 C++ API (libuv) 处理,并提供一个回调函数。
  3. JavaScript 主线程继续执行后续的同步代码,不会等待异步操作的结果。
  4. 事件循环持续运行,它会检查是否有事件队列中有待处理的事件。
  5. 当底层的异步操作完成时(例如文件读取完毕),它会将对应的回调函数放入一个 事件队列 (Callback Queue 或 Event Queue) 中。
  6. 当 JavaScript 主线程执行栈为空(即所有同步代码都执行完毕)时,事件循环会从事件队列中取出一个回调函数,将其推到执行栈中执行。
  7. 这个过程不断重复,直到所有事件都处理完毕,程序退出(如果没有监听事件,或者所有事件都已处理)。

事件循环的阶段 (一个更详细的视图):

实际的 Node.js 事件循环比上面的简化版本更复杂,它分为多个阶段,每个阶段处理不同类型的事件:

  • timers: 执行 setTimeout()setInterval() 设定的回调。
  • pending callbacks: 执行某些系统操作的回调(例如 TCP 错误)。
  • idle, prepare: 仅内部使用。
  • poll: 等待新的 I/O 事件,处理 I/O 完成后的回调。大多数 I/O 回调(如文件读取、网络请求)都在此阶段执行。如果队列中有回调,Poll 阶段会执行它们;如果队列为空,Poll 阶段可能会等待新的事件,或者进入 check 阶段。
  • check: 执行 setImmediate() 设定的回调。
  • close callbacks: 执行一些关闭句柄的回调(例如 socket 的 close 事件)。

理解事件循环有助于你理解 Node.js 代码的执行顺序,尤其是在涉及到多个异步操作时。最重要的一点是:JavaScript 代码的执行是单线程的,异步操作本身不是在 JavaScript 线程中完成的,而是通过事件循环机制和底层非阻塞 I/O 来管理和调度回调的执行。

3. 搭建你的 Node.js 开发环境

在开始编写 Node.js 代码之前,你需要先安装 Node.js。

3.1 下载与安装 Node.js

访问 Node.js 官方网站:https://nodejs.org/

在首页你会看到两个下载选项:
* LTS (长期支持版): 推荐大多数用户选择,更稳定,有更长的维护周期。
* Current (最新版): 包含最新的功能,但可能不够稳定。

选择 LTS 版本下载适用于你操作系统的安装包(Windows, macOS, Linux)。安装过程通常是下一步、下一步,接受许可协议即可。安装程序会自动帮你配置环境变量。

3.2 验证安装

安装完成后,打开你的命令行终端(如 Windows 的命令提示符/PowerShell, macOS/Linux 的 Terminal)。输入以下命令来验证 Node.js 和 NPM 是否安装成功:

bash
node -v
npm -v

如果显示了版本号,说明安装成功了。

3.3 Node 版本管理工具 (NVM)

在实际开发中,你可能需要在不同的项目中使用不同版本的 Node.js。手动管理多个版本非常麻烦。推荐使用 Node 版本管理工具,其中最流行的是 NVM (Node Version Manager)

安装 NVM 后,你可以轻松地:
* 列出可用的 Node.js 版本:nvm list-remote
* 安装特定版本:nvm install <version> (例如 nvm install 18.17.0)
* 切换使用版本:nvm use <version>
* 设置默认版本:nvm alias default <version>

4. 运行你的第一个 Node.js 程序

环境搭建好了,让我们来运行一些 Node.js 代码。

4.1 使用 Node.js REPL

REPL (Read-Eval-Print Loop) 是 Node.js 提供的交互式环境。打开终端,输入 node,你就进入了 REPL 环境:

“`bash
$ node

“`

你可以在这里直接输入 JavaScript 代码并立即执行:

“`javascript

console.log(‘Hello from Node.js REPL!’);
Hello from Node.js REPL!
undefined // console.log 返回 undefined
let a = 10;
undefined
a + 5
15
.help // 查看帮助命令
.exit // 退出 REPL
“`

REPL 是一个学习和测试 Node.js 代码片段的好地方。

4.2 执行 JavaScript 文件

通常我们是将 JavaScript 代码写入文件,然后使用 Node.js 来执行这个文件。

创建一个新文件,命名为 hello.js,然后用文本编辑器打开,输入以下代码:

javascript
// hello.js
console.log("Hello, Node.js!");

保存文件。回到终端,进入到 hello.js 文件所在的目录,然后运行以下命令:

bash
node hello.js

你会在终端看到输出:

Hello, Node.js!

恭喜!你成功运行了你的第一个 Node.js 程序。这表明 Node.js 能够读取并执行独立的 JavaScript 文件。

5. Node.js 模块系统:requiremodule.exports

Node.js 采用了模块化的设计,这使得组织和复用代码变得非常容易。每个文件都被视为一个独立的模块。

5.1 什么是模块?

模块是封装了一定功能的文件。模块内部定义的变量、函数、类等默认是私有的,外部无法直接访问。如果希望外部能够使用模块内部的功能,就需要通过特定的方式将它们“导出”;如果希望使用其他模块导出的功能,就需要通过特定的方式“引入”。

Node.js 的模块系统遵循 CommonJS 规范(Node.js 的早期版本实现,现在也有 ES Modules 支持,但入门阶段先掌握 CommonJS)。

5.2 引入模块:require()

在 Node.js 中,使用 require() 函数来引入其他模块。require() 函数接收一个模块标识符作为参数,返回该模块导出的内容。

模块标识符可以是:
* 核心模块名:'fs', 'http', 'path'。Node.js 会直接加载内建模块。
* 相对路径:'./my-module', '../utils/helper'。用于引入当前项目中的其他文件模块,路径是相对于当前文件。文件后缀名 .js 通常可以省略。
* 绝对路径: 不常用。
* 包名:'express', 'lodash'。用于引入通过 NPM 安装的第三方模块。Node.js 会在 node_modules 目录中查找对应的包。

示例:引入核心模块

“`javascript
// index.js
const fs = require(‘fs’); // 引入内建的文件系统模块

// 现在可以使用 fs 模块提供的功能了
fs.readFile(‘somefile.txt’, ‘utf8’, (err, data) => {
if (err) {
console.error(‘Error reading file:’, err);
return;
}
console.log(‘File content:’, data);
});
“`

5.3 导出模块:module.exports / exports

在一个模块中,你可以使用 module.exportsexports 对象来定义该模块要导出的内容,供其他模块使用 require() 引入。

  • module.exports: 这是真正用于导出的对象。默认情况下,module.exports 是一个空对象 {}。你可以将任何值赋给 module.exports,例如一个对象、一个函数、一个字符串等等。
  • exports: 这是一个指向 module.exports 的引用,即 exports = module.exports。你可以通过 exports.propertyName = value 的方式向导出的对象添加属性。

重要区别:
* 如果你想将一个函数、类或单一值作为模块的导出,应该直接赋值给 module.exports。例如:module.exports = myFunction;module.exports = { a: 1, b: 2 };
* 如果你想导出多个属性,可以通过 exports.prop1 = value1; exports.prop2 = value2; 的方式。
* 不要 同时使用 exports = someValue;module.exports = anotherValue;,因为 exports = someValue 会切断 exportsmodule.exports 的引用,导致最终导出的是 module.exports 的值,而不是 exports 新指向的值。推荐始终使用 module.exports 进行导出,以避免混淆。

示例:自定义模块导出与引入

创建文件 my-module.js:

“`javascript
// my-module.js

const greeting = “Hello from my module!”;

function sayHello(name) {
return Hello, ${name}! ${greeting};
}

const myNumber = 123;

// 导出多个属性
module.exports = {
sayHello: sayHello,
myNumber: myNumber,
// 可以直接导出值
myConstant: “This is a constant”
};

// 或者你也可以这样写 (不推荐同时使用 exports 和 module.exports = …)
// exports.sayHello = sayHello;
// exports.myNumber = myNumber;
“`

创建文件 app.js,引入并使用 my-module.js:

“`javascript
// app.js

const myModule = require(‘./my-module’); // 引入当前目录下的 my-module.js

console.log(myModule.myConstant); // 输出: This is a constant
console.log(myModule.sayHello(‘Alice’)); // 输出: Hello, Alice! Hello from my module!
console.log(myModule.myNumber + 10); // 输出: 133
“`

运行 node app.js,你会看到相应的输出。

6. Node.js 内建核心模块

Node.js 提供了许多非常有用的内建模块,无需额外安装即可直接使用 require() 引入。这些模块提供了操作系统、文件系统、网络等底层功能的接口。

6.1 fs 模块:文件系统操作

fs 模块提供了与文件系统交互的功能,例如读取文件、写入文件、创建目录、删除文件等。fs 模块中的大多数方法都提供了同步 (Synchronous) 和异步 (Asynchronous) 两个版本。

  • 同步版本 (Sync): 方法名通常以 Sync 结尾,如 readFileSync。同步方法会阻塞 Node.js 主线程,直到操作完成。在服务器端,应尽量避免使用同步方法,除非是在程序启动时的配置加载等场景,因为它们会阻止处理其他请求。
  • 异步版本: 方法名通常不带 Sync 结尾,如 readFile。异步方法接收一个回调函数作为最后一个参数,操作完成后会调用该回调函数,不会阻塞主线程。在 Node.js 服务器开发中,异步方法是首选。

异步读取文件示例:

“`javascript
const fs = require(‘fs’);

fs.readFile(‘example.txt’, ‘utf8’, (err, data) => {
if (err) {
console.error(‘读取文件出错:’, err);
return;
}
console.log(‘文件内容:’, data);
});

console.log(‘这行代码会先于文件内容输出,因为 readFile 是异步的’);

// example.txt 文件内容随意写一些,比如 “Hello file system!”
“`

同步读取文件示例 (应谨慎使用):

“`javascript
const fs = require(‘fs’);

try {
const data = fs.readFileSync(‘example.txt’, ‘utf8’);
console.log(‘文件内容 (同步):’, data);
} catch (err) {
console.error(‘读取文件出错 (同步):’, err);
}

console.log(‘这行代码会后于文件内容输出’);
“`

异步方法通常采用 (err, data) 这种 错误优先的回调 (Error-First Callback) 模式,即回调函数的第一个参数是错误对象,如果操作成功,错误对象为 null,第二个参数才是操作的结果。

6.2 http 模块:构建 Web 服务器

http 模块是 Node.js 构建 Web 服务器的核心。

创建一个简单的 HTTP 服务器示例:

“`javascript
const http = require(‘http’);

const hostname = ‘127.0.0.1’; // 本地主机
const port = 3000; // 监听端口

const server = http.createServer((req, res) => {
// req (request): 客户端请求对象,包含请求头、URL、方法等信息
// res (response): 服务器响应对象,用于向客户端发送响应

res.statusCode = 200; // 设置响应状态码为 200 (OK)
res.setHeader(‘Content-Type’, ‘text/plain’); // 设置响应头,说明内容类型是纯文本
res.end(‘Hello from Node.js HTTP Server!\n’); // 发送响应体并结束响应
});

server.listen(port, hostname, () => {
console.log(服务器运行在 http://${hostname}:${port}/);
});
“`

将这段代码保存为 server.js,然后在终端运行 node server.js。打开浏览器,访问 http://127.0.0.1:3000/,你将看到 “Hello from Node.js HTTP Server!” 的字样。

这个例子展示了如何创建一个服务器、如何处理请求 (req) 和发送响应 (res),以及如何让服务器监听特定的端口。

6.3 path 模块:处理文件路径

path 模块提供了用于处理文件和目录路径的工具。它非常有用,尤其是在处理跨平台路径问题时。

“`javascript
const path = require(‘path’);

// 拼接路径
const filePath = path.join(__dirname, ‘data’, ‘users.json’);
console.log(‘拼接后的路径:’, filePath);
// __dirname 是 Node.js 中一个全局变量,表示当前文件所在的目录的绝对路径

// 获取文件名
const fileName = path.basename(filePath);
console.log(‘文件名:’, fileName);

// 获取文件所在的目录名
const dirName = path.dirname(filePath);
console.log(‘目录名:’, dirName);

// 获取文件扩展名
const fileExt = path.extname(filePath);
console.log(‘文件扩展名:’, fileExt);

// 解析路径为一个对象
const parsedPath = path.parse(filePath);
console.log(‘解析后的路径:’, parsedPath);
/ 输出类似:
{
root: ‘/’,
dir: ‘/path/to/your/project/data’,
base: ‘users.json’,
ext: ‘.json’,
name: ‘users’
}
/
“`

使用 path 模块可以确保你的路径操作在 Windows、macOS 和 Linux 等不同操作系统上都能正确工作。

6.4 其他常用核心模块

  • os: 提供与操作系统交互的方法,如获取 CPU 信息、内存信息、网络接口等。
  • url: 解析和格式化 URL。
  • querystring: 解析和格式化 URL 查询字符串。
  • events: Node.js 的事件触发器,许多 Node.js 对象(如 HTTP 服务器、文件流)都继承自 EventEmitter,用于处理事件。

深入了解这些核心模块的文档对于 Node.js 开发至关重要。

7. 掌握 NPM:Node.js 的包管理器

NPM (Node Package Manager) 是 Node.js 官方的包管理器,也是世界上最大的软件包注册中心之一。它使得开发者能够轻松地分享、使用和管理 Node.js 项目中的代码包(也称为模块或库)。

安装 Node.js 时,NPM 通常也会被一起安装。

7.1 NPM 是什么?

NPM 包含两部分:
1. 命令行工具: 允许你与 NPM 注册中心进行交互,安装、发布、管理依赖等。
2. NPM 注册中心: 一个巨大的在线数据库,存储了成千上万的开源 Node.js 包。

7.2 package.json 文件:项目的心脏

package.json 文件是 Node.js 项目的配置文件,它包含了项目的元数据以及项目所依赖的第三方包信息。

如何创建 package.json?

在你的项目根目录下打开终端,运行:

bash
npm init

NPM 会引导你填写一些项目信息(项目名、版本、描述、入口文件等),按 Enter 键接受默认值或自行输入。最终会生成一个 package.json 文件。

package.json 中的重要字段:

  • name: 项目名称(必须,小写,不能包含空格)
  • version: 项目版本号(必须)
  • description: 项目描述
  • main: 项目的入口文件,使用 require() 引入项目时默认加载的文件。
  • scripts: 定义可执行的脚本命令,如启动、测试等。
  • dependencies: 项目在生产环境运行时所依赖的包。
  • devDependencies: 项目在开发和测试环境所依赖的包(如测试框架、打包工具等)。
  • keywords: 项目关键词,方便搜索。
  • author: 作者信息。
  • license: 项目许可证。

示例 package.json:

json
{
"name": "my-node-app",
"version": "1.0.0",
"description": "A simple Node.js application",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"node",
"example"
],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.17.1" // 示例:依赖了 Express 框架
},
"devDependencies": {
"nodemon": "^2.0.7" // 示例:开发依赖了 nodemon (自动重启工具)
}
}

7.3 安装依赖包:npm install

使用 npm install 命令来安装依赖包。

  • 安装生产依赖:
    bash
    npm install <package-name>
    # 或简写
    npm i <package-name>

    这会将 <package-name> 安装到项目本地的 node_modules 目录中,并在 package.jsondependencies 字段中添加该依赖及其版本信息。

  • 安装开发依赖:
    bash
    npm install <package-name> --save-dev
    # 或简写
    npm i <package-name> -D

    这会将 <package-name> 安装到项目本地的 node_modules 目录中,并在 package.jsondevDependencies 字段中添加该依赖及其版本信息。

  • 安装所有依赖:
    在包含 package.json 的项目目录下,运行 npm installnpm i,NPM 会读取 package.json 中的 dependenciesdevDependencies 字段,并安装所有列出的包。

  • 安装特定版本:
    bash
    npm install <package-name>@<version>
    # 例如
    npm install [email protected]

  • 卸载依赖:
    bash
    npm uninstall <package-name>

7.4 全局安装与本地安装

  • 本地安装 (默认): npm install <package-name> 将包安装在当前项目的 node_modules 目录下。这是推荐的方式,因为每个项目可以拥有自己独立的依赖版本,避免冲突。通过 require() 引入本地安装的包。
  • 全局安装: npm install -g <package-name> 将包安装在系统全局的 NPM 目录下。全局安装通常用于安装命令行工具,例如 nodemon (一个自动重启 Node.js 应用的工具) 或 create-react-app (React 应用创建工具)。全局安装的包不能直接在代码中通过 require() 引入。

示例:全局安装 nodemon

bash
npm install -g nodemon

安装后,你就可以在任何目录下使用 nodemon 命令来运行你的 Node.js 文件,它会自动监控文件变化并重启应用。

bash
nodemon server.js

7.5 node_modules 目录与 package-lock.json

  • node_modules: 当你运行 npm install 时,所有安装的包及其依赖的包都会被放在项目根目录下的 node_modules 目录中。这个目录可能会非常庞大,通常不应该提交到版本控制系统(如 Git)中(在 .gitignore 文件中忽略)。
  • package-lock.json:npm install 后会自动生成或更新。它精确地记录了安装时所有依赖包的版本号以及它们的依赖关系树。它的作用是锁定依赖版本,确保团队成员或在不同环境中安装依赖时,都能安装到完全相同的版本,避免由于版本差异导致的问题。你应该将 package-lock.json 文件提交到版本控制系统中。

7.6 运行脚本:npm run

package.jsonscripts 字段允许你定义一些快捷命令。

例如,在上面的 package.json 中定义了 "start": "node index.js"。你可以在终端中运行:

bash
npm start

NPM 会执行 scripts.start 对应的命令 node index.js

你也可以运行其他自定义脚本:

bash
npm run test

运行 npm run 命令时,NPM 会将 node_modules/.bin 目录添加到系统的 PATH 环境变量中,这意味着你可以在脚本中直接使用本地安装的可执行文件,例如测试框架 Jest (jest) 或构建工具 Webpack (webpack),而无需全局安装它们。

8. Node.js 中的异步编程:回调、Promise 与 Async/Await

正如前面提到的,Node.js 的核心是单线程和非阻塞 I/O。这意味着异步编程是 Node.js 开发中最重要的部分。你需要学习如何处理那些不会立即返回结果,而是在未来某个时间点通过回调函数通知你的操作。

异步编程在 Node.js 中主要经历了三个发展阶段:回调函数 -> Promise -> Async/Await。理解它们之间的演进对于写出清晰、易维护的异步代码至关重要。

8.1 为什么异步编程如此重要?

想象一个 Web 服务器需要处理用户请求:读取数据库、访问外部 API、读写文件等。这些操作通常都需要花费一些时间(几毫秒到几秒不等)。

如果使用同步阻塞的方式:
javascript
// 伪代码 - 阻塞版本
const userData = readUserFromDatabase(userId); // 线程在这里等待数据库查询结果
const result = processData(userData);
sendResponse(result); // 发送响应

当一个用户请求正在等待数据库查询时,整个 Node.js 主线程会被阻塞住,无法处理其他用户的请求,直到当前请求的数据库操作完成。这会导致服务器在面对并发请求时性能急剧下降。

如果使用异步非阻塞的方式:
javascript
// 伪代码 - 非阻塞版本
readUserFromDatabase(userId, (err, userData) => {
if (err) { handleError(err); return; }
const result = processData(userData);
sendResponse(result);
});
// 在数据库查询进行的同时,Node.js 可以继续处理其他请求,不会阻塞
console.log("正在处理其他请求...");

当发起数据库查询时,Node.js 将任务交给底层并注册一个回调函数,然后立即去处理下一个请求。当数据库查询完成后,事件循环会将对应的回调函数放入队列,并在主线程空闲时执行它。这样,Node.js 就能高效地处理大量并发请求。

8.2 回调函数 (Callbacks):Node.js 的传统异步模式

回调函数是将一个函数作为参数传递给另一个函数,在特定事件发生或异步操作完成后,被传递的函数会被调用执行。这是 Node.js 早期和许多核心模块采用的主要异步模式。

错误优先回调 (Error-First Callback): 在 Node.js 中,异步回调函数的第一个参数通常是 err 对象,如果操作成功,errnull;如果发生错误,err 包含错误信息。第二个参数通常是操作成功时的结果数据。

“`javascript
const fs = require(‘fs’);

fs.readFile(‘nonexistent.txt’, ‘utf8’, (err, data) => {
if (err) {
console.error(‘读取文件出错:’, err.message); // 处理错误
return;
}
console.log(‘文件内容:’, data); // 处理成功数据
});

fs.readFile(‘example.txt’, ‘utf8’, (err, data) => {
if (err) {
console.error(‘读取文件出错:’, err);
return;
}
console.log(‘成功读取文件:’, data);
});
“`

8.3 回调地狱 (Callback Hell) 问题

当存在多个相互依赖的异步操作时,使用回调函数会导致代码层层嵌套,形成难以阅读和维护的“回调地狱”:

javascript
// 示例:回调地狱
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
if (err1) { return handleError(err1); }
console.log('读取 file1 成功:', data1);
fs.readFile('file2.txt', 'utf8', (err2, data2) => { // 第二层嵌套
if (err2) { return handleError(err2); }
console.log('读取 file2 成功:', data2);
fs.writeFile('file3.txt', data1 + data2, 'utf8', (err3) => { // 第三层嵌套
if (err3) { return handleError(err3); }
console.log('写入 file3 成功');
// 更多嵌套...
});
});
});

随着异步操作数量的增加,代码会向右缩进,变得越来越难看和难以理解逻辑流程。

8.4 Promise:异步操作的优雅解决方案

为了解决回调地狱问题,Promise 应运而生。Promise 代表一个异步操作的最终结果,它有三种状态:
* Pending (进行中): 初始状态,既不是成功也不是失败。
* Fulfilled (已成功): 操作成功完成。
* Rejected (已失败): 操作失败。

Promise 一旦从 Pending 变为 Fulfilled 或 Rejected,状态就不会再改变 (Resolved)。

Promise 提供了 .then() 方法来处理成功的结果,以及 .catch() 方法来处理失败(错误)。Promise 可以链式调用,使得多个异步操作的顺序执行变得更加清晰。

许多 Node.js 核心模块也提供了 Promise 版本的 API (通常在 require('模块名/promises')),或者你可以使用库将回调 API 转换为 Promise API (util.promisify)。

使用 Promise 改进上面的回调地狱示例:

首先,假设我们有 Promise 版本的 readFilewriteFile (Node.js 内建的 fs/promises 模块就提供了):

“`javascript
const fsPromises = require(‘fs/promises’); // 引入 Promise 版本的 fs

fsPromises.readFile(‘file1.txt’, ‘utf8’)
.then(data1 => {
console.log(‘读取 file1 成功:’, data1);
return fsPromises.readFile(‘file2.txt’, ‘utf8’); // 返回一个新的 Promise
})
.then(data2 => {
console.log(‘读取 file2 成功:’, data2);
return fsPromises.writeFile(‘file3.txt’, data1 + data2, ‘utf8’); // 返回一个新的 Promise
})
.then(() => {
console.log(‘写入 file3 成功’);
})
.catch(err => { // 统一处理链条中的任何错误
console.error(‘发生错误:’, err);
});
“`

使用 Promise 后,代码变得更加扁平化,逻辑流程更清晰,错误处理也集中在最后的 .catch() 中。

8.5 Async/Await:让异步代码看起来像同步

Async/Await 是基于 Promise 的一种更现代的异步编程语法糖,它使用 async 函数和 await 关键字,使得异步代码的写法更接近同步代码,进一步提高了代码的可读性和可维护性。

  • async 关键字用于声明一个函数是异步函数。异步函数总是返回一个 Promise。
  • await 关键字用于等待一个 Promise 解决(Resolved,即成功或失败)。await 只能在 async 函数内部使用。当遇到 await 时,异步函数会暂停执行,直到被等待的 Promise 解决,然后恢复执行,并返回 Promise 解决的值(如果成功)或抛出错误(如果失败)。

使用 Async/Await 改进上面的 Promise 示例:

“`javascript
const fsPromises = require(‘fs/promises’);

async function processFiles() {
try {
const data1 = await fsPromises.readFile(‘file1.txt’, ‘utf8’); // 等待 file1 读取完成
console.log(‘读取 file1 成功:’, data1);

const data2 = await fsPromises.readFile('file2.txt', 'utf8'); // 等待 file2 读取完成
console.log('读取 file2 成功:', data2);

await fsPromises.writeFile('file3.txt', data1 + data2, 'utf8'); // 等待写入完成
console.log('写入 file3 成功');

} catch (err) {
console.error(‘发生错误:’, err); // 使用 try…catch 处理错误
}
}

processFiles(); // 调用异步函数
“`

使用 Async/Await 后,代码的可读性得到了极大的提升,看起来几乎就像同步代码一样,但底层仍然是非阻塞的 Promise 机制。错误处理也变得像同步代码一样,可以使用 try...catch 块。

总结异步编程:
* 理解 Node.js 的非阻塞特性是异步编程的根本原因。
* 从回调函数开始学习,理解其工作原理。
* 认识回调地狱的问题,学习 Promise 如何解决它。
* 掌握 Async/Await,这是目前编写异步 Node.js 代码最推荐的方式,因为它结合了非阻塞的性能和同步代码的可读性。

9. 构建一个简单的 Web 服务器示例 (综合运用)

让我们结合之前学到的 http 模块、文件系统 fs (使用 Promise 版本) 和 Async/Await 来构建一个更实用的 Web 服务器。这个服务器将根据不同的 URL 路径返回不同的内容或处理不同的请求。

“`javascript
const http = require(‘http’);
const url = require(‘url’); // 解析 URL
const fsPromises = require(‘fs/promises’); // 使用 Promise 版本的 fs
const path = require(‘path’); // 处理文件路径

const hostname = ‘127.0.0.1’;
const port = 3000;
const publicDir = path.join(__dirname, ‘public’); // 存放静态文件的目录

// 确保 public 目录存在
// 注意:在生产环境中,静态文件通常由专门的 Web 服务器 (如 Nginx) 或框架处理
async function ensurePublicDir() {
try {
await fsPromises.access(publicDir); // 检查目录是否存在
console.log(目录 ${publicDir} 已存在.);
} catch (error) {
if (error.code === ‘ENOENT’) { // 如果是 ‘No Entry’ 错误码,表示目录不存在
try {
await fsPromises.mkdir(publicDir); // 创建目录
console.log(目录 ${publicDir} 创建成功.);
// 创建一个示例文件
await fsPromises.writeFile(path.join(publicDir, ‘index.html’), ‘

Welcome!

This is a simple Node.js server.

‘, ‘utf8’);
console.log(‘创建示例 index.html 文件.’);
} catch (mkdirError) {
console.error(‘创建目录或文件失败:’, mkdirError);
process.exit(1); // 创建失败则退出应用
}
} else {
console.error(‘访问目录时发生未知错误:’, error);
process.exit(1); // 其他错误也退出
}
}
}

// 在服务器启动前先确保目录存在
ensurePublicDir().then(() => {
const server = http.createServer(async (req, res) => {
// 使用 async/await 处理请求
const parsedUrl = url.parse(req.url, true); // 解析 URL,true 表示解析查询字符串
const pathname = parsedUrl.pathname; // 获取路径部分 (不包含查询参数)

console.log(`收到请求: ${req.method} ${req.url}`);

if (pathname === '/') {
  // 处理根路径请求,返回 index.html
  const filePath = path.join(publicDir, 'index.html');
  try {
    const data = await fsPromises.readFile(filePath, 'utf8');
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end(data);
  } catch (error) {
    console.error('读取 index.html 失败:', error);
    res.statusCode = 500;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Internal Server Error: Could not load index.html');
  }

} else if (pathname === '/api/users' && req.method === 'GET') {
  // 处理 /api/users 的 GET 请求
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];
  res.end(JSON.stringify(users)); // 发送 JSON 响应

} else if (pathname === '/greet' && req.method === 'GET') {
  // 处理 /greet 的 GET 请求,带查询参数 ?name=...
  const name = parsedUrl.query.name || 'Guest'; // 获取查询参数 'name',默认为 'Guest'
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end(`Hello, ${name}!`);

} else {
  // 处理所有其他未知路径
  res.statusCode = 404;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Not Found');
}

});

server.listen(port, hostname, () => {
console.log(服务器运行在 http://${hostname}:${port}/);
});

}).catch(err => {
console.error(‘启动服务器前置任务失败:’, err);
});

“`

将此代码保存为 advanced_server.js。确保在同一目录下运行 node advanced_server.js
这个例子展示了:
* 使用 http 模块创建服务器。
* 使用 url 模块解析请求 URL。
* 使用 path 模块构建文件路径。
* 使用 fs/promises 异步读取文件。
* 在请求处理函数中使用 async/await 来简化异步代码。
* 根据不同的请求路径和方法返回不同的响应(HTML、JSON、纯文本)。
* 处理简单的错误。

请注意,这仍然是一个非常基础的服务器。在实际应用中,通常会使用 Express、Koa 或 NestJS 等 Web 框架来简化路由、中间件、模板引擎、数据库集成等任务。

10. Node.js 的常见应用场景

凭借其高性能、可扩展性和庞大的生态系统,Node.js 在许多领域都有广泛的应用:

  • Web 服务器和 API 服务: 构建高性能的 RESTful API、GraphQL API 是 Node.js 最常见的用途。Express、Koa、NestJS 是流行的框架。
  • 实时应用: 如聊天应用、在线游戏、协作工具等,Node.js 的事件驱动和非阻塞特性非常适合处理大量实时连接。Socket.IO 是一个常用的实时通信库。
  • 微服务 (Microservices): Node.js 轻量且启动速度快,非常适合构建独立的、专注于特定功能的微服务。
  • 命令行工具 (CLI): NPM 上的许多工具(如 Webpack, Babel, ESLint, Prettier)都是用 Node.js 编写的。Node.js 提供了方便的接口来与命令行交互。
  • 构建工具: 前端构建工具链(如 Webpack, Gulp, Grunt)都运行在 Node.js 环境中。
  • 全栈应用: 随着前端框架(如 React, Vue, Angular)的兴起,开发者可以使用 Node.js 作为后端,实现真正的全栈 JavaScript 开发。
  • 服务器端渲染 (SSR): 配合现代前端框架实现服务器端渲染,提高首次加载速度和 SEO。

11. 总结与后续学习方向

恭喜你读完了这篇 Node.js 入门指南!现在你应该对 Node.js 是什么、它的核心工作原理、如何搭建环境、运行代码、使用模块、管理依赖以及处理异步操作有了基本的理解。

Node.js 的基础知识围绕着 JavaScript 运行时非阻塞 I/O事件循环模块系统包管理 (NPM) 展开。理解并掌握这些概念是深入学习 Node.js 的基石。

但这仅仅是一个开始,Node.js 的世界非常广阔。要成为一名熟练的 Node.js 开发者,你还需要继续学习:

  • 更深入的模块系统: 学习 ES Modules 在 Node.js 中的使用。
  • 更多核心模块: 深入学习 events, stream, buffer, cluster, worker_threads 等模块。
  • Web 框架: 学习使用 Express (简单易用)、Koa (更灵活,基于 async/await)、NestJS (基于 TypeScript,企业级应用框架) 等框架,它们能极大地简化 Web 应用开发。
  • 数据库交互: 学习如何连接和操作不同类型的数据库(如 MongoDB, PostgreSQL, MySQL)以及相关的 ORM/ODM 库。
  • 错误处理和调试: 学习如何在 Node.js 应用中进行有效的错误处理和使用调试工具。
  • 测试: 学习使用 Jest, Mocha 等测试框架编写单元测试、集成测试。
  • 安全: 学习 Node.js 应用的常见安全漏洞和防范措施。
  • 性能优化: 学习如何诊断和优化 Node.js 应用的性能。
  • 部署: 学习如何将 Node.js 应用部署到服务器或云平台。
  • 进阶异步模式: 学习事件触发器、流 (Streams) 等更高级的异步处理方式。

多动手实践、阅读官方文档、参与开源社区是提升 Node.js 技能的最好方法。从搭建一个简单的博客或 API 开始,逐步构建更复杂的应用。

祝你在 Node.js 的学习旅程中一切顺利!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部