上一篇文章讲解了如何找到源码以及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阶段
这一步会执行两个动作:
- 判断是否是全局安装,(this.global 为true 表示是全局安装有 -g 参数),如果是则调用readGlobalPackageData读取全局目录下的node_modules文件树,如果不是则调用readLocalPackageData读取当前项目目录下的node_modules文件树 。
- 调用normalizeCurrentTree将上一步得到的tree序标准化。
loadIdealTree阶段
这一步会执行三个动作,
-
调用cloneCurrentTree函数 把currentTree复制给idealTree
-
调用loadShrinkwrap函数 ,该函数会依次读取npm-shrinkwrap.json、package-lock.json、package.json ,确定完整的依赖树,注意这时候的依赖树只是逻辑上的依赖树
-
调用loadAllDepsIntoIdealTree函数 遍历上一步得到的idealTree,添加任何缺少的依赖项,在不破坏原来的module的情况下,依赖项会尽可能的放到顶层。
debugTree阶段
这一步会对上面生成的currentTree和idealTree进行一些处理
返回带有unicode管道字符的obj字符串表示形式 并打印在控制台,效果类似于npm ls
实际调用了 archy 方法 ,详细信息点这里
generateActionsToTake阶段
这阶段会有五个动作
-
调用validateTree方法 对idealTree进行校验,校验的内容有是否需要peer dependencies,node版本校验,要安装的包是否在dev和prod环境中都存在。
-
调用diffTrees方法找到由currentTree转为idelTree的不同动作并放到 differences队列中
-
调用 computeLinked 方法计算局部包如何链接到合适的全局包
-
调用checkPermissions方法检查differnces里的anction操作是否有权限,比如是否有写文件的权限等。
-
调用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 阶段
在这个阶段会有三个步骤
-
检测安装是否失败,如果失败就进行回滚操作
-
执行所有的生命周期函数(如果定义了的话)
-
在控制台打印警告信息和安装了那些模块信息