一、前言
npm作为前端开发人员必备工具之一,有着无法撼动的地位。但很多人只停留在使用的层面上,对于它内部工作原理很少去深究。笔者出于学习的目的,尝试从源码的角度去解析它的工作原理。本篇作为npm源码解析第一篇,会先分析它的启动流程,也即是从输入命令行到给出执行结果这一环节。
二、vscode源码调试
在开始解析之前,我们需要准备调试环境,这里推荐使用开源IDE Visual Studio Code 进行代码调试。
1、创建项目
mkdir npm-test
npm init -y
npm install npm@9.7.2 --no-save
-y
跳过询问,快速生成package.json文件。npm@9.7.2
将npm安装到项目中,用于源码调试,因为npm是基于node.js开发的开源项目,代码并未经过压缩,可以直接阅读调试,因此直接下载。至于下载哪个版本,尽量选择9.x版本,本文是基于npm 9.7.2进行调试。--no-save
不将包写入package.json依赖项中。
2、启动调试
切换到运行和调试
面板,创建launch.json
文件,选择Node.js项目。
此时会生成.vscode
目录和launch.json
文件。接下来修改启动文件配置,修改program
值为:${workspaceFolder}/node_modules/.bin/npm
,指定调试入口,即npm可执行文件。args
值为:["view", "npm", "versions"]
,携带的参数。总体配置的意思是:启动调试模式时,用node程序执行npm文件并添加view npm versions
参数。
最后,点击启动程序按钮或者F5开始调试,在调试控制台输出信息即代表配置成功。接下来我们就可以在npm代码中打断点愉快的进行调试了。
三、启动流程
npm不管是全局还是项目内部安装,都会通过npm可执行文件来运行命令。这里npm可执文件指的是node_modules/.bin
目录中存放的文件。安装npm包时,会根据package.json文件中的bin字段将可执行文件安装至.bin
中,所以,.bin/npm
是npm/bin/npm-cli.js
的映射文件。而npm-cli.js
只是简单的导入lib/cli.js
文件
所以,我们先查看lib/cli.js文件:
// lib/cli.js
const validateEngines = require('./es6/validate-engines.js')
const cliEntry = require('path').resolve(__dirname, 'cli-entry.js')
module.exports = (process) => validateEngines(process, () => require(cliEntry))
validateEngines
:验证node版本是否匹配npm版本,并执行cliEntry函数。cliEntry
:命令行工具执行入口lib/cli-entry.js
文件,它导出一个函数。
// lib/cli-entry.js
module.exports = async (process, validateEngines) => {
// ...
// 创建npm实例
const Npm = require('./npm.js')
const npm = new Npm()
// 定义变量存放命令行参数。
let cmd
try {
// 启动
await npm.load()
// ...
// 此处省略对命令行参数的一些转化处理,例如-v、-versions等。
// 执行命令并将参数传入
await npm.exec(cmd)
} catch (err) {
// ...
}
}
cliEntry
函数的重点就是创建npm实例,定义cmd变量处理命令行参数,通过npm.load
方法处理一些控制台日志输出相关的逻辑,然后通过npm.exec()
执行命令。所以核心代码就是npm类的实现。
// lib/npm.js
class Npm {
// ...
async exec (cmd, args = this.argv) {
// 调用setCmd方法
const command = this.setCmd(cmd)
return command.cmdExec(args).finally(timeEnd)
}
setCmd (cmd) {
// 调用Npm静态方法cmd,通过命令行参数加载模块,并实例化。
const Command = Npm.cmd(cmd)
const command = new Command(this)
return command
}
static cmd (c) {
// 命令行参数转化函数
// 我们知道npm提供很多参数别名,而且容错率很高,这些都是通过内部转换函数处理的。
// 例如:distTag转为dist-tag,i转为install、r转为uninstall等。
const command = deref(c)
// 通过参数名加载模块,如view返回 require(`./commands/view.js`)
return require(`./commands/${command}.js`)
}
}
便于理解的伪代码:
// 当输入npm view 命令行:
const Command = require(`./commands/view.js`)
const command = new Command(this)
command.cmdExec(args)
通过代码可以看出,当执行命令时,程序会解析输入的参数,然后巧妙的通过参数名来匹配对应的文件。接着导入该模块,而模块对应的是一个类,实现对应功能的类。最后通过调用实例方法cmdExec,作进一步处理。从这里,我们能够看到npm工程化的思路,将复杂的命令,分流至不同类中进行处理,这是分而治之的思想,值得我们学习借鉴。
四、view源码解析
通过调用堆栈可以看到cmdExec
方法最终调用的是View class
中的exec
方法。而npm view npm versions
命令的功能是在控制台输出npm版本信息。
// bin/commands/view.js
class View extends BaseCommand {
// ...
async exec (args) {
// ...
// getData方法用于获取包的指定信息,内部实现是通过npm-package-arg和pacote模块,具体阅读源码。
// 这里pkg的值是npm,args是versions,也就是获取npm的所有版本信息。
const [pckmnt, data] = await this.getData(pkg, args)
if (!this.npm.config.get('json') && wholePackument) {
// 进到这里,说明args参数为空,则打印当前项目信息。
data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]]['']))
} else {
// JSON格式的输出
let reducedData = data.reduce(reducer, {})
// 转为字符串
const msg = await this.jsonData(reducedData, pckmnt._id)
if (msg !== '') {
// 控制台打印信息
this.npm.output(msg)
}
}
}
view命令的实现相对简单,可以自行阅读源码,这里就不作过多分析了。
五、总结
- 创建项目,通过
npm install
安装npm包进行源码调试,这种方式简单直接,但你也可以去npm官方git仓库下载代码来调试。 - 合理利用vscode代码调试工具可以做到事半功倍,但要懂得在阅读源码的同时,使用断点、上下文变量和调用堆栈进行辅助调试。
- npm命令行启动流程的核心在于创建
Npm class
的实例,通过npm.exec
方法执行lib/commands
目录中对应的文件。所以npm view
命令可以直接对应lib/commands/view.js
文件内View class
的实现。 View class
功能是在控制台打印指定包的信息,可以是版本信息、名称、甚至package.json内的任何信息。