Skip to content

包管理工具

sermer

Semantic Versioning 语义化版本的缩写,由[major, minor, patch]3 部分构成。

major: 当你发了一个含有 Breaking Change 的 API minor: 当你新增了一个向后兼容的功能时 patch: 当你修复了一个向后兼容的 Bug 时

在发包前,npm version 的相关命令可以自动更新版本

shell
# 增加一个修复版本号: 1.0.1 -> 1.0.2 (自动更改 package.json 中的 version 字段)
$ npm version patch

# 增加一个小的版本号: 1.0.1 -> 1.1.0 (自动更改 package.json 中的 version 字段)
$ npm version minor

# 将更新后的包发布到 npm 中
$ npm publish
# 增加一个修复版本号: 1.0.1 -> 1.0.2 (自动更改 package.json 中的 version 字段)
$ npm version patch

# 增加一个小的版本号: 1.0.1 -> 1.1.0 (自动更改 package.json 中的 version 字段)
$ npm version minor

# 将更新后的包发布到 npm 中
$ npm publish

~^的范围

  1. 对于 ~1.2.3 而言,它的版本号范围是 >=1.2.3 <1.3.0

  2. 对于 ^1.2.3 而言,它的版本号范围是 >=1.2.3 <2.0.0

package-lock 的工作流程

  1. package-lock.jsonpackage 锁死的版本号符合 package.json 中的版本号范围时,将以 package-lock.json 锁死版本号为主。

  2. package-lock.jsonpackage 锁死的版本号不符合 package.json 中的版本号范围时,将会安装该 package 符合 package.json 版本号范围的最新版本号,并重写 package-lock.json

package.json 中的一些关键字段

main

json
{
  "module": "./dist/index.js"
}
{
  "module": "./dist/index.js"
}

指定了入口文件,是 CommonJS 时代的产物。

module

示例:

json
{
  "module": "./es/index.mjs"
}
{
  "module": "./es/index.mjs"
}

指定了 ESM 模块的入口文件,如果使用了 import 进行导入,文件系统会先查找 module 指定的文件,如果未找到则使用 main 指定的文件。

随着 ESM 的发展,许多 package 会打包成多种模块化格式,如antd既支持ESM又支持umd,分别打包出esdist目录。

如果代码只分发ESM方案,则直接使用main指定即可。

exports

json
{
  "exports": {
    ".": "./dist/index.js",
    "get": "./dist/get.js"
  }
}
{
  "exports": {
    ".": "./dist/index.js",
    "get": "./dist/get.js"
  }
}

可以控制子目录的路径,如指定了此项,那么不在 exports 字段中的模块则无法引用。

exports还可以根据环境变量和运行环境导入不同的入口文件。

json
{
  "type": "module",
  "exports": {
    "electron": {
      "node": {
        "development": {
          "module": "./index-electron-node-with-devtools.js",
          "import": "./wrapper-electron-node-with-devtools.js",
          "require": "./index-electron-node-with-devtools.cjs"
        },
        "production": {
          "module": "./index-electron-node-optimized.js",
          "import": "./wrapper-electron-node-optimized.js",
          "require": "./index-electron-node-optimized.cjs"
        },
        "default": "./wrapper-electron-node-process-env.cjs"
      },
      "development": "./index-electron-with-devtools.js",
      "production": "./index-electron-optimized.js",
      "default": "./index-electron-optimized.js"
    },
    "node": {
      "development": {
        "module": "./index-node-with-devtools.js",
        "import": "./wrapper-node-with-devtools.js",
        "require": "./index-node-with-devtools.cjs"
      },
      "production": {
        "module": "./index-node-optimized.js",
        "import": "./wrapper-node-optimized.js",
        "require": "./index-node-optimized.cjs"
      },
      "default": "./wrapper-node-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}
{
  "type": "module",
  "exports": {
    "electron": {
      "node": {
        "development": {
          "module": "./index-electron-node-with-devtools.js",
          "import": "./wrapper-electron-node-with-devtools.js",
          "require": "./index-electron-node-with-devtools.cjs"
        },
        "production": {
          "module": "./index-electron-node-optimized.js",
          "import": "./wrapper-electron-node-optimized.js",
          "require": "./index-electron-node-optimized.cjs"
        },
        "default": "./wrapper-electron-node-process-env.cjs"
      },
      "development": "./index-electron-with-devtools.js",
      "production": "./index-electron-optimized.js",
      "default": "./index-electron-optimized.js"
    },
    "node": {
      "development": {
        "module": "./index-node-with-devtools.js",
        "import": "./wrapper-node-with-devtools.js",
        "require": "./index-node-with-devtools.cjs"
      },
      "production": {
        "module": "./index-node-optimized.js",
        "import": "./wrapper-node-optimized.js",
        "require": "./index-node-optimized.cjs"
      },
      "default": "./wrapper-node-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

dependencies 与 devDependencies

当进行业务开发时,严格区分 dependenciesdevDependencies 并无必要,实际上,大部分业务对二者也并无严格区别。

但是对于库 (Package) 开发而言,是有严格区分的。

当在项目中安装一个依赖的 Package 时,该依赖的 dependencies 也会安装到项目中,即被下载到 node_modules 目录中。但是 devDependencies 不会。

engines

可以用于指定一个项目所需的 node 最小版本。

json
{
  "engines": {
    "node": ">=14.0.0"
  }
}
{
  "engines": {
    "node": ">=14.0.0"
  }
}

如果对于版本不匹配的情况,npm 会抛出警告,而 yarn 会直接报错。

files

控制实际发包内容,通常只需要发构建后的资源,源代码目录可发可不发

json
{
  "files": ["dist"]
}
{
  "files": ["dist"]
}

npm scripts

npm 自带 script

shell
npm install
npm test
npm publish
npm install
npm test
npm publish

自定义工具链

json
{
  "start": "serve ./dist",
  "build": "webpack",
  "lint": "eslint"
}
{
  "start": "serve ./dist",
  "build": "webpack",
  "lint": "eslint"
}
shell
npm run start
npm run build
npm run lint
npm run start
npm run build
npm run lint

生命周期

npm scripts 的生命周期常用来解决 2 大问题

  1. 在某个 npm 包安装完毕后,执行自动操作

  2. npm 包在发布前需要执行自动打包

当我们执行任意npm run脚本时,将会自动触发prepost的生命周期。

pre发生在执行前,post发生在执行后

例如:

json
{
  "scripts": {
    "postinstall": "patch-package"
  }
}
{
  "scripts": {
    "postinstall": "patch-package"
  }
}

那么在执行完npm install后将会自动执行npm run postintall

而发包涉及到的声明周期则更为复杂,当执行npm publish时,将自动执行以下脚本

  1. prepublishOnly: 如果发包之前需要构建,可以放在这里执行

  2. prepack

  3. prepare: npm install 后和 npm publish 前都会执行

  4. postpack

  5. publish

  6. postpublish

如果涉及到类型检查、测试、构建,最常用的是 prepublishOnly

json
{
  "scripts": {
    "prepublishOnly": "npm run test && npm run build"
  }
}
{
  "scripts": {
    "prepublishOnly": "npm run test && npm run build"
  }
}

prepare,会在npm install之后执行,在npm publish之前执行。例如需要安装 husky

json
{
  prepare: "husky install";
}
{
  prepare: "husky install";
}

npm scripts也存在一定的风险,例如被攻击后注入了 npm postinstall 自动执行一些事,例如挖矿。

npm cache

npm 会把所有下载的包,保存在用户文件夹下面。

下次 npm install 时,会根据 package-lock.json 里面保存的 sha1 值去文件夹里面寻找包文件,如果找到就不用从新下载安装了。

可以通过以下命令清空缓存

shell
npm cache clear --force
npm cache clear --force

node_modules 的结构问题

npm v2时期

嵌套结构

package-a
|--lodash@4.17.4
package-b
|--lodash@4.17.4
package-a
|--lodash@4.17.4
package-b
|--lodash@4.17.4

存在 2 大问题

  1. 嵌套过深

  2. 体积过大

npm v3之后

平铺结构

package-a
package-b
|--lodash@4.16.1
lodash@4.17.4
package-a
package-b
|--lodash@4.16.1
lodash@4.17.4

如果ab分别依赖了不同的lodash版本,那么会有一个版本被放到子目录中,产生分身

幻影依赖

当项目中使用了一个没有在package.json中定义的包时,便产生了幻影依赖。

json
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "lib/index.js",
  "dependencies": {
    "minimatch": "^3.0.4"
  },
  "devDependencies": {
    "rimraf": "^2.6.2"
  }
}
{
  "name": "my-library",
  "version": "1.0.0",
  "main": "lib/index.js",
  "dependencies": {
    "minimatch": "^3.0.4"
  },
  "devDependencies": {
    "rimraf": "^2.6.2"
  }
}
js
var minimatch = require('minimatch')
var expand = require('brace-expansion') // ???
var glob = require('glob') // ???
var minimatch = require('minimatch')
var expand = require('brace-expansion') // ???
var glob = require('glob') // ???

这是因为 node_modules 目录的平铺结构,并且 nodeesm 不需要考虑 package.json,所以它找到了这些库。

这就是 NPMnode_modules 树的特性,是必然的,是由其设计导致,无法避免。

分身的结果

小项目内很少遇到分身,但是在大型的 monorepo 中很常见,这会导致一些问题。

  1. 更慢的安装时间

  2. 增大包体积

  3. 非单一的全局变量

  4. 多重类型

  5. 语义上的分身

  6. 破坏单例模式,破坏缓存,如 postcss 的许多插件将 postcss 扔进 dependencies,重复的版本将导致解析 AST 多次

包管理工具

npm: 它是当今最广泛的 JavaScript 包管理工具,它开创了包管理标准,其开发者还维护了世界上最多人使用的分布式开源 JavaScript 包管理网站 npmjs.com

yarn: 它重新实现了 NPM, 与之相比,Yarn 具有相同的管理方式,但是安装速度更快(多线程),稳定性更好,而且提供了一些新特性(例如 Yarn workspaces),用于大型开发。

pnpm: 它提供了一个全新的包管理模式,该模式解决了“幻影依赖”和“ NPM 分身”的问题,同时符号链接使之与 NodeJS 模块解析标准保持 100% 兼容。

pnpm 详解

软链接和硬链接

shell
$ ln -s hello hello-soft
$ ln hello hello-hard

$ ls -lh
total 768
45459612 -rw-r--r--  2 xiange  staff   153K 11 19 17:56 hello
45459612 -rw-r--r--  2 xiange  staff   153K 11 19 17:56 hello-hard
45463415 lrwxr-xr-x  1 xiange  staff     5B 11 19 19:40 hello-soft -> hello
$ ln -s hello hello-soft
$ ln hello hello-hard

$ ls -lh
total 768
45459612 -rw-r--r--  2 xiange  staff   153K 11 19 17:56 hello
45459612 -rw-r--r--  2 xiange  staff   153K 11 19 17:56 hello-hard
45463415 lrwxr-xr-x  1 xiange  staff     5B 11 19 19:40 hello-soft -> hello

他们的区别有以下几点:

1.软链接可理解为指向源文件的指针,它是单独的一个文件,仅仅只有几个字节,它拥有独立的 inode

2.硬链接与源文件同时指向一个物理地址,它与源文件共享存储数据,它俩拥有相同的 inode

pnpm 为何省空间

它解决了 npm/yarn 平铺 node_modules 带来的依赖项重复的问题 (doppelgangers)

生成的 node_modules 结构如图所示

shell
./node_modules/package-a       ->  .pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/package-b       ->  .pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/package-c       ->  .pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/package-d       ->  .pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/lodash@3.0.0
./node_modules/.pnpm/lodash@4.0.0
./node_modules/.pnpm/package-a@1.0.0
./node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/.pnpm/package-a@1.0.0/node_modules/lodash     -> .pnpm/package-a@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-b@1.0.0
./node_modules/.pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/.pnpm/package-b@1.0.0/node_modules/lodash     -> .pnpm/package-b@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-c@1.0.0
./node_modules/.pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/.pnpm/package-c@1.0.0/node_modules/lodash     -> .pnpm/package-c@1.0.0/node_modules/lodash@3.0.0
./node_modules/.pnpm/package-d@1.0.0
./node_modules/.pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/package-d@1.0.0/node_modules/lodash     -> .pnpm/package-d@1.0.0/node_modules/lodash@3.0.0
./node_modules/package-a       ->  .pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/package-b       ->  .pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/package-c       ->  .pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/package-d       ->  .pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/lodash@3.0.0
./node_modules/.pnpm/lodash@4.0.0
./node_modules/.pnpm/package-a@1.0.0
./node_modules/.pnpm/package-a@1.0.0/node_modules/package-a
./node_modules/.pnpm/package-a@1.0.0/node_modules/lodash     -> .pnpm/package-a@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-b@1.0.0
./node_modules/.pnpm/package-b@1.0.0/node_modules/package-b
./node_modules/.pnpm/package-b@1.0.0/node_modules/lodash     -> .pnpm/package-b@1.0.0/node_modules/lodash@4.0.0
./node_modules/.pnpm/package-c@1.0.0
./node_modules/.pnpm/package-c@1.0.0/node_modules/package-c
./node_modules/.pnpm/package-c@1.0.0/node_modules/lodash     -> .pnpm/package-c@1.0.0/node_modules/lodash@3.0.0
./node_modules/.pnpm/package-d@1.0.0
./node_modules/.pnpm/package-d@1.0.0/node_modules/package-d
./node_modules/.pnpm/package-d@1.0.0/node_modules/lodash     -> .pnpm/package-d@1.0.0/node_modules/lodash@3.0.0

它做了 2 件事情

  1. 借助软链接的方式,解决了重复依赖安装的问题,节省了单个项目的体积

  2. 借助硬链接的方式,节省了多个项目重复依赖的体积

monorepo