Skip to content

S08-03 Node-包管理工具

[TOC]

概述

代码共享方案

代码共享需求:我们已经学习了在 JavaScript 中可以通过模块化的方式将代码划分成一个个小的结构:

  • 在以后的开发中我们就可以通过模块化的方式来封装自己的代码,并且封装成一个工具;
  • 这个工具我们可以让同事通过导入的方式来使用,甚至你可以分享给世界各地的程序员来使用;

代码共享方案

如果我们分享给世界上所有的程序员使用,有哪些方式呢?

  1. 方式一:上传 GitHub/官网发布

    上传到 GitHub 上、其他程序员通过 GitHub 下载我们的代码手动的引用;

    缺点

    • 必须知晓地址并手动下载:大家必须知道你的代码 GitHub 的地址,并且从 GitHub 上手动下载;

    • 手动引入并关联依赖

      需要在自己的项目中手动的引用,并且管理相关的依赖;

      不需要使用的时候,需要手动来删除相关的依赖;

      当遇到版本升级或者切换时,需要重复上面的操作;

    显然,上面的方式是有效的,但是这种传统的方式非常麻烦,并且容易出错

  2. 方式二:包管理工具

    使用一个专业的工具来管理我们的代码,

    • 发布:我们通过工具将代码发布到特定的位置;

    • 安装:其他程序员直接通过工具来安装、升级、删除我们的工具代码;

    显然,通过第二种方式我们可以更好的管理自己的工具包,其他人也可以更好的使用我们的工具包。

npm

npm 介绍

npm(Node Package Manager,Node 包管理器):是 Node.js 官方内置的包管理工具,也是 JavaScript 生态中最成熟、使用最广泛的包管理系统。它不仅用于管理项目依赖(如 React、Vue、Express 等),还提供了包发布版本控制脚本运行等核心功能,是 Node.js 开发的基础工具之一。


下载/安装 npm:安装 Node.js 时会自动附带 npm(无需单独安装),开箱即用


npm 搜索/安装包


包存放位置:npm registry

  • 发布包:我们发布自己的包其实是发布到 registry 上面的;
  • 安装包:当我们安装一个包时其实是从 registry 上面下载的包;

package.json

概述

事实上,我们每一个项目都会有一个对应的配置文件,无论是前端项目还是后端项目:

  • 这个配置文件会记录着你项目的名称、版本号、项目描述等;
  • 也会记录着你项目所依赖的其他库的信息和依赖库的版本号;

这个配置文件在 Node 环境下面(无论是前端还是后端)就是 package.json。

package.json:是 npm 管理的项目中最核心的配置文件,位于项目根目录,用于描述项目的元信息依赖关系脚本命令等关键信息,是 npm 进行依赖管理、脚本执行、包发布的基础。

初始化 package.json

创建方式

我们可以创建一个 package.json 文件:

  • 手动创建

    在项目根目录新建 package.json 并填写字段。

  • 自动生成

    执行 npm init,通过交互问答生成(加 -y 可跳过交互,直接生成默认配置):

    bash
    npm init -y  # 快速生成基础 package.json

    生成结果:

    json
    {
      "name": "learn-npm",
      "version": "1.0.0",
      "description": "",
      "main": "main.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }

示例:Vue CLI4 / Vue CLI2 配置

Vue CLI4 对脚手架创建的项目进行了简化,CLI2 创建的项目更加详细:

json
{
  "name": "my-vue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^2.6.11"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },
  "browserslist": ["> 1%", "last 2 versions", "not dead"]
}
json
{
  "name": "vuerouterbasic",
  "version": "1.0.0",
  "description": "A Vue.js project",
  "author": "'coderwhy' <'coderwhy@gmail.com'>",
  "private": true,
  "scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "build": "node build/build.js"
  },
  "dependencies": {
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "babel-core": "^6.22.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "chalk": "^2.0.1",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^2.30.1",
    "node-notifier": "^5.1.2",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "postcss-url": "^7.2.1",
    "rimraf": "^2.6.0",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "uglifyjs-webpack-plugin": "^1.1.1",
    "url-loader": "^0.5.8",
    "vue-loader": "^13.3.0",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.5.2",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-dev-server": "^2.9.1",
    "webpack-merge": "^4.1.0"
  },
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": ["> 1%", "last 2 versions", "not ie <= 8"]
}

字段解析

核心字段

package.json 由一系列键值对组成,以下是最常用的核心字段:

基本元信息字段

  • name必填项目名称。发布到 npm 时必须唯一,小写,可包含 -_
  • version必填版本号,必须遵循 语义化版本
  • description项目描述
  • author作者信息(格式:姓名 <邮箱> (个人主页),或仅姓名)。
  • license开源协议(如 MITApache-2.0,说明用户如何使用此项目)。
  • private:布尔值,项目是否私有。设为 true 时禁止将项目发布到 npm 仓库。
  • main项目入口文件(默认 index.js,当其他包引入当前包时,会加载此文件)。

依赖管理字段

  • dependencies生产环境依赖,项目运行时必须的包,会被打包到最终产物中。
  • devDependencies开发环境依赖,仅开发时需要的包,不会被打包到生产环境,安装时需加 -D
  • peerDependencies对等依赖,声明当前包与其他包的兼容版本(常用于插件开发)。

脚本命令字段

  • scripts自定义脚本命令,通过 npm run <脚本名> 执行,用于启动、测试、打包等。

生命周期钩子

  • pre<脚本名>:在目标脚本执行前运行。
  • post<脚本名>:在目标脚本执行后运行。

其他重要字段

  • repository:项目代码仓库地址(如 GitHub 链接),方便他人查看源码。
  • bugs问题反馈地址(如 GitHub Issues)。
  • homepage项目主页(如官网、文档地址)。
  • engines:指定项目运行所需的 Node.js 或 npm 版本(避免环境不兼容)。
  • browserslist:声明前端项目兼容的浏览器范围(供 babel、autoprefixer 等工具使用)。
main

main项目入口文件(默认 index.js,当其他包引入当前包时,会加载此文件)。


包查找流程(以axios为例)

  1. 在项目中使用 require() 引入 axios

    js
    const axios = require('axios')
  2. 在 node_modules/axios 目录中找到 package.json 配置文件的 main 字段,找到 axios 的入口文件

  3. 访问找到的入口文件

image-20240719155238845

scripts

scripts自定义脚本命令,通过 npm run <脚本名> 执行,用于启动、测试、打包等。

pre<脚本名>:在目标脚本执行前运行。

post<脚本名>:在目标脚本执行后运行。


示例:配置脚本 npm run start

  1. 自定义脚本:node 在终端运行 js 脚本是通过以下命令:

    sh
    node ./src/main.js
  2. 配置脚本:我们可以在 package.json 的 scripts 字段添加脚本命令:

    json
    'scripts': {
      'start': 'node ./src/main.js'
    }
  3. 运行脚本:配置完成后,我们可以通过npm run <key>以下命令来执行

    省略 run:常用的 start / test / stop / restart 可以省略掉 run

    sh
    npm run start
    
    # 省略 run 
    npm start
dependencies

dependencies生产环境依赖,项目运行时必须的包,会被打包到最终产物中。


项目分享流程

  1. 记录安装过的包

    在项目开发过程中每次执行npm i <包>命令安装包时,都会在dependenciesdevDependencies 中记录安装的包名和版本信息。

  2. 分享项目

    1. 由于 node_modules 目录中安装了很多包,造成了它体积会非常大,文件非常多。直接复制分享很耗时。

    2. 因此在日常分享项目时,我们会先删除 node_modules ,再分享项目。

  3. 接收并重新安装项目

    其他人下载好项目后,直接执行 npm i 命令可以根据 dependencies 和 devDependencies 字段的记录重新安装对应版本的包。

devDependencies

devDependencies开发环境依赖,仅开发时需要的包,不会被打包到生产环境,安装时需加 -D


一些包在生产环境是不需要的(如 webpack、babel 等),此时我们会通过以下命令将它安装到 devDependencies 属性中;

sh
npm install webpack --save-dev
  • --save简写:-S默认省略,用于将依赖包信息加入到生产环境依赖 dependencies 中。
  • --save-dev简写:-D,用于将依赖包信息加入到开发环境依赖 devDependencies 中。
peerDependencies

peerDependencies对等依赖,声明当前包与其他包的兼容版本(常用于插件开发)。


对等依赖:用于声明当前包与其他核心包的兼容版本范围,并要求使用当前包的开发者(宿主项目)手动安装符合范围的核心包。


为什么需要 peerDependencies

想象一个场景:你开发了一个 React 插件(如 react-plugin),这个插件必须依赖 React 才能运行。如果用普通的 dependencies 声明依赖,会导致:

  • 插件安装时,npm 会自动下载 React 到插件自己的 node_modules 目录。 宿主项目(使用插件的项目)本身也会安装 React,最终可能出现多个 React 版本共存(项目的 React + 插件自带的 React)。
  • 这会引发严重问题(如 React 上下文冲突、组件复用失败等)。

peerDependencies 正是为解决这类问题而生:它不自动安装依赖,而是要求宿主项目必须安装符合版本范围的核心包,插件直接使用宿主项目已安装的版本,避免重复和冲突。

package-lock.json

概述

package-lock.json:是 npm 5.0 及以上版本自动生成的核心配置文件,与 package.json 配合工作,其核心使命是精确锁定项目所有依赖的版本、下载来源和依赖关系,确保同一项目在不同环境(如开发机、测试服务器、生产服务器)执行 npm install 时,安装的依赖完全一致,从根本上解决 “版本不一致导致的兼容性问题”。


为什么需要 package-lock.json

在没有 package-lock.json 的时代,package.json 中依赖的版本通常是 “范围描述”(如 ^1.2.3 表示允许 1.x.x 范围内的更新)。这会导致:

  • 不同时间、不同机器执行 npm install 时,可能安装不同版本的依赖(例如今天安装 1.2.3,明天可能因仓库更新安装 1.3.0)。
  • 版本差异可能引发隐性问题(如功能异常、报错),尤其是大型项目中依赖链复杂时,排查难度极大。

生成与更新机制

  1. 首次生成

    当项目中没有 package-lock.json 时,执行 npm install自动生成该文件,完整记录当前安装的所有依赖(包括直接依赖、间接依赖)的详细信息。

  2. 自动更新

    当通过 npm install <包名>npm update <包名>npm uninstall <包名> 等命令修改依赖时,npm 会自动同步更新 package-lock.json,确保其始终反映最新的依赖状态。

  3. 强制遵循

    package-lock.json 已存在,执行 npm install 时,npm 会忽略 package.json 中的版本范围,直接根据 package-lock.json 安装精确版本(除非手动修改了 package.json 中的依赖版本范围,此时会重新解析并更新锁定文件)。


注意事项

  1. 禁止手动修改

    package-lock.json 由 npm 自动生成和更新,手动修改可能导致依赖树错乱,引发安装失败或版本不一致。

  2. 必须提交到代码仓库

    团队协作或 CI/CD 部署时,需将 package-lock.json 纳入 Git 等版本控制工具,确保所有开发者和环境使用完全相同的依赖。

  3. 删除的风险

    若删除 package-lock.json,再次执行 npm install 会重新生成它,但可能根据 package.json 的版本范围安装新的版本(与之前可能不同),可能引入兼容性问题

  4. 与其他包管理工具的兼容性

    package-lock.json 是 npm 专属文件,yarn 对应 yarn.lock,pnpm 对应 pnpm-lock.yaml,混用不同工具可能导致锁定文件冲突,建议项目中统一使用一种包管理工具。

核心字段

package-lock.json 核心字段围绕 “精确记录依赖信息” 设计。

  • version:依赖的精确版本号(如 4.18.2)。

  • resolved:依赖包的官方下载地址(从 npm 仓库获取的 .tgz 压缩包地址)。

  • integrity:通过 SHA-512 等哈希算法生成的校验值,用于验证下载的包是否完整、未被篡改

  • requires间接依赖,记录当前依赖所依赖的其他包及其版本范围。

  • lockfileVersion标识锁定文件的格式版本(不同 npm 版本生成的格式可能不同,如 npm 7+ 对应 3)。

json
{
  "name": "my-project",       // 项目名称(与 package.json 一致)
  "version": "1.0.0",         // 项目版本(与 package.json 一致)
  "lockfileVersion": 3,       // lock 文件格式版本(npm 7+ 对应 3)
  "requires": true,           // 表示依赖树需被严格遵循(默认 true)
  "dependencies": {           // 所有依赖的详细信息(核心部分)
    "express": {              // 依赖包名
      "version": "4.18.2",    // 精确安装版本(非范围,如 4.18.2)
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",  // 下载地址(.tgz 包)
      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",  // 哈希校验值
      "requires": {           // 该依赖自身的依赖(间接依赖),有时是 dependencies
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1"
        // ... 其他间接依赖
      }
    },
    "lodash": {
      "version": "4.17.21",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
    }
    // ... 其他依赖
  }
}

SemVer 版本规范

SemVer 介绍@

SemVer 官网:https://semver.org/lang/zh-CN/

我们会发现安装的依赖版本出现:^2.0.3~2.0.3,这是什么意思呢?

SemVer(Semantic Versioning,语义化版本规范 ):是一套用于规范软件版本号命名的标准,让开发者和用户能通过版本号直观了解软件的更新性质(如是否兼容、是否新增功能、是否修复 bug 等),从而简化依赖管理和版本控制。

npm 的包通常遵循 SemVer 规范,并通过版本范围符号定义依赖的允许更新范围,最终通过 package-lock.json 实现精确版本锁定。


SemVer 的核心格式

遵循 SemVer 的版本号由三部分组成,格式为:X.Y.Z,分别代表:

  • X 主版本号(Major):当软件发生不兼容的 API 变更(升级后可能导致现有代码报错)时递增。
  • Y 次版本号(Minor):当软件新增功能,但保持向后兼容(升级后现有代码可正常运行)时递增。
  • Z 修订号(Patch):当软件仅修复 bug,且完全向后兼容(不影响现有功能)时递增。

版本范围@

版本范围控制:定义允许的更新范围

在 package.json 中,依赖的版本通常不是固定的某个版本,而是通过 版本范围符号 定义 “允许安装的版本区间”,npm 会根据范围自动选择最合适的版本。

  1. 精确版本(无符号)

    直接指定具体版本号(如 1.2.3),表示仅允许安装该版本,不接受任何更新。

    json
    { "lodash": "4.17.21" }  // 只能安装 4.17.21 版本
  2. 兼容更新(^ 符号)

    允许 次版本号和补丁版本号更新,但不允许主版本号更新(保证不引入不兼容变更)。

    规则

    • 若主版本号 > 0^1.2.3 → 允许 1.2.3 ≤ 版本 < 2.0.0
    • 若主版本号 = 0:表示不稳定版本,^0.2.3 → 允许 0.2.3 ≤ 版本 < 0.3.0(仅允许补丁更新)
    json
    { "express": "^4.18.2" }  // 可安装 4.18.2、4.19.0 等,但不允许 5.0.0
  3. 补丁更新(~ 符号)

    仅允许 补丁版本号更新,次版本号和主版本号保持不变(仅接受 bug 修复)。

    规则

    • ~1.2.3:允许 1.2.3 ≤ 版本 < 1.3.0
    • ~1.2:(等价于 ~1.2.0)→ 同上
    json
    { "axios": "~1.6.8" }  // 可安装 1.6.8、1.6.9 等,但不允许 1.7.0
  4. 任意版本(* 符号)

    允许 所有版本(不推荐,可能引入不兼容变更)。

    json
    { "moment": "*" }  // 会安装最新版本(如 2.30.1,未来可能升级到 3.x)
  5. 范围区间(><>=<=-

    通过比较符号或区间定义更灵活的范围。

    json
    {
      "react": ">=18.0.0 <19.0.0",  // 允许 18.x 所有版本
      "vue": "3.2.0 - 3.4.0"        // 允许 3.2.0 到 3.4.0 之间的版本(包含首尾)
    }

版本配置字段

engines

engines:指定项目运行所需的 Node.js 或 npm 版本(避免环境不兼容)。


  • engines 属性用于指定 Node 和 NPM 的版本号;
  • 在安装的过程中,会先检查对应的引擎版本,如果不符合就会报错;
  • 事实上也可以指定所在的操作系统 "os" : [ "darwin", "linux" ],只是很少用到;
browserslist

browserslist:声明前端项目兼容的浏览器范围(供 babel、autoprefixer 等工具使用)。


  • 用于配置打包后的 JavaScript 浏览器的兼容情况,参考;
  • 否则我们需要手动的添加 polyfills 来让支持某些语法;
  • 也就是说它是为 webpack 等打包工具服务的一个属性(这里不是详细讲解 webpack 等工具的工作原理,所以不再给出详情);

npm 常用命令

npm install

npm install:核心功能是从 npm 仓库下载依赖包到本地,并通过配置文件(package.jsonpackage-lock.json)记录依赖信息,确保项目环境的一致性。

全局/局部安装

npm 安装依赖分为两种情况

  1. 全局安装(global install,工具类依赖)

    • 语法:安装时添加 --global / -g 参数。

    • 安装位置:全局安装的包会被安装到系统全局目录(Linux/macOS 的 ~/.npm,Windows 的 %AppData%\npm)。

    • 同步到环境变量:全局安装的包会同步添加到系统的环境变量中。

    • 使用:可在任何项目中通过命令行直接调用(通常用于脚手架、命令行工具等)。

    • 注意:全局安装的包不会出现在项目的 package.json 中,也不会影响当前项目的 node_modules

    sh
    npm install <> -g
  2. 本地安装(local install)

    本地安装会在当前目录下产生一个 node_modules 文件夹,安装包的 require 查找顺序见: require-查找规则

    本地安装又分为:

    1. 安装生产依赖(dependencies)

      • 语法:安装时添加 --save / -S 参数(可以省略)。

      • 生产依赖:是项目运行时必须的包(如框架、核心库等),会被添加到 package.jsondependencies 字段。

      sh
      npm install <> # 省略 -S / --save
      npm install <> -S
      npm install <> --save
    2. 安装开发依赖(devDependencies)

      • 语法:安装时添加 --save-dev / -D 参数。

      • 开发依赖:是仅开发阶段需要的工具(如代码检查、打包、测试工具等),不会被包含在生产环境的最终产物中,会被添加到 package.jsondevDependencies 字段。

      sh
      npm install <> -D
      npm install <> --save-dev
    3. 安装 package.json 中的所有依赖

      当从代码仓库克隆项目或拿到一个新项目时,执行不带包名的 npm install 可自动安装 package.json 中声明的所有依赖(包括 dependenciesdevDependencies)。

      sh
      npm install  # 或 npm i

      执行逻辑

      • 若存在 package-lock.json:严格按照其中记录的精确版本安装(确保与上次安装完全一致)。
      • 若不存在 package-lock.json:根据 package.json 的版本范围解析并安装最新兼容版本,然后生成 package-lock.json
  3. 从非官方仓库安装依赖

    npm install 支持从多种来源安装包,不限于 npm 官方仓库:

    1. GitHub 仓库

      直接安装 GitHub 上的开源项目

      bash
      # 语法格式:
      npm install github:<用户>/<>#<分支>
      
      npm install github:vuejs/vue  # 安装 Vue 官方仓库的最新代码
      npm install github:facebook/react#v18.2.0  # 安装指定标签(v18.2.0)的版本
    2. 本地路径

      安装本地开发中的包(常用于调试自己开发的依赖)

      bash
      npm install ../my-local-package  # 安装同级目录下的本地包
    3. URL 地址

      直接安装 .tgz 格式的包压缩文件

      bash
      npm install https://registry.npmjs.org/axios/-/axios-1.6.8.tgz
npm install 原理@

很多同学之情应该已经会了 npm install <package>,但是你是否思考过它的内部原理呢?

  • 执行 npm install它背后帮助我们完成了什么操作?
  • 我们会发现还有一个成为 package-lock.json 的文件,它的作用是什么?
  • 从 npm5 开始,npm 支持缓存策略(来自 yarn 的压力),缓存有什么作用呢?

npm install 原理图

image-20251016150139251

npm install 的底层原理是一套复杂的 “依赖解析 - 下载 - 安装 - 校验” 流程,核心目标是根据项目配置,在本地构建出一致、可运行的依赖环境:

  1. 读取配置文件,确定依赖需求

    npm 首先读取项目中的核心配置文件,明确 “需要安装哪些依赖” 以及 “安装什么版本”:

    • package.json:读取 dependencies、devDependencies 等字段,获取依赖的版本范围(如 ^1.2.3~2.3.4)。
    • package-lock.json(若存在):读取已锁定的精确版本、下载地址、哈希值等信息,优先遵循锁文件(忽略 package.json 的版本范围,确保一致性)。
    • npm 配置:读取全局 / 局部 npm 配置(如镜像源 registry、缓存路径 cache 等),影响后续下载地址和行为。
  2. 解析依赖树(核心步骤)

    npm 需要递归解析出 “完整的依赖关系树”,包括:

    • 直接依赖:package.json 中显式声明的依赖(如项目直接依赖的 express)。
    • 间接依赖:直接依赖所依赖的包(如 express 依赖的 accepts、body-parser 等)。

    解析逻辑分两种情况

    • package-lock.json:直接使用锁文件中记录的完整依赖树,无需重新计算版本(确保与上次安装完全一致)。
    • package-lock.json:根据 package.json 的版本范围,从 npm 仓库查询符合条件的版本,递归解析所有依赖的版本范围,生成完整依赖树。

    版本范围解析规则(基于 SemVer):

    • ^1.2.3:允许次版本和补丁更新(1.2.3 ≤ 版本 < 2.0.0)。
    • ~1.2.3:仅允许补丁更新(1.2.3 ≤ 版本 < 1.3.0)。
    • 1.2.3:锁定精确版本。
  3. 处理依赖冲突(避免版本不一致)

    当不同依赖要求同一包的不同版本时(如 A 依赖 lodash@3.x,B 依赖 lodash@4.x),npm 会通过以下策略处理:

    • 优先扁平化解构

      若多个依赖可共享同一版本(如 A 和 B 都兼容 lodash@4.17.21),则将该版本安装在 node_modules 根目录,供所有依赖共享(减少冗余)。

    • 嵌套安装(隔离冲突)

      若版本不兼容(如 3.x 和 4.x),则在对应依赖的子目录中单独安装各自版本(如 node_modules/A/node_modules/lodash@3.xnode_modules/B/node_modules/lodash@4.x),避免冲突。

      plaintext
      node_modules/
      ├── A/                # 直接依赖 A
      │   └── node_modules/
      │       └── C@1.0/    # A 的私有依赖(与 B 的 C 版本冲突)
      ├── B/                # 直接依赖 B
      │   └── node_modules/
      │       └── C@2.0/    # B 的私有依赖
      └── ...
  4. 下载依赖(利用缓存加速)

    解析完依赖树后,npm 会从配置的镜像源(默认 https://registry.npmjs.org)下载依赖包(.tgz 格式的压缩包),并优先利用缓存提升效率:

    • 首次安装:直接从远程仓库下载包,同时将包缓存到本地缓存目录(默认路径:Linux/macOS 为 ~/.npm,Windows 为 %AppData%\npm-cache),并记录哈希值(用于校验完整性)。
    • 非首次安装:先检查缓存中是否存在 “版本匹配且哈希值一致” 的包,若存在则直接从缓存复制到 node_modules,无需重新下载(这是二次安装速度快的核心原因)。

    缓存校验:通过 package-lock.json 中的 integrity 字段(哈希值)验证缓存包的完整性,防止文件被篡改或损坏。

  5. 生成/更新 package-lock.json

    安装完成后,npm 会自动生成或更新 package-lock.json,精确记录以下信息:

    • 每个依赖的最终安装版本(即使 package.json 中是版本范围)。
    • 包的下载地址resolved 字段)和哈希值integrity 字段)。
    • 完整的依赖树结构(包括直接依赖和间接依赖的嵌套关系)。

    这一步是 “不同环境安装一致性” 的核心保障,后续执行 npm install 时,npm 会直接遵循锁文件,忽略 package.json 的版本范围。

  6. 执行生命周期脚本(可选)

    部分依赖(尤其是包含 C++ 扩展、需要编译的包,如 node-sasssqlite3)会在安装后执行初始化脚本,npm 会按以下顺序触发:

    • 执行包自身 package.json 中的 preinstall 脚本(安装前准备)。
    • 完成包的文件解压和复制(到 node_modules)。
    • 执行包的 installpostinstall 脚本(如编译二进制文件、生成配置等)。

    例如,node-sass 会在 postinstall 阶段下载对应平台的二进制文件并编译,确保在当前系统可运行。

其他 npm 命令

我们这里再介绍几个比较常用的:

  • npm uninstall <包名>卸载某个依赖包。
  • npm rebuild:强制重新构建包。
  • npm cache clean清除缓存
  • npm config get cache:获取缓存所在目录
  • npm config list:查看配置列表
  • 更多 npm 命令https://docs.npmjs.com/cli-documentation/cli

npx

npx 介绍

npx:是 npm 5.2.0 版本(2017 年)开始内置的包执行工具,核心作用是快速执行 npm 包中的可执行文件(如命令行工具、脚本等),无需手动安装包到全局或本地。


npm 痛点

npm 痛点

在 npx 出现前,执行一个 npm 包中的命令(如 create-react-appeslint)通常需要:

  1. 先全局安装包(npm install -g <包名>),再执行命令;
  2. 或本地安装后,通过 ./node_modules/.bin/<命令> 路径执行。

这种方式存在明显痛点:

  • 全局安装会占用系统空间,且不同项目可能需要同一工具的不同版本,容易冲突
  • 本地安装后执行路径冗长,不够便捷。

npx 彻底解决了这些问题,它允许你直接执行包中的命令,无需显式安装,且执行后不会残留冗余文件。

npx 工作原理

npx 工作原理

当执行 npx <包名>npx <命令> 时,npx 会按以下逻辑处理:

  1. 查找本地已安装的包:先检查本地 node_modules/.bin 目录和全局安装目录中是否存在目标包的可执行文件。
  2. 未找到则临时下载:若本地不存在,npx 会临时从 npm 仓库下载包到缓存目录(执行后自动清理,不占用额外空间)。
  3. 执行命令:找到或下载完成后,直接执行包中的可执行文件。

示例

  1. 简化本地包的执行

    本地安装的包(在 node_modules 中),通常需要通过 ./node_modules/.bin/<命令> 执行,npx 可直接简化为 npx <命令>

    bash
    # 本地安装了 webpack 后,传统执行方式:
    ./node_modules/.bin/webpack --mode production
    
    # 用 npx 简化:
    npx webpack --mode production
  2. 临时使用 create-react-app 创建 React 项目

    bash
    # 临时使用 create-react-app 创建 React 项目,无需先全局安装
    npx create-react-app my-app
  3. 临时使用 prettier 格式化代码

    bash
    # 临时使用 prettier 格式化代码,无需本地/全局安装
    npx prettier --write src/index.js
  4. 执行 GitHub 仓库中的包

    npx 支持直接从 GitHub 仓库执行代码,无需先克隆仓库,适合快速试用开源项目。

    bash
    # 执行 GitHub 仓库中的脚本(格式:github:用户名/仓库名)
    npx github:vuejs/vue-cli create my-vue-app

对比 npm

npx 不是包管理工具,而是包执行工具,与 npm 的定位完全不同:

  • npm:负责包的安装、卸载、版本管理等(管理依赖);
  • npx:负责快速执行已安装或临时下载的包中的命令(执行操作)。

yarn

yarn 介绍

Yarn(Yet Another Resource Negotiator):是 Facebook 联合 Google、Exponent、Tilde 于 2016 年推出的 JS 包管理工具,旨在解决早期 npm3 之前的性能低下、依赖版本不稳定、安装速度慢等问题。如今已成为与 npm 并驾齐驱的主流包管理工具。


安装 yarn

sh
npm i yarn -g # 全局安装 yarn

核心优势

  1. 安装速度更快

    • 并行安装机制:可同时处理多个依赖的下载和解析,而早期 npm 是串行安装。
    • 本地缓存:重复安装同一依赖时无需重新下载。
  2. 严格的版本锁定

    • yarn.lock:精确记录每个依赖的精确版本、下载地址和哈希值,确保同一项目在不同环境安装的依赖完全一致。
  3. 更简洁的命令体验:Yarn 简化了常用命令的语法,如无需 run 关键字。

  4. 原生支持 Monorepo:Yarn 内置 Workspace 功能,可高效管理 Monorepo 项目。

  5. 更安全的依赖校验

    Yarn 会通过 yarn.lock 中的 integrity 字段对下载的依赖进行哈希校验,确保包未被篡改,降低恶意包的风险。

常用命令@

Yarn 的命令设计与 npm 类似,但更简洁:

功能场景Yarn 命令npm 命令
初始化项目yarn init 或 yarn init -ynpm init 或 npm init -y
安装生产依赖yarn add <包名>npm install <包名>
安装开发依赖yarn add <包名> -Dnpm install <包名> -D
全局安装yarn global add <包名>npm install <包名> -g
卸载依赖yarn remove <包名>npm uninstall <包名>
安装 package.json 所有依赖yarn 或 yarn installnpm install
执行脚本yarn <脚本名>(如 yarn dev)npm run <脚本名>(如 npm run dev)
更新依赖yarn upgrade <包名>npm update <包名>
查看依赖树yarn why <包名>npm ls <包名>
清理缓存yarn cache cleannpm cache clean --force

yarn.lock

yarn.lock:是 Yarn 的版本锁定文件,功能类似 npm 的 package-lock.json,但格式更简洁:

js
"@aashutoshrathi/word-wrap@^1.2.3":
  version "1.2.6" 	// 依赖包精确版本
  resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" 	// 下载地址(.tgz 包)
  integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== // 哈希校验值

"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0":
  version "2.3.0"
  resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
  integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
  dependencies: // 该依赖自身的依赖(间接依赖)
    "@jridgewell/gen-mapping" "^0.3.5"
    "@jridgewell/trace-mapping" "^0.3.24"
...

cnpm

cnpm 介绍

cnpm(Chinese npm):是由淘宝团队开发的 npm 镜像工具,主要用于解决国内开发者访问 npm 官方仓库速度慢、不稳定的问题。

它通过同步 npm 官方仓库的包资源到国内服务器,让国内用户能够快速下载依赖,是国内前端 / Node.js 开发者常用的包管理工具之一。


cnpm 的解决方案

  1. 维护一个与 npm 官方仓库**实时同步(每 10 分钟同步一次)**的国内镜像。
    • 默认地址https://registry.npmmirror.com,原 registry.npm.taobao.org 已迁移至此。
  2. 提供与 npm 兼容的命令行工具,用户可以用几乎相同的语法使用 cnpm,实现依赖的快速安装、更新等操作。

安装 cnpm

cnpm 本身需要通过 npm 全局安装,安装时指定国内镜像源以确保安装成功:

js
npm i -g cnpm --registry=https://registry.npmmirror.com

常用命令

cnpm 的命令设计与 npm 高度一致,几乎可以无缝替代 npm 使用,常用命令如下:

功能场景cnpm 命令说明
初始化项目cnpm initcnpm init -y生成 package.json(同 npm)
安装生产依赖cnpm install <包名>cnpm i <包名>从国内镜像下载依赖,写入 dependencies
安装开发依赖cnpm install <包名> -Dcnpm i <包名> -D写入 devDependencies
全局安装cnpm install <包名> -g安装到系统全局目录(如工具类包)
卸载依赖cnpm uninstall <包名>node_modulespackage.json 中移除
安装所有依赖cnpm installcnpm i根据 package.json 安装所有依赖
执行脚本cnpm run <脚本名>(如 cnpm run dev执行 package.json 中的自定义脚本
更新依赖cnpm update <包名>按版本范围更新依赖

替代方案

  • npm config set registry <镜像地址>设置依赖包安装的镜像仓库地址
  • npm config get registry <镜像地址>获取依赖包安装的镜像仓库地址

除了使用 cnpm 工具,也可直接给 npm 配置国内镜像(无需安装 cnpm):

js
npm config set registry https://registry.npmmirror.com

配置后,npm 会直接从国内镜像下载依赖,效果与 cnpm 类似,但仍使用 npm 自身的命令和锁文件。

pnpm

硬链接/软连接

概述

硬链接(Hard Link):是电脑文件系统中的多个文件平等地共享同一个文件存储单元。删除一个文件名字后,还可以用其它名字继续访问该文件。

软链接(Soft Link,Symbolic Link,符号链接):是一类特殊的文件,包含有一条以绝对路径或者相对路径的形式指向其它文件或者目录的引用

image-20251017134013103

创建方式

文件的拷贝

文件的拷贝每个人都非常熟悉,会在硬盘中复制出来一份新的文件数据;

sh
# window
copy <原文件> <复制文件>

# macos
cp <原文件> <复制文件>

image-20251017134537026


文件的硬链接

sh
# window
mklink /H <硬链接文件> <原文件>

# macos
ln <原文件> <硬链接文件>

image-20251017134549332


文件的软连接

sh
# window(需要管理员权限)
mklink <软链接文件> <原文件>

# macos
ln -S <原文件> <软链接文件>

image-20251017134557195

pnpm

pnpm 介绍

pnpm(Performant npm):是由 Zoltan Kochan 于 2017 年推出的高性能 JS 包管理工具,核心优势是极致的空间利用率超快的安装速度,同时解决了传统包管理工具(npm、yarn)的依赖冗余和隐式依赖问题


核心优势

  1. 极致节省磁盘空间

    pnpm 通过 “全局存储 + 硬链接 + 符号链接” 机制解决了传统工具重复存储同一版本包的问题:

    1. 所有依赖包会被统一存储在全局内容可寻址仓库(默认路径:~/.pnpm-store),每个版本的包仅存储一次。
    2. 项目的 node_modules 中,依赖通过硬链接指向全局仓库的对应版本(硬链接不占用额外空间),再通过符号链接组织依赖树结构。

    结果:相同版本的依赖在系统中仅存一份,可节省 50% 以上的磁盘空间,尤其适合多项目开发。

  2. 超快速的安装速度

    pnpm 的安装速度远超 npm 和 yarn(尤其二次安装):

    • 首次安装:通过并行下载和高效的依赖解析,速度比 npm 快 30%~50%。
    • 二次安装:由于依赖已存储在全局仓库,仅需创建链接即可,几乎瞬间完成(无需重新下载)。
  3. 严格的依赖隔离

    传统工具允许项目访问 node_modules 中未在 package.json 声明的隐式依赖,可能导致依赖泄露。

    pnpm 默认通过符号链接结构实现严格隔离:

    • 项目只能访问 package.json 中显式声明的依赖,未声明的依赖即使存在于 node_modules 中也无法访问。
    • 从根源上避免隐式依赖,提升项目稳定性。
  4. 原生支持 Monorepo

    pnpm 内置对 Monorepo 的支持,通过 pnpm workspace 功能实现:

安装

pnpm 可以通过 npm 安装:

sh
npm install pnpm -g # 通过 npm 安装(推荐)

pnpm -v  # 验证安装

pnpm 特性

硬链接存储依赖

依赖包存储方式对比

  • npm 依赖包存储方式

    当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么你在硬盘上就需要保存 100 份该相同依赖包的副本。

  • pnpm 依赖包存储方式

    如果是使用 pnpm,依赖包将被存放在一个统一的位置,因此:

    • 如果你对同一依赖包使用相同的版本,那么磁盘上只有这个依赖包的一份文件;

    • 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来;

    安装软件包时, 其包含的所有文件都会硬链接到此位置,而不会占用额外的硬盘空间,让你可以在项目之间方便地共享相同版本的依赖包。

image-20251017134622601

非扁平 node_modules

扁平 node_modules:当使用 npm 或 yarn 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。

  • 隐式依赖:扁平 node_modules 会造成允许项目访问 node_modules 中未在 package.json 声明的依赖。

非扁平 node_modules:pnpm 默认通过符号链接结构实现依赖树(非扁平)结构:

  • 项目只能访问 package.json 中显式声明的依赖,未声明的依赖即使存在于 node_modules 中也无法访问。

image-20251017134640302

常用命令

pnpm 的命令设计与 npm、yarn 兼容,核心命令如下:

功能场景pnpm 命令说明
初始化项目pnpm init生成 package.json
安装生产依赖pnpm add <包名>pnpm i <包名>写入 dependencies
安装开发依赖pnpm add <包名> -Dpnpm i <包名> -D写入 devDependencies
全局安装pnpm add <包名> -gpnpm i <包名> -g安装到全局目录
卸载依赖pnpm remove <包名>package.jsonnode_modules 移除
安装所有依赖pnpm installpnpm i根据 package.json 和锁文件安装
执行脚本pnpm <脚本名>(如 pnpm dev执行 package.json 中的脚本
更新依赖pnpm update <包名>pnpm up <包名>按版本范围更新依赖
查看依赖树pnpm why <包名>查看依赖的引用关系
清理缓存pnpm store prune清理全局仓库中未被引用的依赖

pnpm store

pnpm store 存储位置

  • 在 pnpm7.0 之前:统一的存储位置是 ~/.pnpm-store中的;

  • 在 pnpm7.0 之后:统一的存储位置进行了更改:<pnpm home directory>/store

    • 在 Linux 上,默认是 ~/.local/share/pnpm/store

    • 在 Windows 上: %LOCALAPPDATA%/pnpm/store

    • 在 macOS 上: ~/Library/pnpm/store


pnpm store 相关命令:

我们可以通过一些终端命令获取这个目录:

sh
pnpm store path # 获取当前活跃的store目录

pnpm store prune # 从store中删除当前未被引用的包来释放store的空间

Monorepo

发布包

  • npm login:登录 npm 账号。
  • npm publish:发布包。
  • npm version patch:补丁更新。
  • npm version minor:次版本更新。
  • npm version major:主版本更新。
  • npm unpublish <包名>@<版本号>撤销发布指定版本
  • npm deprecate <pkg>[@<version range>] <message>:标记指定版本过期

概述

发布包:将自己开发的工具、库或组件分享到公共仓库,是扩展生态、复用代码的重要方式。主流包管理工具(如 npm、yarn)都支持发布包到 npm 官方仓库

发布流程

发布包的流程

一、准备工作

  • 注册 npm 账号

  • 准备包项目:确保你的项目符合发布要求:

    • 核心文件

      • package.json必须,包的元信息配置。

        json
        // package.json 关键配置
        {
          "name": "my-unique-package",  // 包名(必须唯一,npm 仓库中无重名)
          "version": "1.0.0",           // 版本号(遵循 SemVer 规范:主.次.补丁)
          "main": "index.js",           // 入口文件(其他项目引入时的默认文件)
          "description": "一个示例包",   // 包描述(用于搜索和展示)
          "author": "你的名字",          // 作者信息
          "license": "MIT",             // 开源协议(如 MIT、Apache)
          "private": false              // 必须设为 false(true 会禁止发布)
        }
      • index.js:建议,入口文件。

      • README.md:建议,说明文档。

      • .gitignore / .npmignore:忽略不需要发布的文件。

  • 本地测试包:发布前务必测试包的可用性。

二、发布包(以 npm 为例)

  1. 登录 npm 账号

    在终端中执行登录命令,输入注册时的用户名、密码、邮箱:

    bash
    npm login # 输入用户名、密码(输入时不可见)、邮箱(需验证过的)

    注意:若使用镜像源(如淘宝镜像),需先切换回官方源(否则发布失败):

    bash
    npm config set registry https://registry.npmjs.org/
  2. 发布包

    在项目根目录执行发布命令:

    bash
    npm publish
  3. 验证发布结果

    成功标志:终端显示 + <包名>@<版本号>,表示发布成功。此时可在 npmjs.com/package/<包名> 查看你的包。

三、更新已发布的包

  1. 更新版本号

    根据 SemVer 规范手动更新 package.json 中的 version 字段,或通过 npm 命令自动更新:

    bash
    npm version patch  # 补丁更新(修复 bug,如 1.0.0 → 1.0.1)
    npm version minor  # 次版本更新(新增功能,如 1.0.1 → 1.1.0)
    npm version major  # 主版本更新(不兼容变更,如 1.1.0 → 2.0.0)

    执行后,package.jsonversion自动更新,同时生成 Git 标签(若使用 Git)。

  2. 重新发布

    更新版本后,再次执行发布命令:

    bash
    npm publish

四、撤销已发布的包(谨慎使用)

若发布的包存在严重问题,可在发布后 24 小时内 撤销特定版本:

注意

  • 发布超过 24 小时的包无法撤销版本(npm 防止恶意删除影响依赖项目)。
  • 禁止撤销整个包(除非包名被恶意占用且未被使用),否则可能被 npm 封号。
bash
npm unpublish <>@<版本>  # 撤销指定版本

五、其他包管理工具的发布差异

  • yarn:发布命令与 npm 类似,登录和发布流程一致:
  • pnpm:同样兼容 npm 发布流程: