前端项目依赖管理详解

1,288 阅读13分钟

💡 Question:在项目开发的时候,我们也经常需要安装和升级对应的依赖。虽然 npm 以及语意化的版本号 (semantic versioning, semver) 让开发过程中依赖的获取和升级变得非常容易, 但不严格的版本号限制,也带来了版本号的不确定性。本章我们就来一起回顾一下npm中控制工程安装版本的文件。

package.json

什么是package.json

package-lock由npm 5版本后引入,npm 5版本前由package.json控制工程安装的版本,遵循semver机制。


如何生成package.json

npm init 交互式问答的方式生成 package.json
npm init -y/--yes 自动生成默认的 package.json


package.json配置详解

描述配置

name

项目的名称,如果是第三方包的话,其他人可以通过该名称使用 npm install 进行安装,有些包名会含有 scope

"name": "@icedesign/pro-scaffold",

name 规范:

  • 不多于 214 个字符,包括 scope
  • 不能以 . 或 _ 开头
  • 不能包含大写字母
  • 有可能会成为 URL 的一部分、命令行参数或者文件夹名字,因此不能包含任何非 URL 安全字符,如空格、(、``、"
  • 可能会作为 require() 的参数传递
  • 最好取简短而语义化的值
  • 不能和 NPM 网站中已有的包名字重名

scope:

scope 是一种将相关的模块组织到一起的一种方式,可以防止包重名。
命名遵循 name 的规范,scope 前面是 @ 符号,后面是 /,格式为 @somescope/somepackagename,如:@somescope/somepackagename,会被安装在 node_modules/@somescope/somepackagename下。

举个例子:


version

当前的版本,开源项目的版本号通常遵循 semver 语义化规范。

"version": "3.0.15",

package.json 中最重要的属性是 name 和 version 两个属性,这两个属性是必须要有的,否则模块就无法被安装,这两个属性一起形成了一个 npm 模块的唯一标识。


description

项目的描述,会展示在 npm 官网,让别人能快速了解该项目。

"description": "橙狮体育商家平台",

keywords

一组项目的技术关键词,比如 Ant Design 组件库的 keywords 如下:

"keywords": [
  "ant",
  "component",
  "components",
  "design",
  "framework",
  "frontend",
  "react",
  "react-component",
  "ui"
 ],


homepage

项目主页的链接,通常是项目 github 链接,项目官网或文档首页。


repository

指定代码存放地址

"repository": {
  "type": "git",
  "url": "https://github.com/xxx/xxx.git"
}

bugs

项目 bug 反馈地址,通常是 github issue 页面的链接或者邮箱

// 如果提供了 url,则可以被 npm bugs 命令使用
"bugs": {
  "url": "",
  "email": ""
}

license

项目的开源许可证。项目的版权拥有人可以使用开源许可证来限制源码的使用、复制、修改和再发布等行为。


author

项目作者

"author": {
  "name": "zhanbaihe",
  "email": "zhanbaihe.zbh@alibaba-inc.com",
  "url": "https://xxxx.com"
}

"author": "zhanbaihe zhanbaihe.zbh@alibaba-inc.com"

contributors

贡献者

"contributors": [
    {
      "name": "zhanbaihe",
  		"email": "zhanbaihe.zbh@alibaba-inc.com",
  		"url": "https://xxxx.com"
    }
]

文件配置

bin

许多软件包都具有一个或多个要安装到 PATH 中的可执行文件。

bin 字段是命令名到本地文件名的映射。在安装时,npm 会将文件符号链接到 prefix/bin 以进行全局安装或./node_modules/.bin/本地安装。

当我们使用 npm 或者 yarn 命令安装包时,如果该包的 package.json 文件有 bin 字段,就会在 node_modules 文件夹下面的 .bin 目录中复制了 bin 字段链接的执行文件。我们在调用执行文件时,可以不带路径,直接使用命令名来执行相对应的执行文件。

{
	"bin": "./path/to/program" 
}

{ 
	"bin" : { "my-program" : "./path/to/program" } 
}

举个例子:


main

指定加载模块的入口文件,require() 导入的时候就会加载这个文件。默认值是模块根目录下的 index.js。


脚本配置

scripts

指定项目的一些内置脚本命令,这些命令可以通过 npm run 来执行。通常包含项目开发,构建 等 CI 命令。

举个例子:

"scripts": {
  "zbh": "a"
},

除了指定基础命令,还可以配合 pre 和 post 完成命令的前置和后续操作。

举个例子:

"scripts": {
	"prebuild" : "echo " this is pre build "",
  "build" : "echo " this is build "",
  "postbuild" : "echo " this is post build ""
}

当执行 npm run build 命令时,会按照 prebuild -> build -> postbuild 的顺序依次执行上方的命令。利用这个特性可以在执行某些命令前、后做一些操作,比如build前清空目录,build后做一些压缩之类的事。


依赖配置

dependencies

运行依赖,也就是项目生产环境下需要用到的依赖。比如 react,vue,状态管理库以及组件库等。

使用 npm install xxx 或则 npm install xxx --save 时,会被自动插入到该字段中。


devDependencies

开发依赖,项目开发环境需要用到而运行时不需要的依赖,用于辅助开发,通常包括项目工程化工具比如 webpack,vite,eslint 等(开发、测试、打包工具)。

使用 npm install xxx -D 或者 npm install xxx --save-dev 时,会被自动插入到该字段中。


peerDependencies

同伴依赖,一种特殊的依赖,不会被自动安装,通常用于表示与另一个包的依赖与兼容性关系来警示使用者。

比如我们安装 A,A 的正常使用依赖 B@2.x 版本,那么 B@2.x 就应该被列在 A 的 peerDependencies 下,表示“如果你使用我,那么你也需要安装 B,并且至少是 2.x 版本”。

举个例子:

仔细查看 antd 的 package.json 中的 dependencies,发现并未安装 react 和 react-dom,但是随意打开一个组件你就会发现,它依赖 react 和 react-dom,如下:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

那 antd 是怎么运行起来的呢?根本原因就是peerDependencies,antd 的 package.json 中 peerDependencies,如下:

"peerDependencies": {
	"react": ">=16.0.0",
	"react-dom": ">=16.0.0"
}

antd 不能独立的运行,需要使用它的项目中安装了 react 和 react-dom 才可以运行。

所以,要想在你的项目中使用 antd,就必须得安装 react 和 react-dom。

作用:

  • 没有peerDependencies会发生什么?
    • 还是以 antd 为例,假设没有 peerDependencies,为了保证 antd 能够顺利的在宿主环境中运行起来,它不得不在 dependencies 中添加 react 和 react-dom:
"dependencies": {
	"react": ">=16.0.0",
	"react-dom": ">=16.0.0"
}

在 npm3 以前,也就是 node_modules 是嵌套而非平铺的年代。 如果开发者使用 antd 并且没有peerDependencies 的话,就需要安装两次 react 和 react-dom。

  • peerDependencies的优点
    • npm 3 以前避免重复安装(npm3之后 node_modules 层级发生了变化,直接解决了重复安装的问题)
    • 提示宿主环境
      • 当 npm 版本为 1、2 和 7 时,如果宿主环境没有安装 peerDependencies 中指定版本或更高版本的依赖,将自动安装,但自动安装将造成另外的问题,即可能宿主会在”不自主安装“的情形直接使用这些自动安装的依赖
      • 当 npm 版本 3 到 6 时,并不会自动安装,但是将收到未安装 peerDependency 的警告。如下:
npm WARN antd@4.19.3 requires a peer of react@>=16.9.0 but none is installed. You must install peer dependencies yourself.

发布配置

private

设置为 true,npm 将拒绝发布它,为了防止私有模块被无意间发布出去。

如果是私有项目,不希望发布到公共 npm 仓库上,可以将 private 设为 true。


yarn.lock、package-lock.json

package-lock.json

package-lock出现的主要目的时为了保证共同的开发者,所安装的具体的package是同一个version,而不是semver所规定的版本规则。package-lock.json 会固化当前安装的每个软件包的版本。当运行 npm update 时,package-lock.json 中的软件包的版本会被更新。

不同 npm 版本下 npm install 的规则

  • npm 5.0.x 不管 package.json 中依赖是否更新, 都会根据 package-lock.json 下载
  • npm 5.1.0后,当 package.json 中的依赖项有新版本时,npm install 会无视 package-lock.json 去下载新版本的依赖并更新 package-lock.json
  • 5.4.2版本后,
    • 如果只有一个 package.json 文件,运行 npm i 会根据它生成一个 package-lock.json 文件

    • 如果 package.json 的 semver-range version 和 package-lock.json 中版本兼容,即使 package.json 中有新的版本,也还是会根据 package-lock.json 下载

    • 如果手动修改了 package.json 的 version ranges,且和 package-lock.json 中版本不兼容,那么执行 npm i 时 package-lock.json 将会更新到兼容 package.json 的版本

如何生成 package-lock.json

npm install 安装 package.json 中的依赖,并生成 package-lock.json, 锁住依赖及依赖的依赖的版本

npm install <pkg-name> 安装某个软件包
npm update <pkg-name> 更新某个软件包

举个例子


yarn.lock

官方对 yarn.lock 文件的说明如下:

为了跨机器安装得到一致的结果, Yarn 需要比你配置在 package.json 中的依赖列表更多的信息。 Yarn 需要准确存储每个安装的依赖是哪个版本。

为了做到这样, Yarn 使用一个你项目根目录里的 yarn.lock 文件。这可以媲美其他像 Bundler 或 Cargo 这样的包管理器的 lockfiles。它类似于 npm 的 npm-shrinkwrap.json, 然而他并不是有损的并且它能创建可重现的结果。

如何生成 yarn.lock

yarn install 安装package.json里所有包,并将包及它的所有依赖项保存进yarn.lock

yarn install --flat 安装一个包的单一版本

yarn install --force 强制重新下载所有包

yarn install --production 只安装dependencies里的包

yarn upgrade 用于更新包到基于规范范围的最新版本

举个例子


区别

速度

yarn的速度要更快:

  • 并行安装:无论 npm 还是 Yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 Yarn 是同步执行所有任务,提高了性能。
  • 离线模式:如果之前已经安装过一个软件包,用Yarn再次安装时之间从缓存中获取,就不用像npm那样再从网络下载了。

文件结构

package-lock.json把所有的包的依赖顺序列出来,第一次出现的包名会提升到顶层,后面重复出现的将会放入被依赖包的node_modules当中。引起不完全扁平化问题。

显然yarn.lock锁文件把所有的依赖包都扁平化的展示了出来,对于同名包但是semver不兼容的作为不同的字段放在了yarn.lock的同一级结构中。

yarn.lock 文件中的信息也比 package.json 文件中详细了很多。


是否可提交

当前,我们有两个不同的程序包管理系统,它们都从package.json安装了相同的依赖项集,但是它们从两个不同的锁文件生成和读取。 NPM 5生成package-lock.json,而Yarn生成yarn.lock。

如果提交package-lock.json,则表示正在支持使用NPM 5安装依赖项的人员。如果提交yarn.lock,则表示正在支持使用Yarn安装依赖性的人。

是否选择提交yarn.lock或package-lock.json或同时提交,取决于在项目上开发的人仅使用Yarn还是NPM 5或同时使用这两种方式。如果您的项目是开源的,那么对社区最友好的事情可能就是同时提交这两者,并有一个自动化的过程来确保yarn.lock和package-lock.json始终保持同步。


yarn.lock 与 package-lock.json 相互转换

安装 synp:

npm install -g synp

命令行用法:

yarn.lock=>package-lock.json

yarn # be sure the node_modules folder dir and is updated
synp --source-file /path/to/yarn.lock
# will create /path/to/package-lock.json

package-lock.json=>yarn.lock

npm install # be sure the node_modules dir exists and is updated
synp --source-file /path/to/package-lock.json
# will create /path/to/yarn.lock

npm-shrinkwrap.json

npm-shrinkwrap.json 是在 npm5 之前(不包括5),主要是用来精确控制安装制定版本的npm包。它长得像package-lock.json,功能也很像。

如何生成 npm-shrinkwrap.json

****npm shrinkwrap 通过运行该命令行就可以生成 npm-shrinkwrap.json 文件

package-lock.json 与 npm-shrinkwrap.json 的区别与联系

npm版本

package-lock.json是npm5的新特性,也不向前兼容,如果npm版本是4或以下,需要使用npm-shrinkwrap.json

npm处理机制

在一个项目里,如果本身不存在这两个文件,那么在运行 npm install 时,会自动生成一个 package-lock.json ,或者在初始化一个项目 npm init 时,也会生成package-lock.json,安装信息会依据该文件进行,而不是单纯按照 package.json,这两个文件的优先级都比 package.json 高。

如果项目两个文件都存在,那么安装的依赖是依据npm-shrinkwrap.json来的,而忽略package-lock.json。官方说明如下:

npm shrinkwrap 命令创建和更新的文件将优先于任何其他现有或将有的 package-lock.json 文件。

运行命令 npm shrinkwrap 后,如果项目里不存在 package-lock.json ,那么会新建一个 npm-shrinkwrap.json 文件,如果存在 package-lock.json ,那么会把 package-lock.json 重命名为 npm-shrinkwrap.json 。

更新机制

npm-shrinkwrap.json只会在运行npm shrinkwrap才会创建/更新

package-lock.json会在修改pacakge.json或者node_modules时就会自动产生或更新了

发布包

package-lock.json不会在发布包中出现,就算出现了,也会遭到npm的无视。

npm-shrinkwrap.json可以在发布包中出现,建议库作者发布包时不要包含npm-shrinkwrap.json,因为这会阻止最终用户控制传递依赖性更新。第三方包如果在npm-shrinkwrap.json中锁定了版本号,引用该包的项目如果和该包有共同的依赖包,就可能会安装/打包多个版本的依赖包。


补充

semver

semver是一套跨语言的语义化版本号标准,简单解释为版本控制的机制,格式为X.Y.Z,X代表主版本,Y代表次要版本,Z代表补丁版本
package的开发者需要遵循semver的机制发布版本迭代:
X:代表大版本迭代,可能引起前后版本的不兼容
Y:代表小版本,功能性增加,不会引起低版本兼融问题
Z:bug修复补丁
如果没有package-lock的情况下,npm install默认安装最新的当前大版本的最新version,也就是说在不影响到兼容的情况下,默认安装最新版本。

semver通配符:

  • 符号^:表示安装不低于当前版本的应用,但大版本号需要相同,如react: "^16.4.0",16.4.0及以上的16.X.X都满足。
  • 无符号:无符号表示固定版本号,例如:react: "16.4.0",此时一定是安装16.4.0版本。

node_modules 层级

在 npm 的早期版本, npm 处理依赖的方式简单粗暴,以'递归的形式',在npm3版本 后采用了'扁平结构'

npm 2.x

执行 'npm install' 后,'npm' 根据 'dependencies' 和 'devDependencies' 属性中指定的包来确定第一层依赖,npm 2 会根据第一层依赖的子依赖,递归安装各个包到子依赖的 'node_modules' 中,直到子依赖不再依赖其他模块。执行完毕后,我们会看到 './node_modules' 这层目录中包含有我们 'package.json' 文件中所有的依赖包,而这些依赖包的子依赖包都安装在了自己的 'node_modules' 中 结构目录:
├── node_modules
│ ├── A@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0
│ ├── B@1.0.0
│ │ └── node_modules
│ │ │ └── D@2.0.0
│ └── C@1.0.0
│ │ └── node_modules
│ │ │ └── D@1.0.0

结构层级图

果你依赖的模块非常之多,你的 node_modules 将非常庞大,嵌套层级非常之深,出现下图的效果,并且也会导致接下来的问题:

  • 在不同层级的依赖中,可能引用了同一个模块,导致大量冗余,举个例子当我的 A,B,C 三个包中有相同的依赖 D 时,执行 npm install 后,D 会被重复下载三次
  • 在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。

npm 3.x

npm3.x 版本后采用了扁平化的解决方案,把依赖以及依赖的依赖平铺'node_modules' 文件夹下共享使用,npm 3 会遍历所有的节点,逐个将模块放在 'node_modules'的第一层,当发现有重复模块时,则丢弃, 如果遇到某些依赖版本不兼容的问题,则继续采用 npm 2 的处理方式,前面的放在 'node_modules' 目录中,后面的放在依赖树中。'过程说明':下图 'A','B' 都依赖'D(v0.0.1)'那么在安装的时候将'A'的依赖包都平铺到'node_modules',即将'D(v0.0.1)'平铺到了'node_modules' ,此时到了安装B包发现B包依赖在'node_modules' 根已经重复这时候就跳过了,但是安装C的时候发现C也依赖'D'但是依赖的版本不同,如果将C依赖的'D'放到'node_modules'根就因为重名问题覆盖掉此时解决方案就像'npm2.x'的时候就将C依赖的'D(v0.0.2)'安装到了C的'node_modules'下。

遗留问题待解决:

npm3是否彻底解决了npm2的问题