npm&yarn项目级应用

359 阅读8分钟

问题

在前端工程化中, 我们离不开npm及yarn的使用 在使用过程中我们通常会有如下疑问:

  • 一个项目中有人使用yarn, 有人使用npm, 这会引发什么问题。
  • 把所有依赖都安装到 dependencies 中,不区分 devDependencies 会有问题吗?
  • 我们是否应该提交 lockfiles 文件到项目仓库呢?
  • 我们的应用依赖了公共库 A 和公共库 B,同时公共库 A 也依赖了公共库 B,那么公共库 B 会被多次安装或重复打包吗?
  • 项目依赖出现问题时,删除大法好,即删除 node_modules 和 lockfiles,再重新 install,这样操作是否存在风险?

要得到这些问题的答案, 我们须从npm安装机制及背后思想说起

npm 内部机制和核心原理

npm安装机制及背后思想

管理工具安装机制说明
npm局部安装优点: 减轻包作者的API兼容压力 缺点: 同一个依赖包可能在电脑上进行多次安装
Ruby Gem全局安装
Python pip全局安装

对于一些工具模块比如 supervisor 和 gulp,你仍然可以使用全局安装模式,这样方便注册 path 环境变量,我们可以在任何地方直接使用 supervisor、 gulp 这些命令。(不过,一般还是建议不同项目维护自己局部的 gulp 开发工具以适配不同项目需求。)

下图为npm install安装机制

image.png

需要注意的是: npm install执行后, 检查并获取npm配置, 这里的优先级为: 项目级的.npmrc文件 > 用户级的.npmrc文件 > 全局级的.npmrc文件 > npm内置的.npmrc文件 然后检查项目中是否有package-lock.json文件, 如果有, 则检查package-lock.json与package.json中声明的依赖是否一致

  • 一致, 直接使用package-lock.json中的信息, 从缓存或者网络资源中加载依赖

  • 不一致, 按照npm版本进行处理 如果没有, 则根据package.json递归构建依赖树。然后按照构建好的依赖树下载完整的依赖资源, 在下载时就会检查是否存在相关资源缓存:

  • 存在, 则将缓存内容解压到node_modules中,

  • 不存在, 就先从npm远程仓库下载包, 检验包的完整性, 并添加到缓存, 同时解压到node_modules。 最后生成package-lock.json。 构建依赖树时, 当前依赖项目不管其是直接依赖还是子依赖的依赖, 都应该按照扁平化原则, 优先将其放置在node_moudles根目录(最新版本npm规范)。在这个过程中, 遇到相同模块就判断已放置在依赖树中的模块版本是否符合新模块的版本范围, 如果符合则跳过;不符合则在当前模块的node_modules下放置该模块。

npm不同版本会有不同处理情况, 在团队中使用npm的最佳实践为: 同一个项目团队, 应该保证npm版本的一致。

npm缓存机制

对于一个依赖包的统一版本进行本地化缓存, 是当代依赖包管理工具的一个常见设计。 以下提到的缓存策略是从npm v5版本开始的。在npm v5版本之前, 每个缓存的模块在~/.npm文件夹中以模块名的形式直接存储, 存储结构是: {cache}/{name}/{version}。

image.png

npm install执行时, 通过pacote把相应的包解压在对应的node_modules下面。npm在下载依赖时, 先下载到缓存当中, 再解压到项目node_modules下。pacote依赖 npm-registry-fetch来下载包, npm-registry-fetch可以通过设置cache属性, 在给定的路径下根据IETF RFC 7234(datatracker.ietf.org/doc/rfc7234…) 生成缓存数据。 接着, 在每次安装资源时, 根据package-lock.json中存储的integrity、version、name信息生成一个唯一的key, 这个key能够对应到index-v5目录下的缓存记录。如果发现有缓存资源, 就会找到tar包的hash, 根据这个hash再去找缓存的tar包, 并再次通过pacote把对应的二进制文件解压到对应项目node-modules下面, 省去了网络下载资源的开销。

npm-init自定义

npm init 命令本身并不复杂,它其实就是调用 shell 脚本输出一个初始化的 package.json 文件。 那么相应地,我们要自定义 npm init 命令,就是写一个 node 脚本而已,它的 module.exports 即为 package.json 配置内容。 为了实现更加灵活的自定义功能,我们可以使用 prompt() 方法,获取用户输入并动态产生的内容:

const desc = prompt('请输入项目描述', '项目描述...')
module.exports = {
  key: 'value',
  name: prompt('name?', process.cwd().split('/').pop()),
  version: prompt('version?', '0.0.1'),
  description: desc,
  main: 'index.js',
  repository: prompt('github repository url', '', function (url) {
    if (url) {
      run('touch README.md');
      run('git init');
      run('git add README.md');
      run('git commit -m "first commit"');
      run(`git remote add origin ${url}`);
      run('git push -u origin master');
    }
    return url;
  })
}

假设该脚本名为 .npm-init.js,我们执行下述命令来确保 npm init 所对应的脚本指向正确的文件:

npm config set init-module ~\.npm-init.js

我们也可以通过配置 npm init 默认字段来自定义 npm init 的内容:

npm config set init.author.name "Lucas"
npm config set init.author.email "lucasXXXXXX@gmail.com"
npm config set init.author.url "lucasXXXXX.com"
npm config set init.license "MIT"

npx使用

npx 由 npm v5.2 版本引入,解决了 npm 的一些使用快速开发、调试,以及项目内使用全局模块的痛点。 npx使用eslint

npx eslint --init
npx eslint yourfile.js

为什么npx操作起来如此便捷呢? 因为它可以直接执行node_modules/.bin文件夹下的文件。在运行命令时, npx可以自动去node-modules/.bin路径和环境变量$PATH里面检查命令是否存在, 而不需要再在package.json中定义相关的script。 npx另一个更实用的好处是: npx执行模块时会优先安装依赖, 但是在安装执行后便删除此依赖, 这就避免了全局安装模块带来的问题。

npm 多源镜像和企业级部署私服原理

npm 中的源(registry),其实就是一个查询服务。以 npmjs.org 为例,它的查询服务网址是 registry.npmjs.org/。这个网址后面跟上模块… JSON 对象,里面是该模块所有版本的信息。比如,访问 registry.npmjs.org/react,就会看到 react 模块所有版本的信息。

我们可以通过npm config set命令来设置安装源或者某个 scope 对应的安装源,很多企业也会搭建自己的 npm 源。我们常常会碰到需要使用多个安装源的项目,这时就可以通过 npm-preinstall 的钩子,通过 npm 脚本,在安装公共依赖前自动进行源切换:

"scripts": {
    "preinstall": "node ./bin/preinstall.js"
}

其中 preinstall.js 脚本内容,具体逻辑为通过 node.js 执行npm config set命令,代码如下:

require(' child_process').exec('npm config get registry', function(error, stdout, stderr) {
  if (!stdout.toString().match(/registry\.x\.com/)) {
    exec('npm config set @xscope:registry https://xxx.com/npm/')
  }
})

Yarn

Yarn是一个由Facebook、Google、Exponent和Tilde构建的新的JavaScript包管理器。它的出现是为了解决历史上npm的某些不足(比如npm对于依赖的完整性和一致性保障, 以及npm安装速度过慢的问题等)。虽然npm目前经过版本迭代汲取了Yarn一些优势特点(比如一致性安装校验算法等),但我们依然有必要关注Yarn的思想和理念。

Yarn和npm的关系, 有点像当年的lo.js和Node.js, 殊途同归,都是为了进一步解放和优化生产力。这里需要说明的是, 不管哪种工具, 你应该做的就是全面了解其思想, 优劣胸中有数, 这样才能驾驭它, 为自己的项目架构服务。

优点说明npm说明
确定性通过yarn.lock等机制, 保证了确定性。即不管安装顺序如何, 相同的依赖关系在任何机器和环境下, 都可以以相同的方式被安装npm v5之前, 没有package-lock.json机制, 只有默认并不会使用的npm-shrinkwrap.json
采用模块扁平安装模式将依赖包的不同版本, 按照一定策略, 归结为单个版本, 以避免创建多个副本造成冗余npm目前也有相同的优化
网络性更好Yarn采用了请求排队的理念, 类似并发连接池, 能够更好地利用网络资源;同时引入更好的安装失败时的重试机制。
采用缓存机制, 实现了离线模式npm目前也有类似实现

yarn-lock没有使用JSON格式, 而是采用了一种自定义的标记格式, 新的格式仍然保持了较高的可读性

"@babel/cli@^7.1.6", "@babel/cli@^7.5.5":
  version "7.8.4"
  resolved "http://npm.in.zhihu.com/@babel%2fcli/-/cli-7.8.4.tgz#505fb053721a98777b2b175323ea4f090b7d3c1c"
  integrity sha1-UF+wU3IamHd7KxdTI+pPCQt9PBw=
  dependencies:
    commander "^4.0.1"
    convert-source-map "^1.1.0"
    fs-readdir-recursive "^1.1.0"
    glob "^7.0.0"
    lodash "^4.17.13"
    make-dir "^2.1.0"
    slash "^2.0.0"
    source-map "^0.5.0"
  optionalDependencies:
    chokidar "^2.1.8"

相比npm,Yarn另外一个显著区别是yarn.lock中子依赖的版本号不是固定版本这就说明单独一个yarn.lock确定不了node_modules目录结构, 还需要和package.json文件进行配合。 如果想在项目中进行npm/Yarn切换, 有个专门的synp工具, 可以将yarn.lock转换为package-lock.json,反之亦然。

Yarn安装过程

image.png

image.png

image.png

  • 在经过解析包这一步骤之后, 我们就确定了所有依赖的具体版本信息以及下载地址。
  • 如何判断缓存中是否存在当前的依赖包?Yarn会根据cacheFolder+slug+node_modules+pkg.name生成一个path, 判断系统中是否存在该path, 如果存在证明已经有缓存, 不用重新下载。这个path也就是依赖包缓存的具体路径

npm ci 应用

最佳实操建议

  1. 优先使用 npm v5.4.2 以上的 npm 版本,以保证 npm 的最基本先进性和稳定性。

  2. 项目的第一次搭建使用 npm install 安装依赖包,并提交 package.json、package-lock.json,而不提交 node_modules 目录。

  3. 其他项目成员首次 checkout/clone 项目代码后,执行一次 npm install 安装依赖包。

  4. 对于升级依赖包的需求:

依靠 npm update 命令升级到新的小版本;

依靠 npm install @ 升级大版本;

也可以手动修改 package.json 中版本号,并执行 npm install 来升级版本;

本地验证升级后新版本无问题,提交新的 package.json、package-lock.json 文件。

  1. 对于降级依赖包的需求:执行 npm install @ 命令,验证没问题后,提交新的 package.json、package-lock.json 文件。

  2. 删除某些依赖:

执行 npm uninstall 命令,验证没问题后,提交新的 package.json、package-lock.json 文件;

或者手动操作 package.json,删除依赖,执行 npm install 命令,验证没问题后,提交新的 package.json、package-lock.json 文件。

  1. 任何团队成员提交 package.json、package-lock.json 更新后,其他成员应该拉取代码后,执行 npm install 更新依赖。

  2. 任何时候都不要修改 package-lock.json。

  3. 如果 package-lock.json 出现冲突或问题,建议将本地的 package-lock.json 文件删除,引入远程的 package-lock.json 文件和 package.json,再执行 npm install 命令。