【npm源码】npm install命令安装过程解析

5,102 阅读5分钟

上一篇文章讲解了如何找到源码以及npm config命令的源码解读,今天来看一下npm install命令。如果不知道怎么找源码可以参考上一篇文章【npm源码】config get命令大扒皮,一件衣服都不留

首先在源码中找到Installer构造函数:

/lib/install.js
function Installer (where, dryrun, args, opts) {
  validate('SBA|SBAO', arguments)
  if (!opts) opts = {}
  this.where = where
  this.dryrun = dryrun
  this.args = args
  // fakechildren are children created from the lockfile and lack relationship data
  // the only exist when the tree does not match the lockfile
  // this is fine when doing full tree installs/updates but not ok when modifying only
  // a few deps via `npm install` or `npm uninstall`.
  this.currentTree = null
  this.idealTree = null
  this.differences = []
  this.todo = []
  this.progress = {}
  this.noPackageJsonOk = !!args.length
  this.topLevelLifecycles = !args.length

  this.autoPrune = npm.config.get('package-lock')

  const dev = npm.config.get('dev')
  const only = npm.config.get('only')
  const onlyProd = /^prod(uction)?$/.test(only)
  const onlyDev = /^dev(elopment)?$/.test(only)
  const prod = npm.config.get('production')
  this.dev = opts.dev != null ? opts.dev : dev || (!onlyProd && !prod) || onlyDev
  this.prod = opts.prod != null ? opts.prod : !onlyDev

  this.packageLockOnly = opts.packageLockOnly != null
    ? opts.packageLockOnly : npm.config.get('package-lock-only')
  this.rollback = opts.rollback != null ? opts.rollback : npm.config.get('rollback')
  this.link = opts.link != null ? opts.link : npm.config.get('link')
  this.saveOnlyLock = opts.saveOnlyLock
  this.global = opts.global != null ? opts.global : this.where === path.resolve(npm.globalDir, '..')
  this.audit = npm.config.get('audit') && !this.global
  this.started = Date.now()
}

参数

  • where 字符串 表示当前运行install命令的目录 D:\workspace\mine\npm完全使用指南\test
  • dryrun 布尔值,如果为true表明你不需要让npm做出任何更新并且只报告完成了什么
  • args 数组,表示晕运行install命令传进来的参数 ['react']
  • opts 表示安装时的一些其参数,如当前在开发环境哈还是测试环境,是否在全局环境等。

重要的属性

  • where : 在那个目录下
  • dryrun : 是否只打印信息
  • args :参数
  • opts: 选项
  • currentTree: 当前硬盘上node_moudules 构成的 tree
  • idealTree: 安装相应的模块后 理想的 tree
  • differences: 两棵树的差异队列
  • todo: 一系列要做的事情的队列
  • progress: 进程管理
  • noPackageJsonOk : 没有packageJson文件是否ok,这里要多说一句,如果install没有安装对象 也就是arg.length = 0 此时!!0 为false,就是说没有packageJson是不行的,因为这时候运行的是npm install ,要去拉取package.json里的依赖去安装,所以没有package.json是不行的。
  • topLevelLifecycles 顶层元素的生命周期
  • autoPrune 布尔值,读取配置文件 package-lock 判断是否自动生成package-lock.json文件
  • only 当是dev或者development时,不带任何参数运行局部npm install,只会有devDependencies(和他们的依赖)会被安装,当是prod或者production是,无参运行npm install,只有non-devDependencies(和他们的依赖)会被安装
  • packageLockOnly 如果为true,则不进行安装动作,只是将安装的内容写进package.json的dependencies里面

再继续找,找到安装过程:

/lib/install.js
Installer.prototype.run = function (_cb) {
  //进行一些校验
  ...
  //将安装的步骤放入队列里面
  var installSteps = []
  var postInstallSteps = []
  if (!this.dryrun) {
    installSteps.push(
      [this.newTracker(log, 'runTopLevelLifecycles', 2)],
      [this, this.runPreinstallTopLevelLifecycles])
  }
  installSteps.push(
    [this.newTracker(log, 'loadCurrentTree', 4)],
    [this, this.loadCurrentTree],
    [this, this.finishTracker, 'loadCurrentTree'],

    [this.newTracker(log, 'loadIdealTree', 12)],
    [this, this.loadIdealTree],
    [this, this.finishTracker, 'loadIdealTree'],

    [this, this.debugTree, 'currentTree', 'currentTree'],
    [this, this.debugTree, 'idealTree', 'idealTree'],

    [this.newTracker(log, 'generateActionsToTake')],
    [this, this.generateActionsToTake],
    [this, this.finishTracker, 'generateActionsToTake'],

    [this, this.debugActions, 'diffTrees', 'differences'],
    [this, this.debugActions, 'decomposeActions', 'todo'],
    [this, this.startAudit]
  )

  if (this.packageLockOnly) {
    postInstallSteps.push(
      [this, this.saveToDependencies])
  } else if (!this.dryrun) {
    installSteps.push(
      [this.newTracker(log, 'executeActions', 8)],
      [this, this.executeActions],
      [this, this.finishTracker, 'executeActions'])
    var node_modules = path.resolve(this.where, 'node_modules')
    var staging = path.resolve(node_modules, '.staging')
    postInstallSteps.push(
      [this.newTracker(log, 'rollbackFailedOptional', 1)],
      [this, this.rollbackFailedOptional, staging, this.todo],
      [this, this.finishTracker, 'rollbackFailedOptional'],
      [this, this.commit, staging, this.todo],

      [this, this.runPostinstallTopLevelLifecycles],
      [this, this.finishTracker, 'runTopLevelLifecycles']
    )
    if (getSaveType()) {
      postInstallSteps.push(
        [this, (next) => { computeMetadata(this.idealTree); next() }],
        [this, this.pruneIdealTree],
        [this, this.debugLogicalTree, 'saveTree', 'idealTree'],
        [this, this.saveToDependencies])
    }
  }
  postInstallSteps.push(
    [this, this.printWarnings],
    [this, this.printInstalled])

  var self = this
  //到这里才真正开始执行
  chain(installSteps, function (installEr) {
    if (installEr) self.failing = true
    chain(postInstallSteps, function (postInstallEr) {
      if (installEr && postInstallEr) {
        var msg = errorMessage(postInstallEr)
        msg.summary.forEach(function (logline) {
          log.warn.apply(log, logline)
        })
        msg.detail.forEach(function (logline) {
          log.verbose.apply(log, logline)
        })
      }
      cb(installEr || postInstallEr, self.getInstalledModules(), self.idealTree)
    })
  })
  return result
}

这里就是安装模块的整个流程了,现在来分析一下。

首先定义两个队列 :

installStep: 安装步骤

postInstallSteps: 安装完成之后的步骤

判断是否是干运行

如果不是干运行,则运行 preinstall 钩子(如果工程定义了的话)

如果是干运行,则进入下一步

loadCurrentTree阶段

这一步会执行两个动作:

  1. 判断是否是全局安装,(this.global 为true 表示是全局安装有 -g 参数),如果是则调用readGlobalPackageData读取全局目录下的node_modules文件树,如果不是则调用readLocalPackageData读取当前项目目录下的node_modules文件树 。
  2. 调用normalizeCurrentTree将上一步得到的tree序标准化。

loadIdealTree阶段

这一步会执行三个动作,

  1. 调用cloneCurrentTree函数 把currentTree复制给idealTree

  2. 调用loadShrinkwrap函数 ,该函数会依次读取npm-shrinkwrap.json、package-lock.json、package.json ,确定完整的依赖树,注意这时候的依赖树只是逻辑上的依赖树

  3. 调用loadAllDepsIntoIdealTree函数 遍历上一步得到的idealTree,添加任何缺少的依赖项,在不破坏原来的module的情况下,依赖项会尽可能的放到顶层。

debugTree阶段

这一步会对上面生成的currentTree和idealTree进行一些处理

返回带有unicode管道字符的obj字符串表示形式 并打印在控制台,效果类似于npm ls

实际调用了 archy 方法 ,详细信息点这里

generateActionsToTake阶段

这阶段会有五个动作

  1. 调用validateTree方法 对idealTree进行校验,校验的内容有是否需要peer dependencies,node版本校验,要安装的包是否在dev和prod环境中都存在。

  2. 调用diffTrees方法找到由currentTree转为idelTree的不同动作并放到 differences队列中

  3. 调用 computeLinked 方法计算局部包如何链接到合适的全局包

  4. 调用checkPermissions方法检查differnces里的anction操作是否有权限,比如是否有写文件的权限等。

  5. 调用decomposeActions方法,把diffrence队列里的action按照add、update、move、remove进行分解并放到todo队列中

debugActions阶段

这个阶段只做一件事就是打印每个action的信息

packageLockOnly阶段

如果packageLockOnly为true

.npmrc中对应的配置项为 package-lock-only

packageLockOnly的意思就是只将要安装的模块写到package.json的dependences里面,而不执行任何其他动作

dryrun阶段

如果packageLockOnly为false 并且dryrun(干运行)为true

.npmrc中对应的配置项为 dry-run

这个时候不执行任何动作只是打印信息

executeActions阶段

如果packageLockOnly为false,并且dryrun(干运行)为false,这个时候开始了真正安装阶段。

这个阶段调用executeActions方法,去执行todo里面的内容

/lib/install.js
steps.push(
    [doSerialActions, 'global-install', staging, todo, trackLifecycle.newGroup('global-install')],
    [lock, node_modules, '.staging'],
    [rimraf, staging],
    [doParallelActions, 'extract', staging, todo, cg.newGroup('extract', 100)],
    [doReverseSerialActions, 'unbuild', staging, todo, cg.newGroup('unbuild')],
    [doSerialActions, 'remove', staging, todo, cg.newGroup('remove')],
    [doSerialActions, 'move', staging, todo, cg.newGroup('move')],
    [doSerialActions, 'finalize', staging, todo, cg.newGroup('finalize')],
    [doParallelActions, 'refresh-package-json', staging, todo, cg.newGroup('refresh-package-json')],
    [doParallelActions, 'preinstall', staging, todo, trackLifecycle.newGroup('preinstall')],
    [doSerialActions, 'build', staging, todo, trackLifecycle.newGroup('build')],
    [doSerialActions, 'global-link', staging, todo, trackLifecycle.newGroup('global-link')],
    [doParallelActions, 'update-linked', staging, todo, trackLifecycle.newGroup('update-linked')],
    [doSerialActions, 'install', staging, todo, trackLifecycle.newGroup('install')],
    [doSerialActions, 'postinstall', staging, todo, trackLifecycle.newGroup('postinstall')])

postInstall 阶段

在这个阶段会有三个步骤

  1. 检测安装是否失败,如果失败就进行回滚操作

  2. 执行所有的生命周期函数(如果定义了的话)

  3. 在控制台打印警告信息和安装了那些模块信息

ヽ✿゜▽゜)ノ