【npm源码】config get命令大扒皮,一件衣服都不留

4,216 阅读7分钟

npm config get命令想必大家都很熟悉,通过它,我们可以得到npm的配置项,大家都知道它的用法,但是它的源码是怎么实现的呢?知其然更要其所以然,今天我就带大家扒一扒config get的源码。

.npmrc文件

要想了解config get命令肯定绕不开.npmrc文件。.npmrc是npm的配置文件,具体可以配置什么不是我们本篇文章的重点,如果大家有兴趣可以读下这篇文章npm-config

.npmrc文件有四种

  1. 每个项目的配置文件,通常在项目的根目录
  2. 用户配置文件
  3. 全局配置文件
  4. npm 内建配置文件,在npm安装的根目录

如果想查看每个用户配置文件和全局配置文件的具体的路径是什么,可以通过以下命令找到。

获取用户配置文件的路径

npm config get userconfig

获取全局配置文件的路径

npm config get globalconfig

这个四个配置文件的优先级是:

每个项目的配置文件 > 用户配置文件 > 全局配置文件 > 内置配置文件

至于为什么优先级是这样,这里先卖个关子。

找到源码

既然我们想要看源码就要把源码找到,有两种方式能够得到源码:

  1. github上的 npm/cli

  2. 本地的已安装的npm

大家本地肯定都装了npm的,使用自己本地的源码也可以,只不过本地的版本可能比较旧,github上是最新的版本,如果找不到源码在哪可以,可以在命令行运行

npm --help --verbose

终端就会打印出node和npm的位置以及的版本信息。

第一行是node的位置,第二行是npm的位置,第三、四行是命令参数,第五行是npm版本,我使用的npm版本是6.9.2的,第六行是node版本。

我们找到npm文件夹,如果是从github克隆下来的就是一个cli文件夹,在编辑器中打开

我建议使用本地的npm来读源码,这样我们可以在代码中加一些调试信息,方便理解。

开始读源码

在源码中我们找到 /lib/config.js ,

找到config构造函数

/npm/lib/config.js
function config (args, cb) {
  var action = args.shift()
  switch (action) {
    case 'set':
      return set(args[0], args[1], cb)
    case 'get':
      return get(args[0], cb)
    case 'delete':
    case 'rm':
    case 'del':
      return del(args[0], cb)
    case 'list':
    case 'ls':
      return npm.config.get('json') ? listJson(cb) : list(cb)
    case 'edit':
      return edit(cb)
    default:
      return unknown(action, cb)
  }
}

config命令接受两个参数,第一个参数args是我们在命令行输入的参数,第二个参数是get执行完之后的回调函数。

在命令行运行

npm config get heading

继续向下看 action 变量保存了你想要执行的动作,里面有 set get delete等,我们主要看下get,返回的是一个get方法,我们找到get方法。

/npm/lib/config.js
function get (key, cb) {
  if (!key) return list(cb)
  if (!publicVar(key)) {
    return cb(new Error('---sekretz---'))
  }
  var val = npm.config.get(key)
  if (key.match(/umask/)) val = umask.toString(val)
  output(val)
  cb()
}

可以看到在get方法里象是对传进来key进行校验,校验完成之后实际调用的是 npm.config.get(key)。

打印一下npm.config对象,看看他是何方神圣。

Conf {
    domain: null,
    _events: {
        error: { [Function: g] listener: [Object]
        }
    },
    _eventsCount: 1,
    _maxListeners: undefined,
    list: [{
        argv: [Object],
        _exit: true
    },
    {},
    {
        '//registry.npmjs.org/:_authToken': '7bea3c0d-a131-4620-9ad2-0d7346addd15',
        heading: '1'
    },
    {
        '//registry.npmjs.org/:_authToken': '7bea3c0d-a131-4620-9ad2-0d7346addd15',
        color: true,
        heading: '2'
    },
    {
        heading: '3'
    },
    {
        prefix: 'C:\\Users\\daimingyu\\AppData\\Roaming\\npm',
        heading: '4'
    }],
    root: [Getter / Setter],
    _awaiting: 0,
    _saving: 0,
    sources: {
        cli: {
            data: {
                argv: [Object],
                _exit: true,
                'user-agent': 'npm/6.9.2 node/v6.14.1 win32 x64',
                'metrics-registry': 'https://registry.npmjs.org/',
                scope: ''
            }
        },
        env: {
            data: {},
            source: {},
            prefix: ''
        },
        project: {
            data: {}
        },
        user: {
            path: 'C:\\Users\\daimingyu\\.npmrc',
            type: 'ini',
            data: {
                '//registry.npmjs.org/:_authToken': '7bea3c0d-a131-4620-9ad2-0d7346addd15',
                color: true
            }
        },
        global: {
            path: 'C:\\Users\\daimingyu\\AppData\\Roaming\\npm\\etc\\npmrc',
            type: 'ini',
            data: {
                heading: '3'
            }
        },
        builtin: {
            data: {
                prefix: 'C:\\Users\\daimingyu\\AppData\\Roaming\\npm',
                heading: '4'
            }
        }
    }
    usingBuiltin: true,
    prefix: [Getter / Setter],
    globalPrefix: [Getter / Setter],
    localPrefix: [Getter / Setter]
}

Config对象有两个非常重要的字段:list和sources,这两个字段存放了六个对象,这六个对象分别描述了:

  • 命令行
  • 环境变量
  • 项目.npmrc文件
  • 用户.npmrc文件
  • 全局.npmrc文件
  • npm内置的.npmrc文件

list存放了六个对象的内容,source字段存放了六个对象信息的地址和内容。

但是仔细看config对象发现并没有get和set方法。

其实get,set方法就在config对象的原型链上。

我们沿着原型链继续向上找

//npm.config.__proto__
Conf {
  loadPrefix: [Function: loadPrefix],
  loadCAFile: [Function: loadCAFile],
  loadUid: [Function: loadUid],
  setUser: [Function: setUser],
  getCredentialsByURI: [Function: getCredentialsByURI],
  setCredentialsByURI: [Function: setCredentialsByURI],
  clearCredentialsByURI: [Function: clearCredentialsByURI],
  loadExtras: [Function],
  save: [Function],
  addFile: [Function],
  parse: [Function],
  add: [Function],
  addEnv: [Function] 
}

可以看到还是没有get和set方法,继续向上找,不撞南墙不回头。

继续沿着原型链继续向上找

//npm.config.__proto__.__proto__
ConfigChain {
  domain: undefined,
  _events: undefined,
  _maxListeners: undefined,
  setMaxListeners: [Function: setMaxListeners],
  getMaxListeners: [Function: getMaxListeners],
  emit: [Function: emit],
  addListener: [Function: addListener],
  on: [Function: addListener],
  prependListener: [Function: prependListener],
  once: [Function: once],
  prependOnceListener: [Function: prependOnceListener],
  removeListener: [Function: removeListener],
  removeAllListeners: [Function: removeAllListeners],
  listeners: [Function: listeners],
  listenerCount: [Function: listenerCount],
  eventNames: [Function: eventNames],
  del: [Function],
  set: [Function],
  get: [Function],
  save: [Function],
  addFile: [Function],
  addEnv: [Function],
  addUrl: [Function],
  addString: [Function],
  add: [Function],
  parse: [Function],
  _await: [Function],
  _resolve: [Function]
}

终于找到get方法了,他是ConfigChain上的一个方法。

我们进到 get 方法里,看下它的具体实现。

/npm/node_modules/config-chain/index.js
ConfigChain.prototype.get = function (key, where) {
  if (where) {
    where = this.sources[where]
    if (where) where = where.data
    if (where && Object.hasOwnProperty.call(where, key)) return where[key]
    return undefined
  }
  return this.list[0][key]
}

get函数接受两个参数: key和where。

打印一下这两个参数看下它们是什么:

有同学可能会有疑问,我们明明只执行了一次get命令,为什么会打印这么多呢,其实读过了源码你就知道,不光我们自己会用config get去取配置项,npm自己也会使用config get拿配置项。

图片中红色箭头的地方就是我们在cmd中运行命令的时候打印的。key参数就是我们想要取得配置项的key,where参数都是undefined,可见该参数不是必须的。

如果传了where值(where值取值为user或global),就去config对象的source中拿到user或global对象的data,如果key是自己的属性就返回这个key对应的value,如果不是就返回undefiend。

如果没有传where值,该方法直接返回 this.list[0][key]。

但是我们可以看到config.list[0]里面没有 heading 属性,那它是怎么找到这个值的呢?

其实list数组里面的内容构成了一条原型链:

this.list[n].proto = this.list[n+1]

查找值的过程就是沿着原型链向上找的过程。

this.list[0] 如果没有 key 则从原型链向上找 ,有则返回

this.list[1] 如果没有 key 则从原型链向上找 ,有则返回

this.list[2] 如果没有 key 在从原型链向上找 ,有则返回

this.list[3] 如果没有 key 在从原型链向上找 ,有则返回

this.list[4] 如果没有 key 在从原型链向上找 ,有则返回

this.list[5] 如果没有 key 则找到default默认对象

总结

  1. 先看命令行有没有该参数,有就返回,没有进入下一步。

  2. 再看环境变量有没有该参数,有就返回,没有进入下一步。

  3. 检查用户配置文件,如果没有.npmrc文件或者有.npmrc文件但是没有设置xxx字段,则进行下一步,有.npmrc文件并且设置xxx字段则直接返回。

  4. 检查当前项目根目录,如果没有.npmrc文件或者有.npmrc文件但是没有设置xxx字段,则进行下一步,有.npmrc文件并且设置xxx字段则直接返回。

  5. 检查全局配置文件,如果没有.npmrc文件或者有.npmrc文件但是没有设置xxx字段,则进行下一步,有.npmrc文件并且设置xxx字段则直接返回。

  6. 检查npm内置文件,如果没有.npmrc文件或者有.npmrc文件但是没有设置xxx字段,则返回默认值,有.npmrc文件并且设置xxx字段则直接返回。

下期预告

下篇文章我打算分析一下 install 命令的源码,一起学习,一起成长。

ヽ✿゜▽゜)ノ