npm7.0源码分析(四)之npm help细节

581 阅读4分钟

前言

nodejs系列文章传送门

首先先抛个问题:如果想要查找某个npm命令甚至npm自身的文档,你会怎么做的?

答案可能会是:打开浏览器,输入npm官网,进入document页面

如果你真是这样做的,那可以看看接下来的内容,帮你打开新的查看文档的技能大门。

如果你说“用什么浏览器,跑一下npm help xxx吖~”,恭喜,你已经打开了npm的大门,但是细节决定成败,不妨让我们一起来好好复习下npm help是如何实现的,看源码来巩固下它的具体使用方式,可能会有些细节收获

我们都知道帮助文档功能强大,对于一个好的命令行工具,帮助文档更是不能缺少,在日常开发中,我们必不可少的要查询很多文档,那么如何快速找到这些文档来帮助我们解决问题就非常重要。今天就来聊一聊容易被忽视的npm help,同时这个命令对于我们了解npm工作方式也帮助良多。

文档类型

我们先看看npm源码目录下都有哪些类型的文档,如下图所示,

docs目录下的是html形式的文档,而man目录下就是man命令形式的文档。同时两种类型的文档又被细分为三个子类:

  • commands(所有命令相关文档)
  • configuring-npm(npm运行时配置相关文档)
  • using-npm(关于npm使用相关文档)

分别对应man类型下157。以上这些子类型下一般是不会出现同名文档的,即使出现了,npm help里也实现了一个优先选择顺序策略,下面会说到,先带着这个问题。

功能范围

npm help能找到关于npm的所有文档,

  • npm help install,查看npm install如何使用
  • npm help package-json或者npm help package.json,查看package.json的使用方式,了解它里面各个字段的含义等等信息
  • ...等等

除上面的例子外还有很多,总之只要官网能找到的文档,npm help都能帮你找到。

实现原理

下面我们先以一个简单的例子开始,分析npm help的常用用法,就拿npm help init来说,我们来查找一下npm init的帮助文档。

同样,vscode debug走起,如果你对npm启动不熟悉的话,可以进入传送门了解,这样对理解其他内容会事半功倍。

我们先瞄一眼npm help的整体源码,然后我们来逐行重点分析它的逻辑。

function help (args, cb) {
  var argv = npm.config.parsedArgv.cooked

  var argnum = 0
  if (args.length === 2 && ~~args[0])
    argnum = ~~args.shift()

  ...
  const affordances = {
    'find-dupes': 'dedupe',
  }
  var section = affordances[args[0]] || npm.deref(args[0]) || args[0]
    ...

  var pref = [1, 5, 7]
  if (argnum) {
    pref = [argnum].concat(pref.filter(function (n) {
      return n !== argnum
    }))
  }

  // npm help <section>: Try to find the path
  var manroot = path.resolve(__dirname, '..', 'man')

  // legacy
  if (section === 'global')
    section = 'folders'
  else if (section.match(/.*json/))
    section = section.replace('.json', '-json')

}

文件查找顺序

刚刚说到的优先查找顺序,假如像我们上面例子npm help init,如上图所示,args长度为1且args[0]='init',此时argnum=0pref就是初始的[1, 5, 7],选择顺序就是1-5-7,(这里的157就是上面的子类型代号)。

但是我们如果这样跑命令npm help 5 init,情况就不一样了,如上图所示,此时刚进来args长度为2。

此时:

var argnum = 0
  if (args.length === 2 && ~~args[0])
    argnum = ~~args.shift()

从下图可以看到,argnum=~~args.shift()就等于5了。

而此时pref=[5, 1, 7],此时顺序就是5-1-7。大家先了解一下这个顺序策略,下面的pickMan方法里会根据这个顺序来查找对应的文档文件。

var pref = [1, 5, 7]
  if (argnum) {
    pref = [argnum].concat(pref.filter(function (n) {
      return n !== argnum
    }))
  }
...
viewMan(pickMan(mans, pref), cb)

glob文件查找

接着就是glob文件查找环节:

var compextglob = '.+(gz|bz2|lzma|[FYzZ]|xz)'
  var compextre = '\\.(gz|bz2|lzma|[FYzZ]|xz)$'
  var f = '+(npm-' + section + '|' + section + ').[0-9]?(' + compextglob + ')'
  return glob(manroot + '/*/' + f, function (er, mans) {
    if (er)
      return cb(er)
        ...

    viewMan(pickMan(mans, pref), cb)
  })

再看一下这张图

glob(manroot + '/*/' + f,此时的f="+(npm-init|init).[0-9]?(.+(gz|bz2|lzma|[FYzZ]|xz))"

这里glob查找出来的mans是有可能有多个值的,因为在npm/man/多个子目录下如果都存在对应文档的话就可以查找到多个文档文件。

这里关于npm init,最终查找到的是npm/man/man1目录下的npm-init.1文件。因为只有npm/man/man1这里有。

最后就是根据这个文件进行最终的展示逻辑。

viewMan(pickMan(mans, pref), cb)

先看一下pickMan,大家还记得上面说的优先顺序选择逻辑吧。

假如存在多个man文档文件时,在pickMan里面,就是根据这个优先顺序来排序,最后返回mans[0]

function pickMan (mans, pref_) {
  var nre = /([0-9]+)$/
  var pref = {}
  pref_.forEach(function (sect, i) {
    pref[sect] = i
  })
  mans = mans.sort(function (a, b) {
    var an = a.match(nre)[1]
    var bn = b.match(nre)[1]
    return an === bn ? (a > b ? -1 : 1)
      : pref[an] < pref[bn] ? -1
      : 1
  })
  return mans[0]
}

文档展示

最最核心的来了,viewMan逻辑,也就是文档展示,

function viewMan (man, cb) {
  var nre = /([0-9]+)$/
  var num = man.match(nre)[1]
  var section = path.basename(man, '.' + num)
    ...
  var viewer = npm.config.get('viewer')

  var conf
  switch (viewer) {
    case 'woman':
      var a = ['-e', '(woman-find-file \'' + man + '\')']
      conf = { env: env, stdio: 'inherit' }
      var woman = spawn('emacsclient', a, conf)
      woman.on('close', cb)
      break

    case 'browser':
      try {
        var url = htmlMan(man)
      } catch (err) {
        return cb(err)
      }
      openUrl(url, 'help available at the following URL', cb)
      break

    default:
      conf = { env: env, stdio: 'inherit' }
      var manProcess = spawn('man', [num, section], conf)
      manProcess.on('close', cb)
      break
  }
}

从上面的源码中我们能看出来,viewMan方法的核心逻辑是根据viewer = npm.config.get('viewer'),也就是--viewer这个命令行option标志来决定最后的展示方式。主要有三种方式:

  • woman,通过--viewer woman设置
  • browser,通过--viewer browser设置
  • man,默认情况下就是man,像我们上面的npm help init,默认viewer=man,最终执行的就是man 1 npm-init

假如npm help init --viewer browser,此时viewer = npm.config.get('viewer')就是browser

function htmlMan (man) {
  var sect = +man.match(/([0-9]+)$/)[1]
  var f = path.basename(man).replace(/[.]([0-9]+)$/, '')
  switch (sect) {
    case 1:
      sect = 'commands'
      break
    case 5:
      sect = 'configuring-npm'
      break
    case 7:
      sect = 'using-npm'
      break
    default:
      throw new Error('invalid man section: ' + sect)
  }
  return 'file://' + path.resolve(__dirname, '..', 'docs', 'output', sect, f + '.html')
}

通过htmlMan拿到对应html形式文档文件,就在npm/docs/output/commands/下的npm-init.html

因为man1对应output/commands

最后通过浏览器打开这个file://xxx文件。至此,npm help的所有执行逻辑就完成了。

总结

快速查找帮助文档是我们日常开发非常重要的技能,可以帮助我们快速找出解题思路线索,而通过终端一个简单的shell命令就一步到位的找到文档更是我们作为程序员的必备技能。希望大家以后不要再通过浏览器了,就用npm help来查找有关npm的所有文档吧,可能隔壁同学还没打开浏览器,你已经知道问题的解决思路了。