Dark Dwarf Blog background

Node.js 基础

Node.js 基础

1. Node.js 概念

a.a. 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。

b.b. Node.js 的用途

凭借其非阻塞、事件驱动的特性,Node.js 特别适合处理高并发的 I/O 密集型任务。常见的应用场景包括:

  • Web 服务器和 API: 这是 Node.js 最经典的应用,用于构建网站后端、RESTful API 或 GraphQL API。
  • 实时应用: 例如在线聊天室、协作工具、游戏服务器等,利用 WebSockets 可以轻松实现实时数据交换。
  • 命令行工具 (CLI): 许多前端构建工具,如 webpack, Vite, ESLint 等,本身就是用 Node.js 开发的。
  • 微服务: Node.js 轻量且启动速度快,非常适合作为微服务架构中的一个服务节点。

c.c. 后端

一个网站可以分为前端 (Client-side)后端 (Server-side)

  • 静态网站: 只包含 HTML, CSS, JavaScript 文件。用户请求时,服务器直接将这些文件发送给浏览器。所有逻辑都在浏览器中发生。这种网站不需要后端(或者说只需要一个最简单的文件服务器)。
  • 动态网站: 需要根据用户请求、数据库中的数据或其他条件动态生成页面内容。例如,一个电商网站需要显示用户信息、订单历史,这些数据存储在服务器的数据库中。这就必须需要一个后端来处理业务逻辑。

后端的主要职责包括:

  1. 处理业务逻辑: 例如用户注册、登录验证、下订单等。
  2. 操作数据库: 对数据进行增、删、改、查。
  3. 提供 API: 给前端或其他服务提供数据接口。
  4. 身份验证与授权: 确保只有合法的用户才能访问特定资源。

2. 异步与事件驱动

Node.js 的核心哲学是 “异步、非阻塞 I/O”。这是它能够高效处理大量并发请求的关键。

a.a. 异步概念

  • 同步代码: 按顺序执行,一行执行完才能执行下一行。如果某一行是耗时操作(如读取大文件、网络请求),整个程序都会被阻塞
  • 异步代码: 发起一个耗时操作后,不等待结果,立即继续执行后续代码。当耗时操作完成后,通过回调函数 (Callback)、Promise 或 Async/Await 来处理结果。

例如,下面这个例子:

// 假设有一个耗时的数据库查询函数
db.query('SELECT * FROM users', function(result) {
  // 这是回调函数,在查询完成后才执行
  console.log('数据库查询完成!');
});

// 这行代码会立即执行,不会等待上面的查询
console.log('发起数据库查询...'); 

输出结果会是:

发起数据库查询...
数据库查询完成!

b.b. 事件循环

Node.js 通过**事件循环(Event Loop)**实现异步。

可以把事件循环想象成一个永不疲倦的调度员。它不断地检查一个叫做“任务队列”的地方,看看有没有已经完成的异步任务。

一个简化的模型如下:

  1. 调用栈 (Call Stack): 所有同步代码都在这里执行。当一个函数被调用,它被推入栈顶;执行完毕后,被弹出。
  2. Node APIs: 当遇到异步操作(如 fs.readFile, setTimeout)时,Node.js 会把它交给底层的 C++ API 处理,然后立即继续执行后面的同步代码。
  3. 任务队列 (Task Queue / Callback Queue): 当异步操作完成后(例如文件读完了,定时器到时间了),其对应的回调函数会被放入任务队列中排队。
  4. 事件循环 (Event Loop): 它的唯一工作就是持续监控调用栈。一旦调用栈变空(即所有同步代码都执行完了),它就会从任务队列里取出第一个回调函数,并将其推入调用栈中执行。

这个过程周而复始,永不停歇,因此得名“事件循环”。

3. 第一个 Node.js 程序

a.a. 运行 Node.js 脚本

  1. 创建一个文件,例如 app.js
  2. 在文件中写入 JavaScript 代码,例如 console.log('Hello, Node.js!');
  3. 打开终端,进入文件所在目录。
  4. 执行命令 node app.js

就可以在终端看到输出 Hello, Node.js!

b.b. “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 内置了许多功能强大的模块,无需安装即可直接使用。

a.a. 模块系统

Node.js 有两种模块系统:传统的 CommonJS 和现代的 ES Modules (ESM)

  1. 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
  1. ES Modules: 这是 JavaScript 官方标准。Node.js 也已全面支持。
    • 使用 import 关键字导入。
    • 使用 export 关键字导出。
    • 为了让 Node.js 将文件识别为 ES 模块,通常将文件后缀改为 .mjs,或在 package.json 中设置 "type": "module"

b.b. 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');
});

c.c. 文件系统模块

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)。

d.d. 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}`);

e.e. Events 模块

Node.js 的许多核心对象(如 HTTP 服务器、流)都是 EventEmitter 的实例。这个模块允许我们实现观察者模式,即创建、触发和监听自定义事件:

  1. 继承 EventEmitter: 让一个类拥有事件能力。
  2. 使用 .on(eventName, listener): 监听事件。
  3. 使用 .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!