公司动态
Node.js入门教程(二):Node.js 基础概念
一、Node.js 是什么Node.js 是一个基于 Chrome V8 JavaScript 引擎构建的JavaScript 运行时环境。简单来说Node.js 让 JavaScript 可以在服务器端运行而不仅仅局限于浏览器中。二、Node.js 的核心特点特点说明单线程事件循环使用单线程的事件循环模型通过事件驱动和回调函数处理并发请求避免了传统多线程编程中的线程切换开销非阻塞 I/O所有 I/O 操作文件读写、网络请求等都是异步的。当等待 I/O 操作完成时程序不会被阻塞可以继续处理其他任务大大提高了应用程序的吞吐量跨平台支持 Windows、macOS、Linux 等多种操作系统一次编写到处运行丰富的生态系统npmNode Package Manager拥有数百万个开源包活跃的开发者社区三、与传统服务器端技术的区别1、传统服务器端技术如 Apache PHP的处理方式请求1 → 创建线程1 → 处理请求 → 返回响应 → 销毁线程1 请求2 → 创建线程2 → 处理请求 → 返回响应 → 销毁线程2 请求3 → 创建线程3 → 处理请求 → 返回响应 → 销毁线程32、Node.js 的处理方式请求1 → 事件循环 → 处理请求 → 返回响应 请求2 → 事件循环 → 处理请求 → 返回响应 复用同一线程 请求3 → 事件循环 → 处理请求 → 返回响应 复用同一线程特性传统多线程模型Node.js 单线程模型内存占用每个线程占用 2MB 左右单线程内存占用少并发处理线程数量限制并发数事件循环处理高并发上下文切换频繁的线程切换开销无线程切换开销编程复杂度需要处理线程同步避免了锁和线程同步问题适用场景CPU 密集型任务I/O 密集型任务四、Node.js 的应用场景1、适合的应用场景Web 应用程序RESTful API 服务、单页应用SPA的后端服务实时 Web 应用聊天应用、在线游戏、协作工具如在线文档编辑微服务架构轻量级的微服务、API 网关、服务间通信工具和命令行应用构建工具如 Webpack、Gulp、脚手架工具、自动化脚本物联网IoT应用设备数据收集、传感器数据处理2、不适合的应用场景CPU 密集型任务图像/视频处理、复杂的数学计算、大数据分析需要大量计算的应用机器学习训练、科学计算、加密货币挖矿五、事件驱动和非阻塞 I/O 模型传统阻塞 I/O 模型示例// 伪代码 - 阻塞式操作 // 每次 readFileSync 都会卡住程序等待文件读取完成后才继续往下执行 const data1 readFileSync(file1.txt); // 程序在此暂停等待读取完成 const data2 readFileSync(file2.txt); // data1 读完后再暂停等待 const data3 readFileSync(file3.txt); // data2 读完后再暂停等待 console.log(所有文件读取完成); // 三个文件全部读完后才执行到这里 // 总耗时 读取file1的时间 读取file2的时间 读取file3的时间Node.js 非阻塞 I/O 模型示例// Node.js 异步操作 const fs require(fs); // 三个读文件的操作几乎同时发出程序不会等待任何一个完成 fs.readFile(file1.txt, (err, data1) { // 这个回调函数会在 file1.txt 读取完成后自动被调用 console.log(文件1读取完成); }); fs.readFile(file2.txt, (err, data2) { console.log(文件2读取完成); }); fs.readFile(file3.txt, (err, data3) { console.log(文件3读取完成); }); // 注意这行代码会最先执行因为上面三个读文件操作是异步的 // 发出读取请求后程序立刻继续往下走不会等待文件读取完成 console.log(程序继续执行不等待文件读取); // 实际输出顺序示例 // 程序继续执行不等待文件读取 ← 最先打印 // 文件2读取完成 ← 哪个文件先读完就先打印顺序不固定 // 文件1读取完成 // 文件3读取完成 // 总耗时 ≈ 三个文件中读取最慢的那一个的时间并发执行而非顺序叠加六、事件循环机制事件循环是 Node.js 实现非阻塞 I/O 的核心机制。它持续运行不断检查是否有待执行的任务从而实现单线程处理并发的效果调用栈Call Stack执行同步代码函数调用时入栈执行完毕后出栈事件队列Event Queue存储异步操作完成后的回调函数等待调用栈空闲时依次执行事件循环Event Loop持续监控调用栈和事件队列一旦调用栈为空就将事件队列中的回调取出放入调用栈执行事件循环按照固定顺序依次处理以下几个阶段┌───────────────────────────┐ ┌─│ timers │ ← 执行 setTimeout、setInterval 的到期回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ ← 执行上一轮循环中延迟的 I/O 回调如某些系统错误回调 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ ← Node.js 内部使用开发者一般不需要关心 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ poll │ ← 核心阶段获取新的 I/O 事件并执行其回调如文件读取、网络请求完成 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ check │ ← 执行 setImmediate 的回调在 poll 阶段之后立即执行 │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ ← 执行关闭事件的回调如 socket.on(close, ...) └───────────────────────────┘对于初学者来说最需要记住的是同步代码总是优先执行异步回调如文件读取完成的处理会在调用栈清空后才被执行。七、JavaScript 运行时环境V8 引擎简介V8 是 Google 开发的高性能 JavaScript 引擎也是 Chrome 浏览器的核心组件。Node.js 使用 V8 引擎来执行 JavaScript 代码。V8 引擎的特点即时编译JIT将 JavaScript 代码直接编译为机器码无需中间字节码执行效率更高垃圾收集自动内存管理开发者无需手动释放内存。使用分代垃圾收集算法将内存分为新生代和老生代分别管理提升回收效率优化技术内联缓存Inline Caching缓存对象属性的查找结果隐藏类Hidden Classes将动态语言的对象结构静态化动态优化分析热点代码并进行针对性优化浏览器 JavaScript vs Node.js JavaScript虽然都使用 JavaScript 语言但运行环境的差异导致了一些重要区别相同点都使用相同的 JavaScript 语法都支持 ES6 特性都使用 V8 引擎Chrome 浏览器不同点特性浏览器 JavaScriptNode.js JavaScript全局对象windowglobal模块系统ES6 modules, AMDCommonJS, ES6 modules文件系统访问不可访问出于安全限制完全访问网络请求XMLHttpRequest, Fetchhttp, https 模块DOM 操作支持操作页面元素不支持服务器端无页面进程控制不支持支持可读取环境变量、退出进程等浏览器环境示例// 浏览器中的全局对象和 DOM 操作 console.log(window); // 浏览器全局对象包含所有浏览器 API document.getElementById(app); // 通过 ID 获取页面元素Node.js 中不可用 localStorage.setItem(key, value); // 浏览器本地存储Node.js 中不可用Node.js 环境示例// Node.js 中的全局对象和文件操作 console.log(global); // Node.js 全局对象对应浏览器中的 window const fs require(fs); // 引入内置文件系统模块浏览器中不可用 fs.readFile(data.txt, utf8, callback); // 读取服务器本地文件浏览器中不可用全局对象的差异浏览器中的全局对象// 浏览器环境 console.log(this window); // true顶层 this 就是 window 对象 var globalVar hello; console.log(window.globalVar); // hello用 var 声明的变量会成为 window 的属性Node.js 中的全局对象// Node.js 环境 // 注意Node.js 中每个文件都是一个独立的模块模块内的 this 不等于 global console.log(this global); // false在模块作用域中this 指向 module.exports var globalVar hello; console.log(global.globalVar); // undefined模块内的变量不会自动挂载到 global // Node.js 中的模块作用域 console.log(this); // {} 空对象即 module.exports 的初始值 console.log(module.exports this); // true模块内 this 指向 module.exportsNode.js 特有的全局变量console.log(__dirname); // 当前模块所在目录的绝对路径例如/home/user/project console.log(__filename); // 当前模块文件的绝对路径例如/home/user/project/app.js console.log(process); // 进程对象可读取命令行参数process.argv、环境变量process.env等 console.log(Buffer); // 用于处理二进制数据的构造函数常用于文件读写和网络通信八、Node.js 的优势与局限优势详解1. 高并发处理能力传统多线程服务器处理 10,000 个并发连接需要约 20GB 内存每个线程约 2MB而 Node.js 只需要很少的内存就能处理相同数量的连接。// Node.js 高并发示例 const http require(http); const server http.createServer((req, res) { // 模拟异步操作如查询数据库100ms 后返回响应 // 在这 100ms 内事件循环可以继续处理其他请求不会被阻塞 setTimeout(() { res.writeHead(200, {Content-Type: text/plain}); res.end(Hello World\n); }, 100); }); server.listen(3000, () { console.log(服务器运行在 http://localhost:3000/); }); // 得益于非阻塞 I/O 和事件循环机制这个服务器可以同时处理数千个请求而不会阻塞2. 快速开发统一的 JavaScript 语言栈前端和后端都用 JavaScript使得前后端开发更加高效。开发者只需掌握一门语言就可以同时编写前端和后端代码并且可以在两端之间共享代码逻辑// 同一份验证逻辑可以在前后端复用无需重复编写 function validateEmail(email) { const regex /^[^\s][^\s]\.[^\s]$/; return regex.test(email); } // 前端使用浏览器中运行用户提交表单时先在前端校验避免无效请求发到服务器 if (validateEmail(userInput)) { // 验证通过发送到服务器 } // 后端使用Node.js 中运行服务器再次校验防止绕过前端直接请求接口 if (validateEmail(req.body.email)) { // 验证通过保存到数据库 }3. 统一语言栈的优势代码复用前后端可以共享工具函数、数据验证逻辑、常量定义等团队效率开发人员可以同时负责前后端开发减少沟通成本技术栈简化只需掌握 JavaScript 一门语言降低学习成本和技术复杂度4. 丰富的 npm 生态系统# npm 提供了数百万个开源包几乎任何功能都能找到现成的库 npm search express # 搜索名为 express 的包 npm install express # 安装 express 包到当前项目 npm update # 更新当前项目中所有包到最新兼容版本局限性分析1. CPU 密集型任务的性能问题// 不好的例子CPU 密集型任务会阻塞事件循环 // fibonacciSync 是纯计算任务会一直占用 CPU期间事件循环无法处理其他请求 function fibonacciSync(n) { if (n 2) return n; return fibonacciSync(n - 1) fibonacciSync(n - 2); } // 这会阻塞整个应用长达数秒期间所有请求都无法响应 console.log(fibonacciSync(40)); // 解决方案使用 Worker Threads 将 CPU 密集型任务放到独立线程中执行 // 主线程的事件循环就不会被阻塞 const { Worker, isMainThread, parentPort, workerData } require(worker_threads); if (isMainThread) { // 主线程创建一个 Worker并将参数 { n: 40 } 传给它 // Worker 会在独立线程中执行当前这个文件__filename 指当前文件路径 const worker new Worker(__filename, { workerData: { n: 40 } }); // 监听 Worker 发来的消息计算结果 worker.on(message, (result) { console.log(结果:, result); // Worker 计算完成后主线程在这里接收结果 }); } else { // Worker 线程通过 workerData 获取主线程传入的参数执行计算后发送结果 const result fibonacciSync(workerData.n); parentPort.postMessage(result); // 将结果发送回主线程 }2. 回调地狱问题// 回调地狱示例多个异步操作需要顺序执行时回调会不断嵌套代码可读性极差 fs.readFile(file1.txt, (err, data1) { if (err) throw err; fs.readFile(file2.txt, (err, data2) { // 嵌套第一层 if (err) throw err; fs.readFile(file3.txt, (err, data3) { // 嵌套第二层 if (err) throw err; // 实际项目中这种嵌套可能有十几层严重影响代码维护性 console.log(所有文件读取完成); }); }); }); // 现代解决方案使用 Promise async/await让异步代码写起来像同步代码一样直观 const fsPromises require(fs).promises; async function readFiles() { try { // await 会等待每个文件读取完成但整个函数不会阻塞事件循环 // 注意这里是顺序读取如需并发读取多个文件可以使用 Promise.all() const data1 await fsPromises.readFile(file1.txt); const data2 await fsPromises.readFile(file2.txt); const data3 await fsPromises.readFile(file3.txt); console.log(所有文件读取完成); } catch (err) { // try/catch 可以统一处理所有异步错误不需要在每个回调里单独判断 console.error(读取文件失败:, err); } }3. 单线程的脆弱性// 未捕获的异常会导致整个 Node.js 进程崩溃 // 与多线程不同单线程中一个未处理的错误会让整个服务停止 setTimeout(() { throw new Error(未捕获的异常); // 这会导致整个应用崩溃所有请求都无法响应 }, 1000); // 解决方案注册全局异常处理器在应用崩溃前记录日志并优雅地退出 process.on(uncaughtException, (err) { console.error(未捕获的异常:, err); // 此处应先记录错误日志再退出避免静默崩溃导致问题难以排查 process.exit(1); // 退出码 1 表示异常退出 }); process.on(unhandledRejection, (reason, promise) { // 当 Promise 被 reject 但没有 .catch() 处理时触发 console.error(未处理的 Promise 拒绝:, reason); process.exit(1); });九、适用场景分析1、RESTful API 服务// 使用 Express 框架搭建 RESTful API 服务 // 首先需要安装 expressnpm install express const express require(express); const app express(); app.use(express.json()); // 中间件自动解析请求体中的 JSON 数据 // GET 请求获取用户列表对应 HTTP 方法 GET路径 /api/users app.get(/api/users, (req, res) { const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]; res.json(users); // 以 JSON 格式返回数据 }); // POST 请求创建新用户客户端在请求体中传入用户信息 app.post(/api/users, (req, res) { const newUser req.body; // 获取客户端发来的 JSON 数据 // 实际项目中这里通常会将用户信息保存到数据库 res.status(201).json({ message: 用户创建成功, user: newUser }); }); // PUT 请求更新指定用户信息:id 是路由参数如 /api/users/1 表示更新 ID 为 1 的用户 app.put(/api/users/:id, (req, res) { const { id } req.params; // 从路径中获取用户 ID const updatedData req.body; // 从请求体中获取更新内容 res.json({ message: 用户 ${id} 更新成功, data: updatedData }); }); // DELETE 请求删除指定用户 app.delete(/api/users/:id, (req, res) { const { id } req.params; res.json({ message: 用户 ${id} 删除成功 }); }); app.listen(3000, () { console.log(API 服务器运行在 http://localhost:3000/); });2、实时应用// WebSocket 实时聊天使用 socket.io 库 // 首先需要安装npm install socket.io const io require(socket.io)(server); // 监听客户端连接事件每有新用户连接就触发一次 io.on(connection, (socket) { // 监听当前连接的客户端发来的 chat message 事件 socket.on(chat message, (msg) { io.emit(chat message, msg); // 将消息广播给所有在线客户端包括发送者 }); });3、中间件和代理服务// HTTP 反向代理服务器示例 // 将前端发来的 /api/xxx 请求转发到后端真实 API 服务器隐藏真实地址常用于解决跨域问题 // 首先需要安装npm install http-proxy-middleware const httpProxy require(http-proxy-middleware); const proxy httpProxy({ target: http://api.example.com, // 真实后端 API 服务器地址 changeOrigin: true, // 修改请求头中的 Host 为 target 地址 pathRewrite: { ^/api: // 将路径中的 /api 前缀去掉再转发例如 /api/users → /users } });十、本章小结知识点说明Node.js 是什么基于 Chrome V8 引擎的 JavaScript 运行时环境单线程事件循环单线程 事件驱动避免了线程切换开销非阻塞 I/O异步 I/O 操作提高吞吐量适用场景I/O 密集型应用如 API 服务、实时应用、微服务不适用场景CPU 密集型任务如图像处理、机器学习V8 引擎即时编译JIT、垃圾收集、隐藏类等优化技术全局对象差异浏览器有 windowNode.js 有 globalNode.js 独有 __dirname、__filename、process