正文
继上篇-# 字节的一个小问题 npm 和 yarn不一样吗?的小作文写完之后,有点惊讶大家对于知识的渴望度;也充分暴露了自己其实对很多东西还是不懂.这里会继续根据之前提出的几个问题做一个具体的更新。
首先,可能还会以一种啰嗦的态度去写,也可能最近被拒绝太多次了,只能重新捡起我的笔来弥补大家之前的热情。
为什么要lockfiles,要不要提交lockfiles到仓库?
其实从前文中 我们已经知道了,npm 从v5
开始, 增加了package-lock.json
文件。那么 package-lock.json
文件的作用是什么呢? 锁定依赖的安装结构, 这么做的目的是为了保证在任意的机器上我们去执行npm install
都会得到完全相同的 node_modules
安装结果。
这里其实我是有一个疑问的?为啥单一的 package.json
不能确定唯一的依赖树呢?
- 首先是不同版本的npm的安装依赖的策略和算法可能是不一样的
- npm install 将根据
package.json
中的 semver-range version 更新依赖,可能某些依赖自上次安装以后,己经发布了新的版本。
因此, 保证能够完整准确的还原项目依赖
就是lockfiles出现的原因。
首先我们这里需要了解一下 package-lock.json
的作用机制。 举个例子:
"@babel/core": {
"version": "7.2.0",
"integrity": "sha1-pN04FJAZmOkzQPAIbphn/voWOto=",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
// ...
},
"dependencies": {
"@babel/generator": {
"version": "7.2.0",
"resolved": "http://www.npm.com/@babel%2fgenerator/-/generator-7.2.0.tgz",
"integrity": "sha1-6vOCH6AwHZ1K74jmPUvMGbc7oWw=",
"dev": true,
"requires": {
"@babel/types": "^7.2.0",
"jsesc": "^2.5.1",
"lodash": "^4.17.10",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
}
},
// ...
}
},
// ...
}
那么, 通过上面的示例, 我们可以看到: 一个 package-lock.json 的 dependency 主要是有以下的几部分组成的:
- Version: 依赖包的版本号
- Resolved: 依赖包的安装源(其实就是可以理解为下载地址)
- Intergrity: 表明完整性的 Hash 值
- Dev: 表示该模块是否为顶级模块的开发依赖或者是一个的传递依赖关系
- requires: 依赖包所需要的所有依赖项,对应依赖包 package.json 里 dependencices 中的依赖项
- dependencices: 依赖包 node_modeles 中依赖的包(特殊情况下才存在)
事实上, 并不是所有的子依赖都有 dependencies
属性,只有子依赖的依赖和当前已安装在根目录的 node_modules
中的依赖冲突之后, 才会有这个属性。 这可能涉及嵌套情况的依赖管理,大家找些资料看看。
至于我们要不要提交 lockfiles 到仓库中? 这个就需要看我们具体的项目的定位了。
-
如果是开发一个应用, 我的理解是
package-lock.json
文件提交到代码版本仓库.这样可以保证项目中成员、运维部署成员或者是 CI 系统, 在执行npm install
后, 保证在不同的节点能得到完全一致的依赖安装的内容 -
如果你的目标是开发一个给外部环境用的库,那么就需要认真考虑一下了, 因为库文件一般都是被其他项目依赖的,在不使用 package-lock.json的情况下,就可以复用主项目已经加载过的包,减少依赖重复和体积
-
如果说我们开发的库依赖了一个精确版本号的模块, 那么在我们去提交 lockfiles 到仓库中可能就会出现, 同一个依赖被不同版本都被下载的情况。如果我们作为一个库的开发者, 其实如果真的使用到某个特定的版本依赖的需求, 那么定义peerDependencies 是一个更好的选择。
所以, 我个人比较推荐的一个做法是:把 package-lock.json一起提交到仓库中去, 不需要 ignore. 但是在执行 npm publish 命令的时候,也就是发布一个库的时候, 它其实应该是被忽略的不应该被发布出去的。
当然,我这里了解到对 lockfiles
的处理,可能需要一个更加细颗粒度的理解,这里我会推荐大家去结合前文去理解。
-
在npm早期所用到的锁定版本的方式是通过使用
npm-shrinkwrap.json
, 它与之前我们提到的package-lock.json
最大的不同之处在于: npm 包发布的时候默认是将npm-shrinkwrap.json
发布的, 因此类库和组件需要慎重。 -
我们在可以使用到
package-lock.json
是在 npm v5.x版本新增的特性,而在 npm v5.6之后才趋于逐步稳定的状态, 在 5.0 - 5.6中间, 其实是对package-lock.json
的处理逻辑进行过几次更新。 -
在 npm v5.0.x版本中, npm install 时都会根据 package-lock.json 文件下载,不管你的
package.json
的内容究竟是什么。 -
npm v5.1.0 版本到 npm v5.4.2, npm install 会无视
package-lock.json
文件下载的, 会去下载最新版本的 npm 包,并且会更新package-lock.json
. -
npm 5.4.2 版本之后呢,我们继续细化分析:
- 如果在我们的实际开发的项目中, 只有
package.json
文件时, npm install 之后, 会根据它生成一个package-lock.json
文件 - 如果在项目中存在了
package.json
和package-lock.json
文件, 同时package.json
的semver-range 版本 和package-lock.json
中版本兼容,即使此时会有新的适用的版本,npm install
还是会根据package-lock.json
下载的 - 如果在项目中存在了
package.json
和package-lock.json
文件, 同时package.json
的semver-range 版本 和package-lock.json
中版本不兼容,npm install
会把package-lock.json
更新到兼容package.json
的版本。 - 如果
package-lock.json
和npm-shrinkwrap.json
同时存在于项目的根目录中的时候,package-lock.json
将会被忽略的。
- 如果在我们的实际开发的项目中, 只有
对于上面的过程分析,我之前的文章中做了一个过程的流程图的分析,大家可以结合前文做一个更加精细化的理解
那么,下面我们继续来看下一个问题, 我们不管是使用 npm 或 yarn 都有可能会把包依赖安装到不同的依赖模块中, 你有没有去思考为什么会这样做呢?这么做会有什么必要关系和我们之后的开发和发布?
为什么会有 xxxDependencies?
其实, npm 设计了以下的几种依赖类型声明:
- dependencies 项目依赖
- devDependencies 开发依赖
- peerDependencies 同版本的依赖
- bundledDependencies 捆绑依赖
- optionalDependencies 可选依赖
它们起到的作用和声明意义是各不相同的。下面我们来具体介绍一下:
dependencies 表示项目依赖,这些依赖都会成为你的线上生产环境中的代码组成的部分。当 它关联到 npm 包被下载的时候, dependencies下的模块也会作为依赖, 一起被下载。
devDependencies表示开发依赖, 不会被自动下载的。
因为 devDependencies 一般是用于开发阶段起作用或是只能用于开发环境中被用到的。 比如说我们用到的 Webpack
,预处理器 babel-loader
、scss-loader
,测试工具E2E
等, 这些都相当于是辅助的工具包, 无需在生产环境被使用到的。
这里有一点还是需要我去啰嗦一下的,并不是只有在dependencies中的模块才会被一起打包, 而是在 devDependencies 中的依赖一定不会被打包的。
实际上, 依赖是否是被打包,完全是取决你的项目里的是否是被引入了该模块。
peerDependencies 表示同版本的依赖, 简单一点说就是: 如果你已经安装我了, 那么你最好也安装我对应的依赖。 这里举个小例子: 加入我们需要开发一个react-ui 就是一个基于react
开发的UI组件库, 它本身是会需要一个宿主环境去运行的, 这个宿主环境还需要指定的 react
版本来搭配使用的, 所以需要我们去 package.json
中去配置:
"peerDependencies": {
"React": "^17.0.0"
}
bundledDependencies 和 npm pack 打包命令有关。假设我们在 package.json
中有如下的配置:
{
"name": "test",
"version": "1.0.0",
"dependencies": {
"dep": "^0.0.2",
...
},
"devDependencies": {
...
"devD1": "^1.0.0"
},
"bundledDependencies": [
"bundleD1",
"bundleD2"
]
}
那我们此时执行 npm pack
的时候, 就会生成一个 test-1.0.0.tgz
的压缩包, 在该压缩包中还包含了 bundleD1
和 bundleD2
两个安装包。 实际使用到 这个压缩包的时候
npm install test-1.0.0.tgz
的命令时, bundleD1
和 bundleD2
也会被安装的。
这里其实也有需要注意的是: 在 bundledDependencies 中指定的依赖包, 必须先在dependencies 和 devDependencies 声明过, 否则 npm pack 阶段是会报错的。
optionalDependencies
表示可选依赖,就是说当你安装对应的依赖项安装失败了, 也不会对整个安装过程有影响的。一般我们很少会用到它, 这里我是 不建议大家去使用, 可能会增加项目的不确定性和复杂性。
到现在为止,大家是不是已经对 npm 规范中相关依赖声明的含义了呢? 接下来我想和大家去聊一聊版本的规范, 我们一起来看一下解析依赖库锁版本的行为。
版本规范——依赖库锁版本行为解析
首先, npm 遵循的是 SemVer
版本规范, 至于具体的内容这个链接供大家学习语义化版本我就不去啰嗦了。 我们会主要针对一个细节点---- 依赖库锁版本的行为。
每个 vue 包的新版本发布时,一个相应版本的 vue-template-compiler 也会随之发布。编译器的版本必须和基本的 vue 包保持同步,这样 vue-loader 就会生成兼容运行时的代码。这意味着你每次升级项目中的 vue 包时,也应该匹配升级 vue-template-compiler。
根据上面说的意思, 我们如果作为一个库的开发者需要考虑的是: 如何去保证依赖包之间的强制的最低版本的要求?
其实我们可以去借鉴一下 create-react-app
的做法, 在 create-react-app
的核心 react-script
中, 它利用了verifyPackageTree
方法, 对业务项目中的依赖进行了一系列的对比和限制的工作。 我们可以去看一下源码:
function verifyPackageTree() {
const depsToCheck = [
'babel-eslint',
'babel-jest',
'babel-loader',
'eslint',
'jest',
'webpack',
'webpack-dev-server',
];
const getSemverRegex = () =>
/\bv?(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?\b/gi;
const ownPackageJson = require('../../package.json');
const expectedVersionsByDep = {};
depsToCheck.forEach(dep => {
const expectedVersion = ownPackageJson.dependencies[dep];
if (!expectedVersion) {
throw new Error('This dependency list is outdated, fix it.');
}
if (!getSemverRegex().test(expectedVersion)) {
throw new Error(
`The ${dep} package should be pinned, instead got version ${expectedVersion}.`
);
}
expectedVersionsByDep[dep] = expectedVersion;
});
let currentDir = __dirname;
while (true) {
const previousDir = currentDir;
currentDir = path.resolve(currentDir, '..');
if (currentDir === previousDir) {
// We've reached the root.
break;
}
const maybeNodeModules = path.resolve(currentDir, 'node_modules');
if (!fs.existsSync(maybeNodeModules)) {
continue;
}
depsToCheck.forEach(dep => {
const maybeDep = path.resolve(maybeNodeModules, dep);
if (!fs.existsSync(maybeDep)) {
return;
}
const maybeDepPackageJson = path.resolve(maybeDep, 'package.json');
if (!fs.existsSync(maybeDepPackageJson)) {
return;
}
const depPackageJson = JSON.parse(
fs.readFileSync(maybeDepPackageJson, 'utf8')
);
const expectedVersion = expectedVersionsByDep[dep];
if (!semver.satisfies(depPackageJson.version, expectedVersion)) {
console.error(//...);
process.exit(1);
}
});
}
}
其实我们去看这一段代码的时候, create-react-app
会对项目中的babel-eslint、 babel-jest、babel-loader、eslint、jest、webpack、webpack-dev-server 这些核心的依赖都会去进行检索的 --- 是否是符合 create-react-app 对于这些核心模块依赖的版本要求。如果不符合依赖版本要求, 那么 create-react-app 的构建过程会直接报错并退出的
。
那么为啥 create-react-app
这么做的理由是什么呢?
我的理解是:需要上述依赖项的某些确定的版本, 以保障 create-react-app 源码相关的功能稳定
不知道你对于这样的一种处理方式会不会有一些思考呢?
那么最好我想去分享一些,自己在对npm实操的一些小建议, 大家伙可以来讨论一下是不是可行的
或许是最佳的实操建议
下面我会给出具体的实操的建议, 供大家来参考:
-
优先去使用 npm 官方已经稳定的支持的版本, 以保证 npm 的最基本先进性和稳定性
-
当我们的项目第一次去搭建的时候, 使用
npm install
安装依赖包, 并去提交package.json、package-lock.json
, 至于node_moduled
目录是不用提交的。 -
当我们作为项目的新成员的时候,
checkout/clone
项目的时候, 执行一次npm install
去安装依赖包。 -
当我们出现了需要升级依赖的需求的时候:
- 升级小版本的时候, 依靠 npm update
- 升级大版本的时候, 依靠 **npm install@ **
- 当然我们也有一种方法, 直接去修改 package.json 中的版本号, 并去执行 npm install 去升级版本
- 当我们本地升级新版本后确认没有问题之后, 去提交新的 package.json 和 **package-lock.json **文件。
-
对于降级的依赖包的需求: 我们去执行npm install @ 命令后,验证没有问题之后, 是需要提交新的 package.json 和 package-lock.json 文件。
-
删除某些依赖的时候:
- 当我们执行 npm uninstall 命令后, 需要去验证,提交新的 package.json 和 package-lock.json 文件。
- 或者是更加暴力一点, 直接操作
package.json
, 删除对应的依赖, 执行 npm install 命令, 需要去验证,提交新的package.json 和 package-lock.json 文件。
-
当你把更新后的package.json 和 package-lock.json提交到代码仓库的时候, 需要通知你的团队成员, 保证其他的团队成员拉取代码之后, 更新依赖可以有一个更友好的开发环境保障持续性的开发工作。
-
任何时候我们都不要去修改 package-lock.json,这是交过智商税的。
-
如果你的 package-lock.json 出现冲突或问题, 我的建议是将本地的 package-lock.json文件删掉, 然后去找远端没有冲突的 package.json 和 package-lock.json, 再去执行
npm install
命令。
到这里我的啰嗦要和大家说再见了, 希望大家有一个愉快的周末, 如果有啥机会可以推荐我的可以留言,感谢各位大佬。