前言
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类型下1、5、7。以上这些子类型下一般是不会出现同名文档的,即使出现了,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=0而pref就是初始的[1, 5, 7],选择顺序就是1-5-7,(这里的1、5、7就是上面的子类型代号)。
但是我们如果这样跑命令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的所有文档吧,可能隔壁同学还没打开浏览器,你已经知道问题的解决思路了。