用nrm管理npm镜像——从使用到源码解析

941 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情 >>

设置或者切换npm镜像是我们经常遇到的场景,你是怎么做的呢?笔者之前是去找到.npmrc文件然后注释掉原来的镜像,再添加新的镜像。后来笔者了解到nrm可以使用命令行的方式来切换或者添加npm镜像,于是研究了一下其源码探究其原理。阅读本文你将了解nrm的使用以及其原理,了解常用命令行工具commander, 了解ini文件格式等,欢迎阅读学习~

1.nrm简介

nrm 用于管理镜像,是一个可以切换npm镜像的管理工具。可以帮助我们轻松地在不同npm注册表之间切换,包括npm、cnpm、淘宝、nj(nodejitsu)。下面我们看一下如何使用nrm:

2.nrm安装使用

如下是安装和查看是否安装成功的命令:

npm i -g nrm
nrm -V

常用nrm命令如下:

想了解更多关于nrm的内容请查看文档和资料

下图是使用nrm ls命令查看镜像:

下图是将镜像切换为npm

3.nrm源码分析

研究源码之前的准备工作必是clone代码啦:

git clone git@github.com:Pana/nrm.git

有了代码之后可以查看package.json文件分析程序的入口文件:

由上图可见程序的入口文件为cli.js, 下面首先分析cli.js

3.1 cli.js——命令定义文件

如下为cli.js文件中截取的部分代码:

const actions = require('./actions');
const PKG = require('./package.json');
const { program } = require('commander');

program
  .version(PKG.version);

program
  .command('ls')
  .description('List all the registries')
  .action(actions.onList);

program
  .command('current')
  .option('-u, --show-url', 'Show the registry URL instead of the name')
  .description('Show current registry name or URL')
  .action(actions.onCurrent);

program
  .command('use <name>')
  .description('Change current registry')
  .action(actions.onUse);

可见cli.js文件定义了nrm都支持哪些cli命令。核心依赖是commander,其常用API如下图所示:

通过上图我们知道.action()的参数是命令的处理函数,下面我们就研究一下这些命令处理函数的定义。

3.2 action.js——命令处理函数

如下为action.js文件截取的部分代码:

const open = require('open');
const chalk = require('chalk');
const fetch = require('node-fetch');

const {
  exit,
  readFile,
  writeFile,
  geneDashLine,
  printMessages,
  printSuccess,
  getCurrentRegistry,
  getRegistries,
  isLowerCaseEqual,
  isRegistryNotFound,
  isInternalRegistry,
} = require('./helpers');

const { NRMRC, NPMRC, AUTH, EMAIL, ALWAYS_AUTH, REPOSITORY, REGISTRY, HOME } = require('./constants');

async function onCurrent({ showUrl }) {
  const currentRegistry = await getCurrentRegistry();
  let usingUnknownRegistry = true;
  const registries = await getRegistries();
  for (const name in registries) {
    const registry = registries[name];
    if (isLowerCaseEqual(registry[REGISTRY], currentRegistry)) {
      usingUnknownRegistry = false;
      printMessages([`You are using ${chalk.green(showUrl ? registry[REGISTRY] : name)} registry.`]);
    }
  }
  if (usingUnknownRegistry) {
    printMessages([
      `Your current registry(${currentRegistry}) is not included in the nrm registries.`,
      `Use the ${chalk.green('nrm add <registry> <url> [home]')} command to add your registry.`,
    ]);
  }
}

// 巴拉巴拉省略一大堆

module.exports = {
  onList,
  onCurrent,
  onUse,
  // 巴拉巴拉省略一大堆
  onLogin,
};

通过阅读如上代码可以看到action.js引入了helpers.js定义的工具函数,引入了constants.js定义的常量值,基于这些,方法内部定义了许多函数名是以on开头的命令处理函数,然后导出这些命令处理函数。

我们以onCurrent为例简单分析一下处理函数的实现:

  • 首先获取正在使用的npm镜像——currentRegistry,这是通过调用getCurrentRegistry实现的
  • 然后获取所有nrm支持的镜像——registries,这是通过调用getRegistries实现的
  • 遍历registries和currentRegistry比较,判断是否为nrm内置镜像
  • 如果是nrm内置支持的则提示“你正在使用的是何种镜像”;否则提示“你正在使用的是什么镜像,但是不在nrm内置支持的范畴,你可以使用nrm add 将其加入”

下面我们重点研究一下helpers.js中定义的工具方法:

3.3 helper.js 工具函数

3.3.1 readFile 读文件

async function readFile(file) {
  return new Promise(resolve => {
    if (!fs.existsSync(file)) {
      resolve({});
    } else {
      try {
        const content = ini.parse(fs.readFileSync(file, 'utf-8'));
        resolve(content);
      } catch (error) {
        exit(error);
      }
    }
  });
}

文件不存在返回不含任何属性的空对象{},否则读取文件内容并将ini格式内容转为对象。readFile方法主要用于读取.npmrc文件。.npmrc文件是npm的配置文件,其格式为ini,最后的rc可以理解为“run command”也就是运行命令的意思,还有很多配置文件都以rc结尾,例如.eslintrc ,.browserslistrc,.babelrc等。

另外介绍一下ini格式。ini是英文"initialization"的头三个字母的缩写,ini格式文件的后缀名不一定非得是.ini,也可以是.cfg、.conf或者是.txt,当然包括.xxxxrc。ini格式文件有三个要素,parameters,sections和comments。其中最基本的就是parameters,每一个parameter都有一个name和一个value,格式为 name = value,下图为笔者的.npmrc文件截图:

想了解更多关于rc结尾文件的含义以及ini文件格式的内容可以阅读文末的参考资料。

我们看到在readFile文件中调用了ini.parse方法,其中ini用于 ini格式文件的序列化和解析,其常用API如下图所示:

3.3.2 writeFile写文件

async function writeFile(path, content) {
  return new Promise(resolve => {
    try {
      fs.writeFileSync(path, ini.stringify(content));
      resolve();
    } catch (error) {
      exit(error);
    }
  });
}

将对象格式的content转为ini格式字符串并写入path文件。关键是node.js fs模块 APIfs.writeFileSync。

3.3.3 padding字符串格式化

function padding(message = '', before = 1, after = 1) {
  return new Array(before).fill(' ').join('') + message + new Array(after).fill(' ').join('');
}

在字符串前后增加空格。

3.3.4 打印成功、失败和普通消息

function printSuccess(message) {
  console.log(chalk.bgGreenBright(padding('SUCCESS')) + ' ' + message);
}

function printError(error) {
  console.error(chalk.bgRed(padding('ERROR')) + ' ' + chalk.red(error));
}

function printMessages(messages) {
  for (const message of messages) {
    console.log(message);
  }
}

printSuccess 用于打印成功消息;printError 用于打印错误;printMessages用于 逐条打印消息。其中打印成功和失败消息用到了chalk的API。

3.3.5 geneDashLine生成虚线

function geneDashLine(message, length) {
  const finalMessage = new Array(Math.max(2, length - message.length + 2)).join('-');
  return padding(chalk.dim(finalMessage));
}

此方法用于生成虚线,调用了padding方法在前后增加空格,chalk.dim用于浅高亮。

3.3.6 isLowerCaseEqual转小写后比较是否相等

function isLowerCaseEqual(str1, str2) {
  if (str1 && str2) {
    return str1.toLowerCase() === str2.toLowerCase();
  } else {
    return !str1 && !str2;
  }
}

isLowerCaseEqual用于比较两个参数str1和str2转成小写之后比较是否相等。

3.3.7 getCurrentRegistry获取当前镜像

async function getCurrentRegistry() {
  const npmrc = await readFile(NPMRC);
  return npmrc[REGISTRY];
}

获取当前的镜像,从npmrc文件中读取registry字段。NPMRC常量定义了.npmrc文件的路径:

const NPMRC = path.join(process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'], '.npmrc');

以笔者使用的windows系统为例,process.env.USERPROFILE 指向了系统盘当前用户目录,此位置即为存放.npmrc文件的路径,详细可参考下图:

3.3.8 getRegistries 获取所有镜像

async function getRegistries() {
  const customRegistries = await readFile(NRMRC);
  return Object.assign({}, REGISTRIES, customRegistries);
}

读取.nrmrc文件,和registries.json文件内容合并。registries.json文件内容为nrm原本内置支持的,.nrmrc文件中为使用者后添加的。registries.json文件内容如下:

{
  "npm": {
    "home": "https://www.npmjs.org",
    "registry": "https://registry.npmjs.org/"
  },
  "yarn": {
    "home": "https://yarnpkg.com",
    "registry": "https://registry.yarnpkg.com/"
  },
  "tencent": {
    "home": "https://mirrors.cloud.tencent.com/npm/",
    "registry": "https://mirrors.cloud.tencent.com/npm/"
  },
  "cnpm": {
    "home": "https://cnpmjs.org",
    "registry": "https://r.cnpmjs.org/"
  },
  "taobao": {
    "home": "https://npmmirror.com",
    "registry": "https://registry.npmmirror.com/"
  },
  "npmMirror": {
    "home": "https://skimdb.npmjs.com/",
    "registry": "https://skimdb.npmjs.com/registry/"
  }
}

3.3.9 isRegistryNotFound 判断是否找到相应npm镜像

async function isRegistryNotFound(name, printErr = true) {
  const registries = await getRegistries();
  if (!Object.keys(registries).includes(name)) {
    printErr && printError(`The registry '${name}' is not found.`);
    return true;
  }
  return false;
}

此方法用于判断是否找到了相应的镜像。获取所有代码,使用Object.keys获取镜像的名字数组,然后判断是否包含name。

3.3.10 isInternalRegistry判断是否是nrm内置镜像

async function isInternalRegistry(name, handle) {
  if (Object.keys(REGISTRIES).includes(name)) {
    handle && printError(`You cannot ${handle} the nrm internal registry.`);
    return true;
  }
  return false;
}

此方法用于判断是否是nrm内置的镜像,如果镜像名字不是registries.json文件中定义的那么就不是nrm内置的。

3.3.11 exit 错误处理

// export process for mock
module.exports = global.process;

function exit(error) {
  error && printError(error);
  process.exit(1);
}

exit方法用于错误处理。process.exit(code) 可以是0也可以是1 , 0表示没有任何故障结束进程,而1表示由于某种故障而结束进程。

4.总结

nrm 能够以命令行的方式对npm的镜像进行配置得益于其使用了commander作为命令行工具定义了相关的命令,以及为这些命令指定了处理函数。nrm之所以能够找到.npmrc文件在于使用node提供的process.env.USERPROFILE 进行了路径定位。此外能够修改.npmrc是因为nrm使用node fs 模块进行文件的读写,使用ini进行文件格式的解析和转换。 下图累出了nrm使用到的核心技术点:

参考资料

【1】c结尾文件的含义

【2】ini配置文件格式