构建项目需要一些依赖项,运行程序时需要其他依赖项。因此,您可以拥有多种不同类型的依赖关系(例如 dependenciesdevDependencies、 和peerDependencies)。 package.json将包含所有这些依赖项:
  1. {
  2. "name": "my-project",
  3. "dependencies": {
  4. "package-a": "^1.0.0"
  5. },
  6. "devDependencies": {
  7. "package-b": "^1.2.1"
  8. },
  9. "peerDependencies": {
  10. "package-c": "^2.5.4"
  11. },
  12. "optionalDependencies": {
  13. "package-d": "^3.1.0"
  14. }
  15. }

大多数人只有 dependenciesdevDependencies,但理解其中每一个都很重要。

  1. devDependencies:这些是您的开发依赖项。您在开发工作流程中的某个时刻需要但在运行代码时不需要的依赖项(例如 Babel 或 Flow),安装使用 npm install -D 或者 npm install —save-dev 命令。
  2. dependencies:这些是您的正常依赖项,或者更确切地说是您运行代码时需要的依赖项(例如 express、lodash、axios 等)。
  3. peerDependencies: 对等依赖是一种特殊类型的依赖,只有在您发布自己的包时才会出现。具有对等依赖关系意味着您的包需要一个与安装您的包的人完全相同的依赖关系。这对于像react需要有一个副本react-dom也可供安装它的人使用的软件包很有用。
  4. optionalDependencies: 可选依赖项就是:可选。如果安装失败,Yarn 仍会说安装过程已成功。这对于不一定在每台机器上都有效的依赖项很有用,并且您有一个后备计划,以防它们未安装(例如 Watchman)。

devDependencies、dependencies 和 peerDependencies 是三个常用的包依赖声明类型。它们在 package.json 文件中定义,并用于管理项目所依赖的不同类型的包。

安装包—save、—save-dev、-S、-D的区别

  • —save等同于-S(默认:即不加-S和-D会默认成-S): 安装包信息将加入到dependencies
    生产阶段的依赖,也就是项目运行时的依赖,就是程序上线后仍然需要依赖,如vue、express、lodash、axios 等包。
  • —save-dev 等同于 -D: 装包信息将加入到devDependencies
    开发阶段的依赖,就是我们在开发过程中需要的依赖,只在开发阶段起作用,如开发工具、测试框架等。
以上概念看起来比较简单容易理解,但是大多数开发者会产生一个误区:那就是以为只有安装在dependencies 中的依赖才会在项目执行 build 的时候会不会被打包进 dist 产物中,而安装在 devDependencies 中的依赖并不会被打包进 dist 产物中。 但其实:项目打包跟 devDependencies 这个字段并没什么关系,只要是项目中用到的依赖(且安装到 node_modules 中),不管这个依赖是放在 devDependencies 还是放在 dependencies ,都会被打包工具解析、构建,最后都打进 dist 产物中。即:生产打包 与 devDependencies 字段无关。devDependencies 中的 dev并不是指我们 dev server 时候的 dev ,不能简单的把 dev 理解成当前项目的 “开发环境” 。 接着往下,我们通过真实的装包来验证一下这个结论。

验证dependencies中的包

  1. 基于下面的命令生产一个vue3项目

详解包依赖声明类型 - 图1

  1. 项目结构和项目依赖如下:

详解包依赖声明类型 - 图2

可以看到,目前的 vue 是放在 dependencies 字段中。

  1. 执行npm i安装依赖,然后在node_modules 中找到 vue 的依赖包,并且找到对应的入口文件。

详解包依赖声明类型 - 图3

  1. 在入口文件打一个log看看,执行npm run build并对dist文件进行搜索

详解包依赖声明类型 - 图4

没有意外,vue包给打进了该项目的dist包中。

验证devDependencies中的包

把 vue 的依赖信息移动到 devDependencies 中,然后删除掉之前的 node_modules 目录后重新执行 install ,同样在vue的入口文件打上log,执行<font style="color:rgb(37, 43, 58);">npm run build</font>,再去dist目录中搜索,结果如下图所示:

详解包依赖声明类型 - 图5

由此可见,放在 devDependencies 的 vue 在 build 时候(mode 为 production)依然会被打包进单页应用的项目中。

那么我们可以思考以下问题:

为什么修改devDependencies包里的内容build时也会打进dist文件

上面这个问题其实很简单,我们从正常的项目打包流程分析(不管是 webpack 还是 vite,打包的核心步骤都类似),针对上面的问题进行简化分析: 1. 初始化配置 2. 项目入口 3. 依赖解析 4. loader处理 5.

看到这样的打包流程(集中关注第2、3点),大家应该也意识到一点:项目打包跟 devDependencies 这个字段并没什么关系。这样一来,上述问题的答案也就很清晰了。只要是项目中用到的依赖(且安装到 node_modules 中),不管这个依赖是放在 devDependencies 还是放在 dependencies ,都会被打包工具解析、构建,最后都打进 dist 产物中。

所以,通过这个实践,就为了搞清楚一个点,devDependencies 的 dev 并不是指我们在业务项目开发中的 dev 和 prod,它甚至跟打包时候的 mode 扯不上关系。

那么 devDependenciesdependencies 的区别在哪里呢?我们接着往下看。

npm包的devDependencies

提到npm包,发过npm包的同学可能已经猜到了,没错devDependencies 这个字段的 dev 的真正含义,更多是指 npm包 的开发阶段所需要的依赖。

怎么理解前面提到的 npm包 开发阶段所需要的依赖?我们大概回忆一下npm包从 开发 - 发包 的流程。

npm发包流程

  1. npm初始化——package.json。想要开发一个 npm包,最先一定是要进行初始化,执行命令 npm init,然后填写一些信息比如 name 、 version 、 description …此时便会生成一个 pakcage.json 文件。
  2. npm包的开发。这个阶段,也就是对 npm包 功能实现的阶段,我们会开始编写代码。然而,我们在编写npm包的时候,可能需要用到其他的库,这个时候我们就需要去安装其他的库
  3. npm包的打包、发布。npm包开发完成后,当然就是要对我们的项目进行打包,然后通过 npm publish 命令去发布我们的npm包。

其中第二步:如果开发过程中需要用到其他的工具库,就要把依赖安装到当前项目里!这就涉及到本文的重点了,要怎么安装呢?-D、还是-S?不同的命令会带来怎么样不同的后果呢?

npm包安装-D和-S对比实验

对比实验1: 安装在devDependencies中依赖

  1. npm包的依赖情况
  1. "dependencies": {
  2. "@element-plus/icons-vue": "^2.0.6",
  3. "@xxx/vc-shared": "workspace:*",
  4. "lodash": "^4.17.21"
  5. },
  6. "devDependencies": {
  7. "@vitejs/plugin-vue": "^2.3.3",
  8. "element-plus": "^2.2.8",
  9. "vue": "^3.2.36",
  10. "vue-router": "4",
  11. "less": "^4.1.3"
  12. }
  1. 将组件库发包。最后,在业务项目中安装该组件库,业务项目依赖如下图:

详解包依赖声明类型 - 图6

3.看下安装的npm包内部的依赖情况如下图:

详解包依赖声明类型 - 图7

以上可以猜想:业务项目中拥有了组件库 dependencies 中的依赖包。接着进行对比实验2将 element-plus 安装在dependencies中来验证这个猜想。

对比实验2: 安装在dependencies中依赖

  1. npm包的依赖情况
  1. "dependencies": {
  2. "@element-plus/icons-vue": "^2.0.6",
  3. "@xxx/vc-shared": "workspace:*",
  4. "lodash": "^4.17.21",
  5. "element-plus": "^2.2.8"
  6. },
  7. "devDependencies": {
  8. "@vitejs/plugin-vue": "^2.3.3",
  9. "vue": "^3.2.36",
  10. "vue-router": "4",
  11. "less": "^4.1.3"
  12. }
  1. 将组件库发包。最后,在业务项目中安装该组件库,业务项目依赖如下图:

详解包依赖声明类型 - 图8

  1. 看下安装的npm包内部的依赖情况如下图:

详解包依赖声明类型 - 图9

ok,这样一对比,应该就很清晰了,npm包安装在dependencies中的 element-plus 被安装进了业务的依赖。也证实了对比实验一的猜想。 结论:devDependenciesdependencies 的区别核心体现在 npm包 中。只要开发的项目是发npm包提供给外部、其他业务项目使用的,需要非常注意依赖的安装地方,否则很容易在业务使用中会出现bug。而如果只是自己项目用,不需要发npm包的话,把依赖安装到 devDependencies 或者 dependencies 中,实质上是没有任何区别的。 为什么在开发 npm包 的时候 不严格区分 devDependenciesdependencies 进行装包可能会导致业务项目的使用中出现bug呢?举个例子来加深理解:
  • 假设npm包开发者不小心把 vue3 的依赖写到了 dependencies 中(用于开发调试的),版本是 3.0.36。业务项目自身用了 vue@3.0.0 的情况下,安装了这个 npm包 ,由于 npm包 中的 dependenciesvue@3.0.36 这个依赖,此时会在装 npm包 的同时安装36版本的vue。由于 npm包中会用到vue,代码是这样引入的:import { onMount } from ‘vue’,此时,npm包会在自己内部的 node_modules 中找到 vue@3.0.36 的包并使用,此时就会产生 2 个 vue3 实例,就很容易出现一些奇怪的bug。(业务项目的vue@3.0.0 和 npm包的vue@3.0.36)
  • 这里还要注意一点就是 **externals** 。有同学可能会说,npm包打包的时候会 externals 掉第三方的库,比如上述中的 vue3 ,externals 只是保证 externals的代码不打包进 npm包 的代码中而已。
经过以上分析,devDependencies和dependencies区别就很明显了,那么peerDependencies又是什么?

peerDependencies

使用 peerDependencies 的一个常见的场景是解决依赖冲突问题 下面通过一个例子,我们来理解一下 peerDependencies 以前运行react工程时,浏览器上抛了一个奇怪的错误:

:::info

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

:::

归纳总结一下可能是一下几个原因导致的:
  • React 和 React DOM 版本不匹配
  • 打破了 Hook 的规则
    你只能在当 React 渲染函数组件时调用 Hook:
    • 在函数组件的顶层调用它们。
    • 在自定义 Hook 的顶层调用它们。
  • 重复的 React
关于第一点,官网解释说有可能使用了不支持 React Hook react-dom 版本(<16.8.0),这点通过确认 package.json 中的 react-dom 版本号得以排除。 再看第二点,关于 Hooks 用法的问题,因为这段代码以前跑成功过,而且通过检查,也可以很确定的排除了。 最后只剩下第三点,仔细看一下,这一点官网也做了详细的描述: In order for Hooks to work, the react import from your application code needs to resolve to the same module as the react import from inside the react-dom package. If these react imports resolve to two different exports objects, you will see this warning. This may happen if you accidentally end up with two copies of the react package. 嗯,看到这个描述有点豁然开朗的感觉,为了使 Hook 正常工作,应用代码中的 react 依赖以及 react-dom 的 package 内部使用的 react 依赖,必须解析为同一个模块。
如果这些 react 依赖解析为两个不同的导出对象,你就会看到本警告。这么分析完应该就是我封装的组件中依赖的 react 和 react-dom 的版本号和主工程中所依赖的 react 和 react-dom 的版本号不一致导致的。 继续看完文档,其中有一句话引起了我的注意:
For example, maybe a library you’re using incorrectly specifies react as a dependency (rather than a peer dependency).
这里提示我说我使用的库可能错误地指定 react 作为 dependency(而不是 peer dependency)。

初识peerDependencies

假设现在有一个 helloWorld 工程,已经在其 package.json 的 dependencies 中声明了 packageA,有两个插件 plugin1 和 plugin2 他们也依赖 packageA,如果在插件中使用 dependencies 而不是 peerDependencies 来声明 packageA,那么 $ npm install 安装完 plugin1 和 plugin2 之后的依赖图是这样的:

:::info

├── helloWorld │ └── node_modules │ ├── packageA │ ├── plugin1 │ │ └── nodule_modules │ │ └── packageA │ └── plugin2 │ │ └── nodule_modules │ │ └── packageA

:::

从上面的依赖图可以看出,helloWorld 本身已经安装了一次packageA,但是因为因为在plugin1 和 plugin2 中的 dependencies 也声明了 packageA,所以最后 packageA 会被安装三次,有两次安装是冗余的。 peerDependency 就可以避免类似的核心依赖库被重复下载的问题。 如果在 plugin1 和 plugin2 的 package.json 中使用 peerDependency 来声明核心依赖库,例如:

plugin1/package.json

  1. {
  2. "peerDependencies": {
  3. "packageA": "1.0.1"
  4. }
  5. }

plugin2/package.json

  1. {
  2. "peerDependencies": {
  3. "packageA": "1.0.1"
  4. }
  5. }
在主系统中声明一下 packageA:

helloWorld/package.json

  1. {
  2. "dependencies": {
  3. "packageA": "1.0.1"
  4. }
  5. }
此时在主系统中执行 $ npm install 生成的依赖图就是这样的:

:::info ├── helloWorld
│ └── node_modules
│ ├── packageA
│ ├── plugin1
│ └── plugin2

:::

可以看到这时候生成的依赖图是扁平的,packageA 也只会被安装一次。

peerDependencies实践

明白了 peerDependencies 的用法,那么开头问题就迎刃而解了。 首先在主系统的 package.json 中的 dependencies 声明下 reactreact-dom 的版本:
  1. {
  2. "dependencies": {
  3. "react": "^16.13.1",
  4. "react-dom": "^16.13.1"
  5. }
  6. }
接着在组件库的 package.json 中的 peerDependencies 声明 reactreact-dom 的版本:
  1. {
  2. "peerDependencies": {
  3. "react": ">=16.12.0",
  4. "react-dom": ">=16.12.0"
  5. }
  6. }
这样在主系统中执行 npm install之后,主系统和组件库就能共用主系统的 node_module 中安装的 reactreact-dom 了,至此问题圆满解决。

小tips: 执行npm install --omit=dev可以不安装devDependencies,一般部署到服务器时候可以用这个命令,因为服务器不需要安装dev的依赖,可以提升安装依赖的速度。

参考文章

npm-Specifying dependencies and devDependencies in a package.json file

npm-peerdependencies

yarn-Types of dependencies

你真的理解 devDependencies 和 dependencies 的区别吗?