最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • npm源码分析(-)之npm启动

    正文概述 掘金(Kelvin)   2021-02-15   1096

    前言

    nodejs系列文章传送门

    • nodejs可以说的秘密之node index.js到底做了什么

    之前分析了nodejs的模块机制,如果大家对nodejs模块机制还不了解,可以再去对应的文章看一下。之前也说了,要写一系列关于nodejs的文章,那npm作为nodejs的包管理工具,就必须要深入学习一下,好好了解它的实现原理,正所谓工欲善其事必先利其器,今天我们就好好分析一下npm的启动逻辑,为后续其他npm命令打下基础。我们知道npm用于在nodejs技术栈对CommonJS模块进行增删改查,然而npm其本身同样也是一个CommonJS模块,也可以通过npm命令对其进行增删改查,它同样遵守模块的规范,以npm@7.0为例(下面所有的分析都是基于7.0版本,该版本较之前的版本从整体执行逻辑上做了较大的重构,代码逻辑更清晰,更易维护和扩展),我们直接来看下它的package.json,会发现在bin字段里,npm作为可执行命令,其逻辑入口是基于bin/npm-cli.js文件,尽然找到入口,话不多说,我们直接从这个入口触发。

    核心启动原理

    为了更好的分析整个npm启动逻辑,直接vscode debug走起。我为了不在全局npm包下做调试,因为可能需要改动一下npm包里的代码来更好的调试,所以就本地安装了npm包,直接利用全局安装的npm包也是可以的。直接index.js里require('npm/bin/npm-cli'),这里以npm i nopt --no-package-lock为例子贯串全文。创建launch.json,打上断点,F5调试开始走起。

    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "node",
          "runtimeExecutable": "/usr/local/bin/node",
          "request": "launch",
          "name": "Launch Program",
          "skipFiles": [
            "<node_internals>/**"
          ],
          "program": "index.js",
          "args": ["i", "nopt", "--no-package-lock"]
        }
      ]
    }
    

    npm逻辑入口

    既然npm命令行的逻辑入口是bin/npm-cli.js,那我们直接锁定该文件,发现它的源码非常简单,就是在内部引用了lib/cli.js

    #!/usr/bin/env node
    require('../lib/cli.js')(process)
    

    那我们马不停蹄,直接来看下cli.js做了什么,先看一下核心代码逻辑:

    checkForBrokenNode()
    ...
    checkForUnsupportedNode()
    ...
    const npm = require('../lib/npm.js')
    ...
    npm.load(async er => {
    
      ...
      const cmd = npm.argv.shift()
      const impl = npm.commands[cmd]
      if (impl)
        impl(npm.argv, errorHandler)
      else {
        npm.config.set('usage', false)
        npm.argv.unshift(cmd)
        npm.commands.help(npm.argv, errorHandler)
      }
    })
    

    总结起来主要做了以下三件重要的事情:

    1. 检查nodejs版本,做一些兼容性提示
    2. 加载核心模块lib/npm.js获取npm实例
    3. 进行npm.load,load之后根据process.argv中解析得到具体cmd执行对应逻辑

    那么关键来了,既然为了获取npm实例而引入lib/npm.js,而且大部分核心处理逻辑都在npm实例上,我们必须分析一下npm实例化的整个过程。

    npm实例化

    从源码中我们会发现,npm实例是继承于EventEmitter的。直接看它的构造器,我省略了一些不重要的代码,

    const npm = module.exports = new class extends EventEmitter {
      constructor () {
        super()
        ...
        this.command = null
        this.commands = proxyCmds(this) // 重点1:代理所有的cmd
            ...
        this.version = require('../package.json').version
        this.config = new Config({
          npmPath: dirname(__dirname),
          types,
          defaults,
          shorthands,
        }) // 重点2:获取执行过程所需的配置信息config
        this[_title] = process.title
        this.updateNotification = null
      }
    

    不难看出,里面主要做了两件重要的事:

    1. 通过proxyCmds加载并代理所有定义过的可执行cmd
    2. 获取所需的配置信息config,查看详细npmrc配置,这些配置信息都维护在Config实例的config.data这个Map里

    config.data里的这些信息会以['default','builtin','global','user','project','env','cli']逐级融合,后一项以前一项为原型,后面会详细说明,这里先有个概念:

    • default:包含所有

    • 包含所有默认命令行options配置

    • builtin:内建的npmrc下的运行时配置

    • global:全局npmrc下的运行时配置

    • user:用户配置的npmrc下的运行时配置

    • project:当前项目npmrc下的运行时配置

    • env:npm环境变量npm_config_开头的所有字段信息

    • cli:当前正在执行的cli命令的所有options(如--save等等),会依据所有命令行options的types类型配置及shorthands简写配置(如--save的简写-S之类)由nopt解析得到,得到的结果是一个以命令行option为key的对象,如{save:true,'save-dev':false}

    逐个来看其实现逻辑,先来看proxyCmds

    const proxyCmds = (npm) => {
      const cmds = {}
      return new Proxy(cmds, {
        get: (prop, cmd) => {
          if (hasOwnProperty(cmds, cmd))
            return cmds[cmd]
    
          const actual = deref(cmd)
          if (!actual) {
            cmds[cmd] = undefined
            return cmds[cmd]
          }
          if (cmds[actual]) {
            cmds[cmd] = cmds[actual]
            return cmds[cmd]
          }
          cmds[actual] = makeCmd(actual)
          cmds[cmd] = cmds[actual] // 同时将真实名字对应的命令实现赋值给别名
          return cmds[cmd]
        },
      })
    }
    
    const makeCmd = cmd => {
      const impl = require(`./${cmd}.js`)
      const fn = (args, cb) => npm[_runCmd](cmd, impl, args, cb)
      Object.assign(fn, impl)
      return fn
    }
    

    proxyCmds返回一个Proxy实例,

    • 将所有在npm中有定义的cmd(如install)都维护到了cmds这个对象中,并以cmds为target生成一个Proxy实例
    • 对cmd名称做derefderef最主要的作用是先将cmd(比如npm i对应了install)从camelCase转化成kebab-case,再从别名-真实名称的映射中寻找最终的真实名称(i对应的真实名称是install),根据这个真实名称来引用对应的模块文件(这里就是lib/install.js)
    • makeCmd中,根据kebab-case形式的真实名称引入对应cmd名称的模块文件,同时将这些引入的命令利用**npm[_runCmd]**实例方法统一封装。
    • 创建完对应cmd命令实现之后,在cmds中将真实名字对应的命令实现赋值给别名,这也是npm命令可以用很多别名的原因,从下图中我们就能直观的看到install这个命令的别名i同样存在于cmds中。

    npm源码分析(-)之npm启动

    再来看一下配置信息是如何获取的:

    先看一下Config构造器,这里的types、shorthands、defaults配置可以参考源码,是所有命令行options的配置。

    constructor ({
        types,
        shorthands,
        defaults,
        npmPath,
        ...
      }) {
            ...
        this.data = new Map()
        let parent = null
        for (const where of wheres) { // 这里的wheres就是['default','builtin','global','user','project','env','cli']
          this.data.set(where, parent = new ConfigData(parent)) 
        }
    
      }
    
    class ConfigData {
      constructor (parent) {
        this[_data] = Object.create(parent && parent.data)
        this[_source] = null
        this[_loadError] = null
        this[_raw] = null
        this[_valid] = true
      }
      
      get data () {
        return this[_data]
      }
    

    npm源码分析(-)之npm启动

    结合ConfigData构造器,我们可以看到按照['default','builtin','global','user','project','env','cli']这个顺序,后一个ConfigData实例的data是以上一个ConfigData实例的data为原型,从而得到最终融合Config.data,所以当某一hasOwnProperty为fasle的字段被修改时正好会覆盖parent上对应字段,所以通过如config.data.get(key, where = 'cli')就能获取到对应key的配置项,如果找不到,则会从原型上逐级向上找。而这些ConfigData的赋值过程在npm.load中伴随npm.config.load进行。上面的图片展示了config还未load时data的样子,所有的ConfigData.data都是空的。

    至此,npm实例化已经完成,接下来就是执行npm.loadnpm.load内部核心逻辑是在[_load]方法里。首先通过which找到process.argv[0](也就是/usr/local/bin/node)对应的可执行文件,这里拿到的是node命令,因为npm命令实际上是通过node来跑npm-cli.js。紧接着就是config.load

    async [_load] () {
      const node = await which(process.argv[0]).catch(er => null)
      if (node && node.toUpperCase() !== process.execPath.toUpperCase()) {
        log.verbose('node symlink', node)
        process.execPath = node
        this.config.execPath = node
      }
    
      await this.config.load()
      this.argv = this.config.parsedArgv.remain
      ...
    }
    

    在这里,会将通过nopt解析得到的所有parsedArgv.remain赋值到npm.argv上,这里的remain字段上包含了nopt解析遗留下来的命令行参数,此时remain就是:

    npm源码分析(-)之npm启动

    config.load

     async load () {
        if (this.loaded)
          throw new Error('attempting to load npm config multiple times')
    
        this.loadDefaults()
        await this.loadBuiltinConfig()
        this.loadCLI()
        this.loadEnv()
        await this.loadProjectConfig()
        await this.loadUserConfig()
        await this.loadGlobalConfig()
        
        ...
        this.validate()
       
        this[_loaded] = true
    
        this.globalPrefix = this.get('prefix')   
          ...
      }
    

    ['default','builtin','global','user','project','env','cli']里的所有都进行加载,加载完成的config.data如下图所示。这里的default就是源码中的默认命令行options配置项

    npm源码分析(-)之npm启动

    npm.load结束之后触发回调,这是在cli.js中执行npm.load时传递的。

    npm.load(async er => {
        if (er)
          return errorHandler(er)
        if (npm.config.get('version', 'cli')) {
          console.log(npm.version)
          return errorHandler.exit(0)
        }
    
        if (npm.config.get('versions', 'cli')) {
          npm.argv = ['version']
          npm.config.set('usage', false, 'cli')
        }
    
        npm.updateNotification = await updateNotifier(npm)
    
        const cmd = npm.argv.shift()
        const impl = npm.commands[cmd]
        if (impl)
          impl(npm.argv, errorHandler)
        else {
          npm.config.set('usage', false)
          npm.argv.unshift(cmd)
          npm.commands.help(npm.argv, errorHandler)
        }
      })
    

    回调中,通过npm.argv.shift(),我们就拿到了当前npm命令行的执行命令名称,此例中的**i**,剩下的npm.argv就是['nopt']。正如上面介绍的,npm.commands里代理了所有定义过的cmd执行逻辑。通过执行impl(npm.argv, errorHandler)就进入到了具体的cmd执行逻辑,这里也就是lib/install.js。前面在makeCmd时也提到过,真正的cmd执行入口其实都已经收敛到npm实例的npm[_runCmd](cmd, impl, args, cb)。来看一下[_runCmd]方法的源码,其核心逻辑其实非常简单:

     [_runCmd] (cmd, impl, args, cb) {
        ...
        
        if (this.config.get('usage')) {
          console.log(impl.usage)
          cb()
        } else {
          impl(args, er => {
            process.emit('timeEnd', `command:${cmd}`)
            cb(er)
          })
        }
      }
    

    如果当前为npm install --usage,此时拿到了usage字段为tue,则只是打印impl.usage,这里我顺便贴一下install.usage,大家发现没,其实install命令用途非常广,不知道的同学可以拓展一下,尤其它可以接收的模块目的地址可以有很多形式,除了模块名称,文件夹路径、git地址及tarball地址都可以,而且它的命令行options也比较丰富,可以满足我们不同的模块安装设置所需

    const usage = usageUtil(
      'install',
      'npm install (with no args, in package dir)' +
      '\nnpm install [<@scope>/]<pkg>' +
      '\nnpm install [<@scope>/]<pkg>@<tag>' +
      '\nnpm install [<@scope>/]<pkg>@<version>' +
      '\nnpm install [<@scope>/]<pkg>@<version range>' +
      '\nnpm install <alias>@npm:<name>' +
      '\nnpm install <folder>' +
      '\nnpm install <tarball file>' +
      '\nnpm install <tarball url>' +
      '\nnpm install <git:// url>' +
      '\nnpm install <github username>/<github project>',
      '[--save-prod|--save-dev|--save-optional|--save-peer] [--save-exact] [--no-save]'
    )
    

    如果不是--usage,则会进入到实际的install执行逻辑,也就是lib/install.js模块导出的执行方法。这里顺便提一句,因为正如上面proxyCmds里所说,当npm实例化时,会根据真实命令名称引入对应文件模块,所以npm源码里所有被定义的cmd文件模块导出形式都是如下形式:

    const cmd = (args, cb) => install(args).then(() => cb()).catch(cb)
    
    Object.assign(cmd, { completion, usage })
    

    总结

    至此,伴随npm实例化,在运行时,我们就可以拿到所有命令执行所需的所有配置项信息,接下来要做的就是根据这些丰富的配置项信息来处理特定的cmd执行逻辑。后续会逐一分析各个具体npm命令的执行逻辑,大家看到这里,我相信也可以很轻松的进入每一个npm命令里面看个究竟了,如果大家看完之后有什么想讨论的,非常欢迎大家留言评论,一起进步。


    起源地下载网 » npm源码分析(-)之npm启动

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元