Node和包管理

Node.js基础简介

Node.js 是一个基于 Chrome V8 JavaScript 引擎构建的开源、跨平台的 JavaScript 运行环境。它允许在服务器端执行 JavaScript,使得开发者可以使用同一种语言编写前后端代码,Node.js 以其非阻塞 I/O 和事件驱动的架构而闻名,特别适合于构建高并发、高性能的实时应用、API 服务器、微服务、工具脚本等。

特点与优势

  1. 异步非阻塞I/O:Node.js采用事件循环和回调函数,能高效处理并发请求,特别适合I/O密集型应用。
  2. 单线程模型:尽管JavaScript是单线程,但Node.js通过事件循环和异步处理,能够有效利用系统资源。
  3. 庞大的生态系统:npm(Node Package Manager)是世界上最大的软件注册表,提供了海量的开源库和工具。
  4. 跨平台:Node.js可在多种操作系统上运行,包括Windows、Linux和macOS。
  5. 轻量级:相比传统的Java、PHP等服务器端技术,Node.js启动速度快,资源消耗少。

运行JavaScript文件

  1. 创建文件:用文本编辑器创建一个名为 app.js 的文件。
  2. 编写代码:在 app.js 中输入简单的 JavaScript 代码,例如:
    javascript
    console.log("Hello, Node.js!");
  3. 运行代码:打开终端,进入该文件所在的目录,然后运行:
    bash
    node app.js
    你会在终端看到输出 “Hello, Node.js!”。

REPL环境

REPL(Read-Eval-Print Loop,读取-求值-打印-循环)是一种简单的交互式编程环境,允许用户输入代码片段,立即执行并查看结果,从而快速测试和探索编程语言的功能。

Node.js的REPL使用:

  1. 启动REPL:打开命令行工具,输入node然后回车,即可进入Node.js的REPL环境。命令行会显示一个提示符,通常是> 或者>,等待你输入JavaScript代码。

  2. 输入代码:在提示符后,你可以直接输入JavaScript表达式、声明、函数定义等,并回车执行。REPL会立刻执行你的代码并显示结果。

    • 例如,输入2 + 2并回车,REPL会输出4
  3. 变量与状态保持:REPL会维持一个上下文环境,意味着你在其中定义的变量和函数可以在后续的输入中继续使用。

  4. 多行输入:如果需要输入多行代码(如函数定义),可以使用分号;或直接按回车进入下一行继续输入,直到输入结束符(如})再回车执行。

  5. 历史记录:大多数REPL支持向上箭头浏览之前的输入历史,方便重复或修改之前的操作。

  6. 退出REPL:在Node.js的REPL中,输入.exit并回车,或者按下组合键Ctrl+C两次(在某些系统中),可以退出REPL环境。


事件驱动和非阻塞I/O

Node.js 的事件驱动模型

Node.js 的核心特性之一是其事件驱动的架构,这一模型主要依赖于事件循环(Event Loop)和回调函数(Callback)。事件驱动编程允许程序响应外部事件,而不是仅仅按照预定的顺序执行代码。在 Node.js 中,这一机制使得它能够高效地处理大量的并发请求,特别适合构建高吞吐量的网络应用。

  1. 事件循环:Node.js 的事件循环不断地检查是否有待处理的事件(比如网络请求、文件读写完成等),如果有,它就会取出对应的事件及其回调函数并执行。这种机制让 Node.js 能够在单个线程中处理多个并发任务,无需为每个请求创建新的线程或进程。

  2. 回调函数:当某个事件发生时,与之关联的回调函数会被放入事件队列等待执行。一旦当前正在执行的代码(包括之前的回调)完成,事件循环就会从队列中取出下一个回调执行。

  3. 异步I/O:Node.js 中的大多数I/O操作(如文件读写、网络通信)都是异步的,意味着它们不会阻塞主线程,而是在操作完成后通过回调通知事件循环。

非阻塞I/O

非阻塞I/O是事件驱动模型的基础。在传统的阻塞I/O模型中,当一个请求发出后,程序会暂停当前执行,直到请求完成并返回结果,这期间不能处理其他任务。而Node.js采用的非阻塞I/O则允许程序在等待I/O操作完成的同时继续执行其他任务。

  1. 如何提高性能:非阻塞I/O避免了线程在等待I/O操作时的空闲,充分利用CPU时间。在高并发场景下,由于Node.js使用单线程处理请求,没有线程上下文切换的开销,可以处理更多的并发连接,减少了内存消耗,提升了整体性能。

  2. libuv:Node.js 的底层依赖于 libuv 库来实现跨平台的异步I/O和事件循环机制。libuv负责管理一个线程池来处理实际的I/O操作,而主线程(事件循环所在的线程)则专注于执行JavaScript代码和调度事件。

事件循环处理并发请求

当Node.js接收到多个请求时,它并不会为每个请求分配一个单独的线程,而是将请求的处理逻辑(主要是I/O操作)注册为事件监听器,并立即返回,继续处理下一个请求。当某个I/O操作完成时,事件循环会触发相应的事件,执行与之关联的回调函数,完成请求处理。因此,即使在处理大量并发请求时,Node.js也能保持低延迟和高吞吐量,因为它总是准备好去处理下一个事件,而不是等待某个操作完成。

总结来说,尽管Node.js在处理I/O时可能会在LibUV层使用线程池,但这与为每个请求或任务创建新线程的概念不同。Node.js通过异步I/O和事件循环机制,在单个主线程中实现了对多个并发任务的高效处理,减少了线程创建和切换的开销,提高了应用的整体性能。


Node及包管理器

Node环境准备:一般而言直接下载安装Node.js即可

  • 在 Windows 或 macOS 上安装可访问 Node.js 官方网站 https://nodejs.org/,下载对应的安装包 安装即可。

  • Linux中则可以使用 sudo apt install nodejs npmsudo yum install nodejs npm 安装。

但是实际开发中通常会使用到不同版本的Node.js,因此需要一个包管理器来管理不同版本的Node.js和它们的包。

Node版本管理

  1. 管理不同版本的 Node.js :
    • nvm (Node Version Manager): 这是最流行和广泛使用的 Node.js 版本管理工具。它允许你在同一台机器上安装和切换多个 Node.js 版本。它是基于 shell 的。 info 总结
  • 对于 Node.js 版本管理: 考虑 Volta (因其项目固定特性) 或 fnm (推荐,因其速度和简洁性)。
  • 对于统一依赖缓存和高效包管理: 强烈推荐 pnpm。 :::

通过 Volta + pnpmfnm + pnpm 的组合,你可以在 Node.js 生态中获得与 python中 uv 相似的核心优势:高效的版本管理和卓越的依赖缓存/管理体验。


npm

npm(Node Package Manager) 是随 Node.js 一起安装的包管理器,它让开发者能够轻松地安装、管理和共享 Node.js 应用程序及其依赖关系。npm 提供了一个巨大的公开注册表,其中包含数百万个开源软件包,这些软件包可以通过简单的命令行操作进行安装和管理。

npm 的特点

  1. 包生态丰富:npm 是目前世界上最大的软件包生态系统,覆盖了从Web框架、数据库驱动到实用工具等各类库。
  2. 依赖管理:自动处理依赖关系,确保安装的包及其依赖版本兼容,通过 package.jsonpackage-lock.json 文件管理项目的依赖。
  3. 脚本执行:支持定义和执行自定义脚本,便于自动化构建、测试和部署任务。
  4. 版本控制:支持语义化版本控制,方便管理包的不同版本以及升级。
  5. 全球镜像:除了官方源,还有众多地区性镜像源可用,比如淘宝 NPM 镜像,可提高下载速度。

由于 Node.js 安装时会自动包含 npm,所以安装 Node.js 即安装了 npm。不过,有时候你可能需要更新 npm 到最新版本:

bash
npm install -g npm

为了加快下载速度,特别是对于中国用户,可以配置使用淘宝 NPM 镜像:

  1. 临时使用

    bash
    npm --registry=https://registry.npmmirror.com install <package-name>
  2. 永久配置

    • 在命令行设置(仅限当前用户):
      bash
      npm config set registry https://registry.npmmirror.com
    • 修改全局配置文件(所有用户): 找到 npm 的配置文件(通常是 ~/.npmrc/etc/npmrc),添加或修改 registry=https://registry.npmmirror.com。 即便使用nvm安装了多个版本的node,所有的版本都适用于此配置文件


npm常用命令

初始化项目

  • npm init: 创建一个新的 package.json 文件,引导你填写项目信息。使用 -y 跳过提示并接受默认值,快速创建:

    bash
    npm init -y

安装依赖

  • npm install <package>: 安装指定的包到当前项目的 node_modules 目录,并添加到 package.jsondependenciesdevDependencies(使用 --save-dev)。

  • npm inpm install: 无参数时,安装 package.json 中列出的所有依赖。

  • npm install --save: 安装的同时将包添加到 dependencies

  • npm install --save-dev: 安装开发依赖,添加到 devDependencies

更新依赖

  • npm update: 更新所有依赖到最新版本(不改变大版本号)。

  • npm update <package>: 更新指定包到最新版本。

卸载依赖

npm uninstall <package>: 卸载或删除一个或多个包。

  • 从项目的本地 node_modules 文件夹中移除指定的包。
  • 同时从 package.jsondependenciesdevDependencies 中移除指定的包。

如果要移除全局安装的包,可以使用 npm uninstall -g <package>

bash
npm rm -g pnpm         # npm uninstall 和 npm rm 作用相同

加上 -g 标志后,这些命令将应用于全局安装的包,即从整个系统范围内的全局位置移除指定的包

查看依赖

  • npm list: 列出当前项目安装的所有依赖包。

  • npm list --global: 查看全局安装的包。

脚本执行

  • npm run <script>: 执行 package.jsonscripts 部分定义的脚本。

发布包

  • npm login: 登录 npm 账户。

  • npm publish: 发布当前目录下的包到 npm 仓库。

其他常用命令

  • npm cache clean --force: 清理 npm 缓存。

  • npm view <package> versions: 查看包的所有版本。

  • npm view <package> dependencies: 查看包的依赖。

  • npm outdated: 检查哪些依赖包有新版本可用。

  • npm help <command>: 获取特定命令的帮助信息。

  • npm search <keyword>: 搜索 npm 仓库中的包。


npx

npx 是 npm 5.2.0 版本之后引入的一个命令行工具,它是 npm 包执行器。npx 的主要目的是为了简化执行 Node.js 包中的命令行工具的过程,特别是在不希望或不必全局安装这些工具的情况下。npx 的引入改善了开发者在使用 CLI 工具和其他托管在 npm 注册表上的可执行文件时的体验。

npx 与 npm 的关系紧密,可以视作 npm 的一部分或扩展功能。它们之间的主要区别包括:

  1. 临时性与局部性:npx 会在每次执行时临时下载(如果尚未存在)并执行所需的包,执行完毕后通常不会在系统中留下全局安装的痕迹。这有助于减少全局安装的包之间的冲突,并保持项目的环境纯净。

  2. 自动安装依赖:npx 会自动处理包的依赖关系,确保执行的命令或脚本可以在当前环境中正确运行,而不需要手动处理依赖安装。

  3. 命令查找与执行:npx 会智能地在本地 node_modules/.bin 目录、环境变量 $PATH 中查找命令,如果找不到,则直接从 npm 注册表下载并执行,使得执行命令变得简单直接。

  4. 版本控制:npx 支持指定执行命令的版本,这对于需要特定版本的工具来兼容项目的情况非常有用,避免了因全局安装版本不同而引发的问题。

总结来说,npx 是 npm 生态系统中的一个实用工具,它在不牺牲易用性的同时,增强了对包中命令行工具的管理和执行能力,特别适合于一次性任务或需要灵活版本控制的场景。

pnpm

pnpm 是一种快速且高效的 Node.js 包管理器,它通过一个全局的、内容可寻址的存储来统一管理所有依赖的实际文件,并在项目中通过硬链接和符号链接来构建 node_modules 目录,从而显著减少磁盘空间占用和提高安装速度

pnpm 的核心机制是 内容可寻址存储 (content-addressable store) 和 符号链接 (symlinks)(以及硬链接)

  • 所有通过 nvm 管理的 Node.js 版本共享一个全局的 .npmrc 文件,因此在此文件中设置的下载源适用于所有版本的 pnpm。每个项目可以有自己独立的 .npmrc 文件,覆盖全局设置,这种配置是隔离的,不随 Node.js 版本变化。
  • 通过环境变量(如 NPM_CONFIG_REGISTRY)设置的下载源也会影响所有 Node.js 版本下的 pnpm。

pnpm 除了使用 .npmrc 外,还可以使用 .pnpmfile.cjs 进行更高级的配置,比如自定义生命周期脚本。



pnpm 使用 pnpm-lock.yaml 文件来锁定依赖版本和描述依赖关系图,这与 npm 使用的 package-lock.json 类似,但格式和机制有所不同。


pnpm 与 npm 的异同和注意事项:

最佳实践是为一个项目选择一个包管理工具并坚持使用它,以避免上述潜在问题。如果决定从 npm 迁移到 pnpm,应该彻底地进行迁移,并且团队成员应统一使用 pnpm 来进行依赖管理。如果项目中已经存在 pnpm-lock.yaml,那么应完全使用 pnpm 进行依赖安装和管理,避免同时使用 npm 安装或更新依赖。如果需要回退到 npm,则应清理 pnpm 特有的文件(如删除 pnpm-lock.yamlnode_modules),然后使用 npm 重新安装依赖。


yarn

Yarn 是 Facebook 推出的一个快速、可靠、安全的依赖管理工具,与 npm 类似,用于 Node.js 项目中管理依赖包。下面是 Yarn 的一些基本使用方法和常用命令:

安装 Yarn

  • 通过 npm 安装:

    bash
    npm install -g yarn
  • 官方推荐安装方法(因地区网络环境差异,请访问 Yarn官网 获取最新安装指令):

    通常包括使用 curl 或者 wget 下载安装脚本直接安装。

初始化项目

  • yarn init: 生成一个新的 package.json 文件,与 npm init 类似,通过交互式提问收集项目信息。

安装依赖

  • yarn add <package>: 安装并添加依赖到 dependencies
  • yarn add <package> --dev: 添加到 devDependencies
  • yarn add <package>@<version>: 安装特定版本的包。
  • yarn: 或 yarn install, 安装 package.json 中列出的所有依赖。

更新依赖

  • yarn upgrade <package>: 更新指定包。
  • yarn upgrade <package>@<version>: 更新到指定版本。
  • yarn upgrade-interactive: 交互式地升级依赖。

移除依赖

  • yarn remove <package>: 从项目中移除依赖并更新 package.json

查看依赖

  • yarn list: 列出所有已安装的依赖。
  • yarn info <package>: 显示包的详细信息。

运行脚本

  • yarn run <script>: 执行 package.json 中定义的脚本。

配置文件

  • Yarn 使用 yarn.lock 文件来锁定依赖版本,确保每次安装时获得相同的依赖树。

总的来说,Yarn、npm 和 pnpm 都是强大的包管理工具,各有侧重。Yarn 强调速度和一致性,npm 是 Node.js 官方默认工具,生态丰富,而 pnpm 在空间效率和一致性方面表现出色。选择哪一个取决于个人或团队的具体需求和偏好。

项目依赖管理

package.json

在前端开发中,package.json文件是项目的核心配置文件,它不仅记录了项目的元数据,还管理着项目的依赖关系。下面是一个package.json示例:

json
{
  // 项目名称,应为唯一标识符
  "name": "my-front-end-project",
  
  // 项目版本,遵循语义化版本控制(SemVer)
  "version": "1.0.0",
  
  // 项目描述,简短介绍项目功能
  "description": "这是一个前端项目示例,展示依赖管理和配置",
  
  // 项目主入口文件,通常是服务器端或构建后的入口
  "main": "dist/index.js",
  
  // 脚本命令,简化日常开发任务
  "scripts": {
    // 启动开发服务器
    "start": "webpack serve --config webpack.dev.js",
    
    // 构建生产环境代码
    "build": "webpack --config webpack.prod.js",
    
    // 运行单元测试
    "test": "jest"
  },
  
  // 生产环境依赖,项目运行时所需
  "dependencies": {
    // React 库,用于构建用户界面
    "react": "^17.0.2",
    
    // React-DOM,React 的 DOM 版本,用于浏览器环境
    "react-dom": "^17.0.2"
  },
  
  // 开发环境依赖,仅用于开发和构建过程
  "devDependencies": {
    // Webpack,模块打包工具
    "webpack": "^5.52.1",
    
    // Webpack 的 CLI 工具,用于命令行操作
    "webpack-cli": "^4.8.0",
    
    // Jest,JavaScript 测试框架
    "jest": "^27.3.1",
    
    // ESLint,代码质量检查工具
    "eslint": "^7.32.0",
    
    // Babel,转译 ES6+ 代码为向后兼容版本
    "@babel/core": "^7.12.9",
    "@babel/preset-env": "^7.12.7",
    "@babel/preset-react": "^7.12.7"
  },
  
  // 关键字,用于提高包的可搜索性
  "keywords": [
    "frontend",
    "webpack",
    "react",
    "javascript"
  ],
  
  // 项目作者信息
  "author": "Your Name <[email protected]>",
  
  // 许可证声明
  "license": "MIT",
  
  // 项目仓库地址
  "repository": {
    "type": "git",
    "url": "https://github.com/your-username/my-front-end-project.git"
  },
  
  // 项目引擎要求,确保正确版本的 Node.js
  "engines": {
    "node": ">=12.0.0"
  }
}

package.json示例展示了如何定义项目的基本信息、配置脚本命令、管理生产与开发环境依赖,以及指定项目的一些附加元数据。完整的配置项参考官方文档:package.json

依赖包及版本号

在npm中,版本号遵循语义化版本控制(Semantic Versioning,简称semver) 格式为:主版本号.次版本号.补丁版本号(MAJOR.MINOR.PATCH)


dependenciesdevDependencies

  • dependencies: 这里列出的依赖项是在项目运行时所必需的。当应用部署到生产环境时,这些依赖会被安装。例如,像express这样的web框架或者axios用于发起HTTP请求的库,都是生产环境中需要的。

  • devDependencies: 这里列出的是开发和构建过程中需要的依赖,但在你的应用程序实际运行时并不需要。这包括诸如代码检查工具eslint、测试框架jest或打包工具webpack等。

==如果一个依赖仅在开发或构建过程中使用,则应放在devDependencies;如果应用在运行时也需要它,则应放入dependencies==。正确地分类依赖有助于保持项目的清晰度和可维护性,同时也优化了生产环境的依赖安装过程。

  • 添加到dependencies

    sh
    npm install <package-name> --save
    # 或者
    yarn add <package-name>
  • 添加到devDependencies

    sh
    npm install <package-name> --save-dev
    # 或者
    yarn add <package-name> --dev

npm执行脚本

在npm中,可以通过scripts字段在package.json文件中定义自定义脚本,这些脚本极大地便利了前端开发过程中的自动化任务,比如启动开发服务器、编译代码、运行测试等。

如何自定义脚本

  1. 打开package.json文件:找到或创建scripts对象,它是一个键值对集合,键是脚本名,值是执行的命令。

  2. 定义脚本:在scripts对象内添加新的键值对。值可以是shell命令或本地可执行文件的路径,也可以调用其他npm脚本。实际开发中通常是由框架或者脚手架生成的。

json
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "start": "react-scripts start", // 启动开发服务器(假设使用Create React App)
    "build": "react-scripts build", // 编译生产环境代码
    "test": "react-scripts test", // 运行测试
    "lint": "eslint './src/**/*.{js,jsx,ts,tsx}' --quiet", // 静默模式运行ESLint代码检查
    "precommit": "npm run lint && npm run test", // 在git commit前执行的脚本
    "deploy": "npm run build && gh-pages -d build" // 部署到GitHub Pages
  },
  // 其他配置...
}

如何使用自定义脚本

  • 命令行执行:在项目根目录下,使用npm run <script-name>, pnpm <script-name>yarn <script-name>来执行脚本。例如,npm run start会启动开发服务器。
  • 省略run:pnpm和yarn均可以不添加 run , npm则建议使用 npm run xx命令(虽然部分情况下,如脚本名是start等时也可以省略run)

自定义脚本极大地提高了开发效率,减少了重复劳动,让开发者能更专注于编写业务逻辑。通过组合不同的命令和工具,你可以根据项目需求灵活定制适合的脚本。

模块系统与核心模块

Node模块系统

Node.js的模块系统是其强大功能之一,它允许开发者将代码组织成独立的、可重用的单元。Node.js支持两种主要的模块规范:CommonJS 和 ES模块(ESM)

CommonJS模块规范

定义:CommonJS是一种用于服务器端JavaScript的模块规范,它在Node.js中被广泛采用。在CommonJS中,每个.js文件都被视为一个独立的模块,有自己的作用域。

导入模块:使用require()函数来导入模块。require()接收模块路径作为参数,并返回模块导出的对象。

javascript
const fs = require('fs'); // 导入Node.js的文件系统模块

导出模块:模块可以通过module.exportsexports对象来导出功能或数据给外部使用。module.exports是模块的默认导出对象,而exportsmodule.exports的一个引用,通常用于简化导出操作。

javascript
// 导出一个函数
exports.sayHello = function() {
  console.log('Hello, World!');
};

// 或者直接修改module.exports
module.exports = {
  greet: function(name) {
    console.log(`Hello, ${name}!`);
  }
};

注意require()是同步操作,且模块在首次加载时只执行一次,之后的require()调用将直接从缓存中获取模块。

ES模块(ESM)支持

定义:ES模块是ECMAScript标准的一部分,从ES6开始引入,它提供了原生的模块导入和导出语法,支持动态导入、顶级await等特性。

导入模块:使用import关键字导入模块。

javascript
import { sayHello } from './hello.mjs'; // 导入指定的导出项
import * as hello from './hello.mjs'; // 导入所有导出项作为一个对象
import hello from './hello.mjs'; // 默认导出

导出模块:使用export关键字导出模块的成员。

javascript
// 导出一个函数
export function sayHello() {
  console.log('Hello, World!');
}

// 默认导出
export default function greet(name) {
  console.log(`Hello, ${name}!`);
}

文件扩展名:ES模块通常使用.mjs作为文件扩展名,而CommonJS模块使用.js。不过,Node.js也允许通过配置使用.js作为ES模块的扩展名。

异步加载:与CommonJS不同,ES模块的导入是异步的,这有助于避免加载时的阻塞。

Node核心模块

Node.js 的模块系统是其设计的核心特性之一,它允许开发者将代码组织成独立的模块,便于复用和维护。Node.js 的模块系统遵循“CommonJS”规范,这使得每个模块都有自己的作用域,避免了全局变量的污染。

Node.js 模块的分类

  1. 核心模块:这些模块是Node.js的一部分,直接由Node.js提供,无需安装即可使用。例如fs(文件系统)、http(HTTP服务器)、path(路径处理)、os(操作系统相关的实用功能)等。

  2. 文件模块:当导入一个以.js.json或编译后的.node(C++扩展)结尾的本地文件时,Node.js会将其视为文件模块。文件模块的路径可以是相对的或绝对的。

  3. 第三方模块:这些是通过npm(Node.js包管理器)安装的外部库。使用前需要先通过npm install命令安装。

示例1:使用fs模块读取文件

javascript
// 引入fs模块
const fs = require('fs');

// 异步读取文件
fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
  } else {
    console.log(data);
  }
});

示例2:使用http模块创建一个简单的HTTP服务器

javascript
const http = require('http');

// 根据req.url判断客户端请求的路径,返回不同的响应内容
const server = http.createServer((req, res) => {
  // 可以使用 req.method 判断请求方式,进行不同的处理
  // if (req.method === 'POST' && req.url === '/submit') { ... }  
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');

  if (req.url === '/') {
    res.statusCode = 200;
    res.end('欢迎来到首页\n');
  } else if (req.url === '/about') {
    res.statusCode = 200;
    res.end('关于我们的信息...\n');
  } else {
    res.statusCode = 404;
    res.end('页面未找到\n');
  }
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});