包管理器原理浅析

76 阅读3分钟

包管理器原理

常见的包管理器npm,yarn,pnpm功能完善,平时只是使用,从来不会去关注实现原理。一款成熟的包管理器经过很多年发展,非一时一刻就能完成,所以这里只想实现最基础的功能了解其基本的工作原理。分析一下,最基础的包管理器需要具备哪些功能。

  • 命令行参数解析。例如npm i -D packageName,我们需要解析出包名,参数D表示开发依赖。
  • 解析package.json依赖版本
  • 读写lock文件,有lock文件时解析依赖直接读取即可,否则需要联网解析
  • 下载安装依赖,主要是下载压缩文件,解压,放到指定目录。
  • 输出提示,下载过程显示进度。

命令行参数解析

命令行参数解析的方式很多。最原始的就是使用process.argv得到命令行参数,自行解析。如图

2.png 但是这种方式对复杂参数解析太麻烦,使用第三方库比较方便。常用的库有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对应布尔或者位置参数。

解析完参数后将参数传递给主函数使用即可。

主流程的一些重点

整体流程:

  1. 从当前目前向上找到package.json的文件路径,读取之(这是因为你可能不是在项目根目录运行命令,所以要向上查,可以使用find-up库)

  2. 如果指定了包名,我们需要将开发或者生产依赖对应的版本置为空,因为指定包名,需要解析出最新版本。(细节1)

  3. 读取lock文件,供解析版本使用,初始时为空。

  4. 根据参数dev或prod,拿对应依赖对象,每个条目(key-value)进行解析,最终结果是两个对象toplevel(包含解析出的依赖信息),unsatisfied包含无法解析出版本的依赖信息。

  5. 每个条目解析思路是:从lock文件内获取版本信息,如果没有得到就联网获取版本信息。根据版本限制,从版本列表中找出合适的版本信息。本依赖就解析完了,如果没存到toplevel中,存之,否则也就是已经有这个依赖了,需要判断是否与已有冲突,如果冲突直接加到unsatisfied里。将解析结果加到lock文件供最后写文件,递归解析依赖的依赖。(这里依赖冲突检测还有一个case较为麻烦:就是需要检测与依赖链里其他分支的依赖是否冲突)

  6. 全部解析完版本后,写lock文件

  7. 下载每个条目,使用fetch下载包的压缩文件,然后解压到目录里即可。

  8. 在解析和安装过程中,使用log-update输出当前状态。使用progress展示安装到进度,progress接受total参数,每完成一项输出一个#,在下载包时每下载完一个就调用下progress的tick方法前进一步即可。