字节的一个小问题npm 和 yarn不一样吗?(续篇)

10,084 阅读11分钟

正文

上篇-# 字节的一个小问题 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的处理,可能需要一个更加细颗粒度的理解,这里我会推荐大家去结合前文去理解。

  1. 在npm早期所用到的锁定版本的方式是通过使用 npm-shrinkwrap.json, 它与之前我们提到的 package-lock.json 最大的不同之处在于: npm 包发布的时候默认是将 npm-shrinkwrap.json 发布的, 因此类库和组件需要慎重。

  2. 我们在可以使用到 package-lock.json 是在 npm v5.x版本新增的特性,而在 npm v5.6之后才趋于逐步稳定的状态, 在 5.0 - 5.6中间, 其实是对 package-lock.json 的处理逻辑进行过几次更新。

  3. 在 npm v5.0.x版本中, npm install 时都会根据 package-lock.json 文件下载,不管你的 package.json的内容究竟是什么。

  4. npm v5.1.0 版本到 npm v5.4.2, npm install 会无视 package-lock.json 文件下载的, 会去下载最新版本的 npm 包,并且会更新 package-lock.json.

  5. npm 5.4.2 版本之后呢,我们继续细化分析:

    • 如果在我们的实际开发的项目中, 只有package.json文件时, npm install 之后, 会根据它生成一个 package-lock.json 文件
    • 如果在项目中存在了 package.jsonpackage-lock.json 文件, 同时 package.jsonsemver-range 版本 和 package-lock.json 中版本兼容,即使此时会有新的适用的版本, npm install 还是会根据 package-lock.json下载的
    • 如果在项目中存在了 package.jsonpackage-lock.json 文件, 同时 package.jsonsemver-range 版本 和 package-lock.json 中版本不兼容,npm install 会把 package-lock.json 更新到兼容 package.json的版本。
    • 如果 package-lock.jsonnpm-shrinkwrap.json 同时存在于项目的根目录中的时候, package-lock.json 将会被忽略的。

对于上面的过程分析,我之前的文章中做了一个过程的流程图的分析,大家可以结合前文做一个更加精细化的理解

那么,下面我们继续来看下一个问题, 我们不管是使用 npmyarn 都有可能会把包依赖安装到不同的依赖模块中, 你有没有去思考为什么会这样做呢?这么做会有什么必要关系和我们之后的开发和发布?

为什么会有 xxxDependencies?

其实, npm 设计了以下的几种依赖类型声明:

  • dependencies 项目依赖
  • devDependencies 开发依赖
  • peerDependencies 同版本的依赖
  • bundledDependencies 捆绑依赖
  • optionalDependencies 可选依赖

它们起到的作用和声明意义是各不相同的。下面我们来具体介绍一下:

dependencies 表示项目依赖,这些依赖都会成为你的线上生产环境中的代码组成的部分。当 它关联到 npm 包被下载的时候, dependencies下的模块也会作为依赖, 一起被下载。

devDependencies表示开发依赖, 不会被自动下载的。因为 devDependencies 一般是用于开发阶段起作用或是只能用于开发环境中被用到的。 比如说我们用到的 Webpack,预处理器 babel-loaderscss-loader,测试工具E2E等, 这些都相当于是辅助的工具包, 无需在生产环境被使用到的。

这里有一点还是需要我去啰嗦一下的,并不是只有在dependencies中的模块才会被一起打包, 而是在 devDependencies 中的依赖一定不会被打包的。 实际上, 依赖是否是被打包,完全是取决你的项目里的是否是被引入了该模块

peerDependencies 表示同版本的依赖, 简单一点说就是: 如果你已经安装我了, 那么你最好也安装我对应的依赖。 这里举个小例子: 加入我们需要开发一个react-ui 就是一个基于react 开发的UI组件库, 它本身是会需要一个宿主环境去运行的, 这个宿主环境还需要指定的 react版本来搭配使用的, 所以需要我们去 package.json中去配置:

"peerDependencies": {
    "React": "^17.0.0"
}

bundledDependenciesnpm 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的压缩包, 在该压缩包中还包含了 bundleD1bundleD2 两个安装包。 实际使用到 这个压缩包的时候

npm install test-1.0.0.tgz 的命令时, bundleD1bundleD2 也会被安装的。

这里其实也有需要注意的是: 在 bundledDependencies 中指定的依赖包, 必须先在dependencies 和 devDependencies 声明过, 否则 npm pack 阶段是会报错的。

optionalDependencies表示可选依赖,就是说当你安装对应的依赖项安装失败了, 也不会对整个安装过程有影响的。一般我们很少会用到它, 这里我是 不建议大家去使用, 可能会增加项目的不确定性和复杂性。

到现在为止,大家是不是已经对 npm 规范中相关依赖声明的含义了呢? 接下来我想和大家去聊一聊版本的规范, 我们一起来看一下解析依赖库锁版本的行为。

版本规范——依赖库锁版本行为解析

首先, npm 遵循的是 SemVer版本规范, 至于具体的内容这个链接供大家学习语义化版本我就不去啰嗦了。 我们会主要针对一个细节点---- 依赖库锁版本的行为。

vue 官方有这样的内容:

每个 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-eslintbabel-jestbabel-loadereslintjestwebpackwebpack-dev-server 这些核心的依赖都会去进行检索的 --- 是否是符合 create-react-app 对于这些核心模块依赖的版本要求。如果不符合依赖版本要求, 那么 create-react-app 的构建过程会直接报错并退出的

那么为啥 create-react-app这么做的理由是什么呢?

我的理解是:需要上述依赖项的某些确定的版本, 以保障 create-react-app 源码相关的功能稳定

不知道你对于这样的一种处理方式会不会有一些思考呢?

那么最好我想去分享一些,自己在对npm实操的一些小建议, 大家伙可以来讨论一下是不是可行的

或许是最佳的实操建议

下面我会给出具体的实操的建议, 供大家来参考:

  1. 优先去使用 npm 官方已经稳定的支持的版本, 以保证 npm 的最基本先进性和稳定性

  2. 当我们的项目第一次去搭建的时候, 使用 npm install 安装依赖包, 并去提交 package.json、package-lock.json, 至于node_moduled目录是不用提交的。

  3. 当我们作为项目的新成员的时候, checkout/clone项目的时候, 执行一次 npm install 去安装依赖包。

  4. 当我们出现了需要升级依赖的需求的时候:

    • 升级小版本的时候, 依靠 npm update
    • 升级大版本的时候, 依靠 **npm install@ **
    • 当然我们也有一种方法, 直接去修改 package.json 中的版本号, 并去执行 npm install 去升级版本
    • 当我们本地升级新版本后确认没有问题之后, 去提交新的 package.json 和 **package-lock.json **文件。
  5. 对于降级的依赖包的需求: 我们去执行npm install @ 命令后,验证没有问题之后, 是需要提交新的 package.jsonpackage-lock.json 文件。

  6. 删除某些依赖的时候:

    • 当我们执行 npm uninstall 命令后, 需要去验证,提交新的 package.json 和 package-lock.json 文件。
    • 或者是更加暴力一点, 直接操作 package.json, 删除对应的依赖, 执行 npm install 命令, 需要去验证,提交新的package.jsonpackage-lock.json 文件。
  7. 当你把更新后的package.jsonpackage-lock.json提交到代码仓库的时候, 需要通知你的团队成员, 保证其他的团队成员拉取代码之后, 更新依赖可以有一个更友好的开发环境保障持续性的开发工作。

  8. 任何时候我们都不要去修改 package-lock.json,这是交过智商税的。

  9. 如果你的 package-lock.json 出现冲突或问题, 我的建议是将本地的 package-lock.json文件删掉, 然后去找远端没有冲突的 package.jsonpackage-lock.json, 再去执行 npm install 命令。

到这里我的啰嗦要和大家说再见了, 希望大家有一个愉快的周末, 如果有啥机会可以推荐我的可以留言,感谢各位大佬。