周下载量千万的npm包import-local

663 阅读3分钟

使用import-local可以让全局安装的包使用你项目中自己的版本。意思就是假设你在电脑里全局安装了webpack4,但是项目中使用的是webpack5,当你在项目中使用webpack命令的时候,他会优先使用webpack5版本,当然这个前提是你的node_modules已经安装l webpack。

import-local常常在cli工具中使用,可以方便cli工具开发(可以参考lerna)。使用方式如下:

```
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");

if (importLocal(__filename)) {
  require("npmlog").info("cli", "using local version of lerna");
} else {
  require(".")(process.argv.slice(2));
}
```

使用方式很简单,导入import-local,传入当前执行文件的路径,当条件满足执行对应逻辑即可。我们通过调试来分析它的实现原理: import-local执行时通过传递__filename作为参数执行。__filename在node中为当前模块的文件名。 这是当前模块文件的已解析符号链接的绝对路径。例如:从 /Users/mjr 运行 node example.js

console.log(__filename);
// 打印: /Users/mjr/example.js

以lerna为例,在执行lerna命令时,他是先执行node全局lib/node_modules中的lerna的可执行文件(全局安装的npm包都会安装在node安装目录下的lib/node_modules文件夹)。

import-local内部源码如下:

'use strict';
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');

module.exports = filename => {
 const globalDir = pkgDir.sync(path.dirname(filename));
 const relativePath = path.relative(globalDir, filename);
 const pkg = require(path.join(globalDir, 'package.json'));
 const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));

 // Use `path.relative()` to detect local package installation,
 // because __filename's case is inconsistent on Windows
 // Can use `===` when targeting Node.js 8
 // See https://github.com/nodejs/node/issues/6624
 return localFile && path.relative(localFile, filename) !== '' ? require(localFile) : null;
};

我们先看第一行代码const globalDir = pkgDir.sync(path.dirname(filename));,它使用了path.dirname处理了传入的filename参数。path.dirname为当前模块的目录名,例如:从 /Users/mjr 运行 node example.js

console.log(__dirname);
// 打印: /Users/mjr

我们前面说过__filename在node中为当前模块的文件名,在lerna中执行,以我的电脑为例,该路径为:/usr/local/lib/node_modules/lerna/cli.js(具体路径会因你电脑上node的安装目录不同有所差异)。 path.dirname处理了这个路径返回的就是/usr/local/lib/node_modules/lerna,然后它将返回路径交给了pkg-dir处理,pkg-dir执行找到 Node.js 项目或 npm 包的根目录,在这里就是lerna的安装目录。

globalDir/usr/local/lib/node_modules/lerna

接下来在看const relativePath = path.relative(globalDir, filename);path.relative() 方法根据当前工作目录返回从 from 到 to 的相对路径。 如果 from 和 to 都解析为相同的路径(在分别调用 path.resolve() 之后),则返回零长度字符串。如果零长度字符串作为 from 或 to 传入,则将使用当前工作目录而不是零长度字符串。

relativePath为:cli.js

在看const pkg = require(path.join(globalDir, 'package.json'));path.join() 方法使用特定于平台的分隔符作为定界符将所有给定的 path 片段连接在一起,然后规范化生成的路径。这里使用path.join将erna的安装目录globalDirpackage.json平成特定平台的路径,然后使用require加载并解析成一个json对象。

require支持加载的文件类型有三种:

  • .js
  • .json
  • .node
  1. .js需要使用module.exports/exports导出模块内容
  2. .json则使用JSON.parse解析
  3. .node则是通过process.dlopen打开c++插件(C++ AddOns) require加载的文件也可以是其他的文件后缀,如:.txt。当加载的是出上述三种以外的文件后缀时,它会当成js文件解析。

pkgpackage.json的对象信息。

const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));通过path.join(pkg.name, relativePath)得到lerna/cli.js,然后使用resolve-cwd库的silent方法在当前工作目录中找到lerna/cli.js,silent方法在可以找到该文件时,返回完整的路径,否则返回undefined,这里返回了/Users/xxx/lerna-v3.22.1/core/lerna/cli.js,也就是工作目录的lerna可执行文件。

const localNodeModules = path.join(process.cwd(), 'node_modules');
const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..');

通过path.relative探测本地是否安装的相应的node包。

到了这里,可以看出import-local是通过获取处理各种路径,然后获取本地localFile路径,并根据本地node_modules是否安装,来判断是否优先使用本地版本的包。 相关调试参数如下:

截屏2021-10-06 下午12.58.41.png