Node.js 基础
1. Node.js 概念
JavaScript 运行时环境
长久以来,JavaScript 只能在浏览器中运行,主要用于给网页添加交互功能。Node.js 的诞生彻底改变了这一点。
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境 (JavaScript runtime)。它允许开发者使用 JavaScript 来编写后端服务器、命令行工具等任何以往由 Python, Ruby, Java 等语言完成的程序。它使用了 Google Chrome 浏览器中性能强大的 V8 JavaScript 引擎,因此执行速度非常快。
为了能在服务器端运行,Node.js 提供了许多浏览器中没有的 API,如:
- 文件系统操作 (
fs
模块) - 创建网络服务器 (
http
模块) - 处理网络请求
- 与数据库交互
注意:Node.js 本身不是一种语言,它只是一个平台或工具。我们编写的仍然是标准的 JavaScript。
Node.js 的用途
凭借其非阻塞、事件驱动的特性,Node.js 特别适合处理高并发的 I/O 密集型任务。常见的应用场景包括:
- Web 服务器和 API: 这是 Node.js 最经典的应用,用于构建网站后端、RESTful API 或 GraphQL API。
- 实时应用: 例如在线聊天室、协作工具、游戏服务器等,利用 WebSockets 可以轻松实现实时数据交换。
- 命令行工具 (CLI): 许多前端构建工具,如
webpack
,Vite
,ESLint
等,本身就是用 Node.js 开发的。 - 微服务: Node.js 轻量且启动速度快,非常适合作为微服务架构中的一个服务节点。
后端
一个网站可以分为前端 (Client-side) 和后端 (Server-side)。
- 静态网站: 只包含 HTML, CSS, JavaScript 文件。用户请求时,服务器直接将这些文件发送给浏览器。所有逻辑都在浏览器中发生。这种网站不需要后端(或者说只需要一个最简单的文件服务器)。
- 动态网站: 需要根据用户请求、数据库中的数据或其他条件动态生成页面内容。例如,一个电商网站需要显示用户信息、订单历史,这些数据存储在服务器的数据库中。这就必须需要一个后端来处理业务逻辑。
后端的主要职责包括:
- 处理业务逻辑: 例如用户注册、登录验证、下订单等。
- 操作数据库: 对数据进行增、删、改、查。
- 提供 API: 给前端或其他服务提供数据接口。
- 身份验证与授权: 确保只有合法的用户才能访问特定资源。
2. 异步与事件驱动
Node.js 的核心哲学是 “异步、非阻塞 I/O”。这是它能够高效处理大量并发请求的关键。
异步概念
- 同步代码: 按顺序执行,一行执行完才能执行下一行。如果某一行是耗时操作(如读取大文件、网络请求),整个程序都会被阻塞。
- 异步代码: 发起一个耗时操作后,不等待结果,立即继续执行后续代码。当耗时操作完成后,通过回调函数 (Callback)、Promise 或 Async/Await 来处理结果。
例如,下面这个例子:
// 假设有一个耗时的数据库查询函数
db.query('SELECT * FROM users', function(result) {
// 这是回调函数,在查询完成后才执行
console.log('数据库查询完成!');
});
// 这行代码会立即执行,不会等待上面的查询
console.log('发起数据库查询...');
输出结果会是:
发起数据库查询...
数据库查询完成!
事件循环
Node.js 通过**事件循环(Event Loop)**实现异步。
可以把事件循环想象成一个永不疲倦的调度员。它不断地检查一个叫做“任务队列”的地方,看看有没有已经完成的异步任务。
一个简化的模型如下:
- 调用栈 (Call Stack): 所有同步代码都在这里执行。当一个函数被调用,它被推入栈顶;执行完毕后,被弹出。
- Node APIs: 当遇到异步操作(如
fs.readFile
,setTimeout
)时,Node.js 会把它交给底层的 C++ API 处理,然后立即继续执行后面的同步代码。 - 任务队列 (Task Queue / Callback Queue): 当异步操作完成后(例如文件读完了,定时器到时间了),其对应的回调函数会被放入任务队列中排队。
- 事件循环 (Event Loop): 它的唯一工作就是持续监控调用栈。一旦调用栈变空(即所有同步代码都执行完了),它就会从任务队列里取出第一个回调函数,并将其推入调用栈中执行。
这个过程周而复始,永不停歇,因此得名“事件循环”。
3. 第一个 Node.js 程序
运行 Node.js 脚本
- 创建一个文件,例如
app.js
。 - 在文件中写入 JavaScript 代码,例如
console.log('Hello, Node.js!');
。 - 打开终端,进入文件所在目录。
- 执行命令
node app.js
。
就可以在终端看到输出 Hello, Node.js!
。
“Hello World” 服务器
使用 http
模块可以轻松创建一个简单的 Web 服务器。
// 1. 引入 http 核心模块
const http = require('http');
// 2. 定义服务器的主机名和端口
const hostname = '127.0.0.1'; // 本地地址
const port = 3000;
// 3. 创建服务器实例
// createServer 接受一个回调函数,每次有请求进来时都会执行
const server = http.createServer((req, res) => {
// req: 请求对象,包含了来自客户端的所有信息(如 URL, 请求头)
// res: 响应对象,用于向客户端发送数据
// 设置响应状态码为 200 (OK)
res.statusCode = 200;
// 设置响应头,告诉浏览器内容是 HTML
res.setHeader('Content-Type', 'text/html');
// 结束响应,并发送内容
res.end('<h1>Hello World!</h1>');
});
// 4. 启动服务器,监听指定端口
server.listen(port, hostname, () => {
// 这个回调函数在服务器成功启动后执行
console.log(`Server running at http://${hostname}:${port}/`);
});
保存为 server.js
并用 node server.js
运行。然后在浏览器中访问 http://127.0.0.1:3000
,可以看到 “Hello World!”。
4. Node.js 核心模块
Node.js 内置了许多功能强大的模块,无需安装即可直接使用。
模块系统
Node.js 有两种模块系统:传统的 CommonJS 和现代的 ES Modules (ESM)。
- CommonJS: 这是 Node.js 默认的、历史悠久的模块系统。
- 使用
require()
函数导入模块。 - 使用
module.exports
对象导出一个模块的内容。
- 使用
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// app.js
const math = require('./math.js');
console.log(math.add(2, 3)); // 5
- ES Modules: 这是 JavaScript 官方标准。Node.js 也已全面支持。
- 使用
import
关键字导入。 - 使用
export
关键字导出。 - 为了让 Node.js 将文件识别为 ES 模块,通常将文件后缀改为
.mjs
,或在package.json
中设置"type": "module"
。
- 使用
HTTP 模块
http
模块是构建网络应用的基础。http.createServer
创建的服务器是一个 EventEmitter
实例。我们可以通过检查 req.url
来实现简单的路由。
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html');
if (req.url === '/') {
res.statusCode = 200;
res.end('<h1>Welcome to the homepage!</h1>');
} else if (req.url === '/about') {
res.statusCode = 200;
res.end('<h1>About Us</h1>');
} else {
res.statusCode = 404;
res.end('<h1>404 Not Found</h1>');
}
});
server.listen(3000, () => {
console.log('Server is listening on port 3000');
});
文件系统模块
fs
模块提供了所有与文件交互的功能。几乎所有 fs
操作都提供同步和异步两个版本。
- 异步读取:
fs.readFile(path, options, callback)
:
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
- 异步写入:
fs.writeFile(path, data, options, callback)
:
const fs = require('fs');
const content = 'Some content!';
fs.writeFile('newfile.txt', content, err => {
if (err) throw err;
console.log('File has been saved!');
});
最佳实践: 始终优先使用异步方法,以避免阻塞事件循环。只有在程序启动时加载配置文件等少数情况下,才考虑使用同步方法(如
fs.readFileSync
)。
URL 模块
const myURL = new URL('https://example.org/foo/bar?q=baz#hash');
console.log(myURL.href); // 'https://example.org/foo/bar?q=baz#hash'
console.log(myURL.protocol); // 'https:'
console.log(myURL.hostname); // 'example.org'
console.log(myURL.pathname); // '/foo/bar'
console.log(myURL.search); // '?q=baz'
console.log(myURL.hash); // '#hash'
// 获取查询参数
console.log(myURL.searchParams.get('q')); // 'baz'
在 HTTP 服务器中,你可以这样解析请求的 URL:
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
Events 模块
Node.js 的许多核心对象(如 HTTP 服务器、流)都是 EventEmitter
的实例。这个模块允许我们实现观察者模式,即创建、触发和监听自定义事件:
- 继承
EventEmitter
: 让一个类拥有事件能力。 - 使用
.on(eventName, listener)
: 监听事件。 - 使用
.emit(eventName, [args])
: 触发事件。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 监听 'greet' 事件
myEmitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
// 触发 'greet' 事件,并传递参数
myEmitter.emit('greet', 'Node.js'); // 输出: Hello, Node.js!