前言
作为前端开发者, NPM 这个包管理工具的重要性显而易见. 优点不再表述, 但一些缺点是为使用者诟病比较多的: 速度慢, 版本控制. 下面主要讨论下 NPM 的版本固化问题, 即 lock 文件.
NPM 语义化版本管理
对于 NPM 来说, 依赖相关的信息体现在 package.JSON 的 dependencies 里, 这里使用了 Semver(语义化版本来控制) 关于语义化版本的规范可以查看 https://docs.npmjs.com/misc/semver .
大致准则如下:
软件的版本通常由三位组成, 形如: X.Y.Z
版本是严格递增的, 此处是: 16.2.0 -> 16.3.0 -> 16.3.1
在发布重要版本时, 可以发布 alpha, rc 等先行版本
alpha 和 rc 等修饰版本的关键字后面可以带上次数和 meta 信息
版本格式:
发布者应该关注的是版本格式的规则
主版本号. 次版本号. 修订号
不同版本号递增规则如下:
主版本号 (major): 当你做了不兼容的 API 修改,
次版本号 (minor): 当你做了向下兼容的功能性新增, 可以理解为 Feature 版本,
修订号 (patch): 当你做了向下兼容的问题修正, 可以理解为 Bug fix 版本.
package.JSON 里面的依赖版本要求遵循上述规则的.
这样才能保证使用者引到期望的版本.
版本控制符
对于使用者来说, 版本前面的控制符是需要关注的, 这决定引用依赖是否与期望相同.
NPM 支持的符号是比较丰富的, 下面的版本符号均支持:
- { "dependencies" :
- { "foo" : "1.0.0 - 2.9999.9999",// 大于等于 1.0.0 小于 2.9999.9999
- "bar" : ">=1.0.2 <2.1.2", // 比较清晰 左闭右开
- "baz" : ">1.0.2 <=2.3.4", // 左开右闭
- "boo" : "2.0.1", // 规定版本
- "qux" : "<1.0.0 ||>=2.3.1 <2.4.5 ||>=2.5.2 <3.0.0", // 表达式也算清晰
- "asd" : "http://asdf.com/asdf.tar.gz"// 指定下载地址代替版本
- , "til" : "^1.2.3"// 同一主版本号, 不小于 1.2.3 即 1.x.y x>=2 y>=3
- , "elf" : "~1.2.3" // 同一主版本和次版本号 即 1.2.x x>= 2
- , "two" : "2.x" // 这个比较形象, x>=0 即 2.0.0 以上均可
- , "thr" : "3.3.x" // 同上 x>= 0 即 3.3.0 以上
- , "lat" : "latest" // 最新版本
- , "dyl" : "file:../dyl" // 从本地下载
- }
- }
根据上面的注释, 应该能看清不同符号的意思了. 这里有一篇文章比较生动的阐述了不同符号的范围, 有兴趣可以详细看下 https://my.oschina.net/u/658505/blog/666137
NPM install 默认使用 ^
这样的目的在于接受指定版本的更新, 例如依赖包的优化和小版本更新.
问题
不同环境依赖不一致
从上面可以看到, 语义化版本是没有强制约束的, 需要开发者自觉遵守规范定义.
常见情况如下:
测试环境完成之后上线出问题, 细究原因在于上线前某个依赖版发布了不兼容或者有 bug 版本, 恰好在发布时装了新版本.
所以才会有下面固化版本即锁版本需求的出现.
固化版本, 保证不同环境或者时间安装的都是相同依赖. 至于是否都应该固化版本, 下面再讨论.
固化版本方式
固话版本, 有下面三种方式:
NPM-shrinkwrap.JSON
该方式是比较早的锁定版本的方式,
与 package-lock.JSON 功能类似, 区别在于 NPM 包发布的时候可以发布上去.
推荐的使用情况是, 通过仓库上的发布过程来部署的应用, 即非库或者工具类.
例如: emons 和命令行工具, 想要被全局安装或者依赖, 此时强烈不建议坐着发布该文件, 因为将会阻止终端用户控制传递依赖的更新.
另外如果 package-lock.JSON 和 NPM-shrinkwrap.JSON 同时存在于项目根目录, package-lock.JSON 将会被忽略.
即该方式会将锁版本依赖通过 NPM 发布, 所以类库或者组件需要慎重.
使用方式:
- // 生成依赖 默认不包括 dev dependencies
- NPM shrinkwrap
- // 将 dev-dependencies 计算在内
- NPM shrinkwrap--dev
- package-lock.JSON
相对于 NPM-shrinkwrap , 其不会被发布到 NPM, 适用于应用程序, 即我们非工具类的项目.
npm5 以后 依赖都会默认增加该文件, 不过迭代了这么多版本, 不同版本 NPM 对 package-lock.JSON 的实现是不同的. 是在一直迭代和发展的
1,NPM 5.0.x 版本,
不管 package.JSON 怎么变, NPM i 时都会根据 lock 文件下载.
2,5.1.0 版本后
NPM install 会无视 lock 文件 去下载最新的 NPM 包
3,5.4.2 版本
如果改了 package.JSON, 且 package.JSON 和 lock 文件不同, 那么执行 NPM i 时 NPM 会根据 package 中的版本号以及语义含义去下载最新的包, 并更新至 lock.
如果两者是同一状态, 那么执行 NPM i 都会根据 lock 下载, 不会理会 package 实际包的版本是否有新.
该段内容参考自知乎用户, 详情请转 https://www.zhihu.com/question/264560841 https://www.zhihu.com/question/264560841
这样带来一个问题, 不同环境不同 NPM 版本, 对于同一项目, 依赖还是可能不同的....
非扁平依赖
对于同一 NPM 包不同版本的管理, npmlock 是非完全扁平化的处理:
所有的包的依赖顺序列出来, 第一次出现的包名会提升到顶层, 后面重复出现的将会放入被依赖包的 node_modules 当中
例如下面这个例子:
第一个依赖, 提升为顶层依赖
- // 顶层声明了 loader-utils 的依赖, 版本为 1.0.4
- "loader-utils": {
- "version": "1.0.4",
- "resolved": "http://r.npm.sankuai.com/loader-utils/download/loader-utils-1.0.4.tgz",
- "integrity": "sha1-E/Vhl/FSOjBYkSSLTHJEVAhIQmw=",
- "requires": {
- "big.js": "^3.1.3",
- "emojis-list": "^2.0.0",
- "json5": "^0.5.0"
- }
- }
- }
对于顶级依赖满足需求的, 则不再安装,
- "sass-loader": {
- "version": "7.1.0",
- "resolved": "http://r.npm.sankuai.com/sass-loader/download/sass-loader-7.1.0.tgz",
- "integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=",
- "dev": true,
- "requires": {
- // ^1.0.1 顶级依赖满足需求
- "loader-utils": "^1.0.1"
- }
- }
对于某些依赖不满足的, 则会在对应文件夹下面根据依赖安装符合版本. 例如 Less-loader
- "less-loader": {
- "version": "4.1.0",
- "resolved": "http://r.npm.sankuai.com/less-loader/download/less-loader-4.1.0.tgz",
- "requires": {
- // 1.0.4 不满足 ^1.1.0
- "loader-utils": "^1.1.0",
- },
- "dependencies": {
- "loader-utils": {
- "version": "1.2.3",
- "resolved": "http://r.npm.sankuai.com/loader-utils/download/loader-utils-1.2.3.tgz",
- "integrity": "sha1-H/XcaRHJ8KBiUxpMBLYJQGEIwsc=",
- "dev": true,
- "requires": {
- "big.js": "^5.2.2",
- "emojis-list": "^2.0.0",
- "json5": "^1.0.1"
- }
- }
- }
- }
package-lock.JSON 和 NPM-shrinkwrap.JSON 差别
package-lock.JSON 不会被发布到 NPM, 而 NPM-shrinkwrap 会被默认发布
非顶层的 package-lock.JSON 会被忽略, 而相同状态的 shrinkwrap 文件都会被保留.
NPM-shrinkwrap.JSON 在 NPM 2,3,4 版本均支持, package-lock.JSON 是 npm5 以后引入
两者同时存在, NPM-shrinkwrap.JSON 的优先级高于 package-lock.JSON
yarn.lcok
yarn 毕竟是针对 NPM 的缺点而生, 所以其自带版本控制, 默认依赖都会生成 yarn.lock 文件, 该文件会通过包名 + 版本来确定具体信息.
yarn-lock 语法
Yarn 用的是自己设计的格式, 语法上有点像 YAML(Yarn 2.0 中将会采用标准的 YAML).# 开头的行是注释.
第一行记录了包的名称及其语义化版本 (由 package.JSON 定义).
接下来的都做了缩进, 表示这些是该包的信息.
version 字段记录了包的确切版本.
resolved 字段记录了包的 URL. 此外, hash 中的值是 shasum.Yarn 记录的这个 shasum 来自于包的 versions[:version].dist.shasum(手动访问 https://registry.npmjs.org/:package 会得到一个 JSON, 解析此 JSON 可得)
dependencies 记录了包的依赖. 也许包的依赖还有依赖, 但不会在这里记录.
如下所示:
- pkg-dir@^1.0.0:
- version "1.0.0"
- resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
- integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
- dependencies:
- find-up "^1.0.0"
- pkg-dir@^2.0.0:
- version "2.0.0"
- resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
- integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
- dependencies:
- find-up "^2.1.0"
- pkg-dir@^3.0.0:
- version "3.0.0"
- resolved "http://r.npm.sankuai.com/pkg-dir/download/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
- integrity sha1-J0kCDyOe2ZCIGx9xIQ1R62UjvqM=
- dependencies:
- find-up "^3.0.0"
不过 Yarn 仅以 flatten 格式 描述各个包之间的依赖关系, 并依赖于其当前实现来创建目录结构. 这意味着如果其内部算法发生变化, 结构也会发生变化.
提升
你会发现, 有很多包你是没有直接依赖它们的, 但它们都出现在了 yarn.lock 中的顶层. 这就是提升, 它有两个意义:
记录依赖的依赖
正如上面所述, 依赖的依赖不会被直接记录在依赖的信息下 -- 它们会被提升, 这样可以简化整个 yarn.lock, 到时安装依赖的时候处理也变得简单, 因为你不必一层一层的嵌套下去来查找依赖的依赖的信息.
便于解决依赖版本冲突
依赖版本冲突是难免的, 当然有时候并不是版本冲突, 而只是语义化版本格式的版本记录不同. 举个例子,^5.0.0 与 5.x.x 在很多时候并不矛盾, 因此信息可以被合并. 如:
- chalk@^2.0.0, chalk@^2.0.1:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65"
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
注意第一行, yarn.lock 记录了 ^2.0.0 和 ^2.0.1, 而在添加 chalk 这个依赖的时候, 符合语义化版本的最新版本是 2.3.2(version 字段), 这个版本对于 ^2.0.0 和 ^2.0.1 这两个要求来说, 都满足了, 因此信息可以合并.
对于固化还是建议使用 yran.lock 实现, NPM 的 lock 在不同版本下存在的差异让人头疼.
是否应锁版本
这个争论是很正常的, 开始使用时, 我们也有过这样的讨论.
大家可以看下我们的场景再讨论:
1, 组内项目都依赖了自己开发的一个工具包 a
2, 该工具类依赖了一些第三方开源包
场景一:
当时某个知名包升级之后移除了某项功能的支持, 被 a 依赖, 导致该段时间后上线的项目全都出了问题.
场景二:
a 发现出了个 bug, 统一修复, 各个业务项目无需自行修改.
结合来看还是要具体分析, 对于自行维护或者确认无误的项目可以不锁版本. 对于第三方需要锁版本, 保证当前是可用的. 对于后期的 bug 修复, 不自行升级, 对于 bugfix 等小版本升级, 验证完成后再次锁版本.
.gitignore 是否应该忽略 lock 文件
对于 是否应该 package-lock.JSON 不应写进 .gitignore, 可以看下贺师俊大佬的解释:
如果你使用 lock 机制, 则应该将 package-lock.JSON 提交到 repo 中. 比如 vue 采取了该策略. 如果你不使用 lock 机制, 则应该加入 .npmrc 文件, 内容为 package-lock=false , 并提交到 repo 中. 比如 ESLint 采取了该策略.
还是回到了那个问题, 是否应该锁版本.
对于类库而言, 锁定依赖版本是 绝对不可行 的. 否则只要应用中使用了两个以上的依赖, 都有概率出现绝对不存在可兼容版本的情况. 这样只是单纯的把问题抛给了最终应用, 并没有解决问题.
最终应用是否锁也有待考虑.
问题出在源码的可靠性不得到保证, 本身语义化没有问题. 但是又 bug 正常, 所以业务项目才锁
结束语
参考文章
https://docs.npmjs.com/files/package.json
针对是否应该固化版本和如何固化版本, 因为水平有限也只是给出了自己的一点看法. 希望能对有需要的同学有所帮助.
来源: https://www.cnblogs.com/pqjwyn/p/11163146.html