包管理器原理
常见的包管理器npm,yarn,pnpm功能完善,平时只是使用,从来不会去关注实现原理。一款成熟的包管理器经过很多年发展,非一时一刻就能完成,所以这里只想实现最基础的功能了解其基本的工作原理。分析一下,最基础的包管理器需要具备哪些功能。
- 命令行参数解析。例如npm i -D packageName,我们需要解析出包名,参数D表示开发依赖。
- 解析package.json依赖版本
- 读写lock文件,有lock文件时解析依赖直接读取即可,否则需要联网解析
- 下载安装依赖,主要是下载压缩文件,解压,放到指定目录。
- 输出提示,下载过程显示进度。
命令行参数解析
命令行参数解析的方式很多。最原始的就是使用process.argv得到命令行参数,自行解析。如图
但是这种方式对复杂参数解析太麻烦,使用第三方库比较方便。常用的库有yargs,commander,使用方式参考官方文档比较方便。这里使用yargs,结合下面案例简要说明下基本使用。
const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
yargs(hideBin(process.argv))
.command('install [package]', 'start the server', (yargs) => {
yargs
.positional('package', {
describe: 'package to install',
default: ''
});
yargs.option('dev', {
alias:'d',
type: 'boolean',
})
return yargs
}, (argv) => {
console.log('------',argv, argv.dev)
})
.option('verbose', {
alias: 'v',
type: 'boolean',
description: 'Run with verbose logging'
})
.parse()
- hideBin基本等同于process.argv.slice(2),兼容了环境问题。
- 主要方法为command,接受四个参数:子命令格式,描述,子命令参数解析,子命令调用(入参为解析后的参数)
使用效果如图
通过install匹配上子命令后,包名是位置参数,布尔参数通过单杠-或者双杠--书写,双杠精确到一个,单杠可以跟多个布尔参数的缩写,解析后的参数是一个对象,_对应数组包含命令名字,全局参数;其他key对应布尔或者位置参数。
解析完参数后将参数传递给主函数使用即可。
主流程的一些重点
整体流程:
-
从当前目前向上找到package.json的文件路径,读取之(这是因为你可能不是在项目根目录运行命令,所以要向上查,可以使用find-up库)
-
如果指定了包名,我们需要将开发或者生产依赖对应的版本置为空,因为指定包名,需要解析出最新版本。(细节1)
-
读取lock文件,供解析版本使用,初始时为空。
-
根据参数dev或prod,拿对应依赖对象,每个条目(key-value)进行解析,最终结果是两个对象toplevel(包含解析出的依赖信息),unsatisfied包含无法解析出版本的依赖信息。
-
每个条目解析思路是:从lock文件内获取版本信息,如果没有得到就联网获取版本信息。根据版本限制,从版本列表中找出合适的版本信息。本依赖就解析完了,如果没存到toplevel中,存之,否则也就是已经有这个依赖了,需要判断是否与已有冲突,如果冲突直接加到unsatisfied里。将解析结果加到lock文件供最后写文件,递归解析依赖的依赖。(这里依赖冲突检测还有一个case较为麻烦:就是需要检测与依赖链里其他分支的依赖是否冲突)
-
全部解析完版本后,写lock文件
-
下载每个条目,使用fetch下载包的压缩文件,然后解压到目录里即可。
-
在解析和安装过程中,使用log-update输出当前状态。使用progress展示安装到进度,progress接受total参数,每完成一项输出一个#,在下载包时每下载完一个就调用下progress的tick方法前进一步即可。