编码思路-工程化

which/whereis

  • which

    搜索范围:仅在当前用户的 PATH 环境变量指定的目录中搜索。

    输出内容:仅返回第一个匹配的可执行文件路径。

  • whereis

    搜索范围:在预定义的标准系统目录(如 /bin, /usr/include, /usr/share/man 等)中搜索,不依赖 PATH。

    输出内容:返回所有相关文件路径(二进制、手册页、源代码等)。

npm/yarn/pnpm

  • npm

    嵌套依赖结构:早期版本采用嵌套的 node_modules 结构,导致依赖重复和路径过长问题。

    确定性依赖:在 npm@5 后引入 package-lock.json,锁定依赖版本(解决早期版本依赖不确定性)。

    早期有重复依赖的问题,扁平化处理后可能导致幽灵依赖。

  • yarn

    确定性依赖:引入 yarn.lock 文件(早于 npm 的 package-lock.json),锁定依赖树。

    并行下载:利用并行请求提升安装速度。

    离线缓存:全局缓存已下载的依赖包,减少重复下载。

    扁平化结构:将嵌套依赖提升到 node_modules 顶层,减少重复安装(但可能引发依赖冲突)

    PnP 模式: 劫持 Node.js 的模块解析逻辑,使其不再依赖物理的 node_modules 目录,而是通过映射表(.pnp.cjs)直接定位到 .zip 文件中的代码。

  • pnpm

    硬链接 + 符号链接:所有依赖包存储在全局存储目录(类似缓存)。通过硬链接共享相同版本的依赖,减少磁盘占用。使用符号链接在项目中按需链接依赖。

    严格依赖隔离:每个包的依赖在独立的 node_modules 中,避免幽灵依赖。

本地依赖

通常使用 lerna, nx 等工具,原生实现可以使用 file 协议。

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "file:/mnt/d/Workspace/my-cli/packages/util"
}
}

使用 pnpm

1
2
3
4
// pnpm-workspace.yaml
packages:
- "packages/*"

1
2
3
4
5
{
"dependencies": {
"@my/cli-util": "workspace:*"
}
}

路径处理

  • import-local 优先使用自己的本地安装版本

  • pkg-dir 查找项目的根目录

  • resolve-cwd 从 CWD 目录解析模块的绝对路径。

  • which 在 path 环境变量中查找第一个匹配

  • node 默认处理 .js .json .node 文件, 其他文件格式当作 js 文件处理

    1
    2
    3
    4
    5
    //a.txt
    console.log(1);

    // index.js
    const a = require("./a.txt"); // 不会报错,可以加载

root 检查

root-check, 如果使用 root 用户启动,尝试降级

参数处理

minimist 解析命令行参数

yargs commander 提供交互式的命令行

检查包是否安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let dir = __dirname;

do {
if (fs.statSync(path.join(dir, "node_modules", packageName)).isDirectory()) {
return true;
}
} while (dir !== (dir = path.dirname(dir)));

//require("module") 用于管理模块的接口
for (const internalPath of require("module").globalPaths) {
if (fs.statSync(path.join(internalPath, packageName)).isDirectory()) {
return true;
}
}
return false;

区分使用的是那种包管理器

1
2
3
4
5
6
7
if (fs.existsSync(path.resolve(process.cwd(), "yarn.lock"))) {
packageManager = "yarn";
} else if (fs.existsSync(path.resolve(process.cwd(), "pnpm-lock.yaml"))) {
packageManager = "pnpm";
} else {
packageManager = "npm";
}

原生的命令行交互 readLine

1
2
3
4
5
6
7
8
9
10
11
const readLine = require("readline");

const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr,
});

questionInterface.question("question one (yes/no):", (answer) => {
if (answer.startsWith("y")) {
}
});

子进程的 spawn 和 exec 函数之间的区别

Node.js 的子进程模块(child_process)有两个函数 spawn 和 exec,使用这两个函数,我们可以启动一个子进程来执行系统中的其他程序。刚接触 child_process 的人可能会问,为什么做同一件事会有两个函数,以及应该使用哪个函数。我将解释 spawn 和 exec 之间的区别,以帮助你决定何时使用哪个函数。

child_process.spawn 和 child_process.exec 的最大区别在于它们的返回值–spawn 返回一个流,而 exec 返回一个缓冲区。

child_process.spawn 返回一个包含 stdout 和 stderr 流的对象。您可以点击 stdout 流来读取子进程发回 Node 的数据。作为一个流,stdout 具有流所具有的 “data”(数据)、”end”(结束)和其他事件。当您希望子进程向 Node 返回大量数据(如图像处理、读取二进制数据等)时,最好使用 spawn。

child_process.spawn 是 “异步 asynchronous”(异步不同步)的,这意味着一旦子进程开始执行,它就会以流的形式从子进程发回数据。

这里有一个例子,我用 spawn 读取了 Node 的 curl 请求结果。

child_process.exec 返回子进程输出的整个缓冲区。默认情况下,缓冲区大小为 200k。如果子进程返回的数据超过该值,程序就会崩溃,并显示错误信息 “Error: maxBuffer exceeded”(错误:超过最大缓冲区)。你可以在执行选项中设置更大的缓冲区大小来解决这个问题。但你不应该这样做,因为 exec 并不适合向 Node 返回巨大缓冲区的进程。你应该使用 spawn 来解决这个问题。那么,exec 用来做什么呢?用它来运行返回结果状态而不是数据的程序。

child_process.exec 是 “同步异步 “的,也就是说,虽然 exec 是异步的,但它会等待子进程结束,并尝试一次性返回所有缓冲数据。如果 exec 的缓冲区大小设置得不够大,就会出现 “maxBuffer exceeded”(超过最大缓冲区)错误,导致执行失败。

请看这里的一个示例,我使用 exec 执行 wget 下载文件,并向 Node 更新执行状态。

这就是 Node 的子进程 span 和 exec 之间的区别。当你希望子进程向 Node 返回大量二进制数据时,请使用 spawn;当你希望子进程返回简单的状态信息时,请使用 exec。

1
2
3
4
5
6
7
8
9
10
11
12
13
const cp = require("child_process");
new Promise((resolve, reject) => {
const executedCommand = cp.spawn("echo 1", [], {
stdio: "inherit",
shell: true,
});

executedCommand.on("error", reject);

executedCommand.on("exit", (code) => {
if (code === 0) resolve();
});
}).then(() => {});

区分包的模块化方案

1
2
3
4
5
6
7
const pkgPath = require.resolve(`${packageName}/package.json`);
const pkg = require(pkgPath);
if (pkg.type === "module" || /\.mjs/i.test(pkg.bin[name])) {
import(path.resolve(path.dirname(pkgPath), pkg.bin[name])).catch();
} else {
require(path.resolve(path.dirname(pkgPath), pkg.bin[name]));
}

检查两个命令是否相似

(莱文斯坦距离)[https://zh.wikipedia.org/wiki/%E8%90%8A%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E9%9B%A2] (fastest-levenshtein)[https://github.com/ka-weihe/fastest-levenshtein]

获取某个包的最新版本

推荐使用 cross-spawn

1
2
3
4
5
6
7
8
9
10
const cp = require("child_process");

const { output } = cp.spawnSync(
"npm.cmd",
["view", "react@latest", "version"],
{
stdio: "pipe",
}
);
console.log(output.toString());

或者通过 api 拉取 https://registry.npmjs.com/lodash

是否是浏览器环境

1
2
// 使用 typeof, 对于不存在的变量会返回 undefined
const hasDocument = typeof document !== "undefined";

检查 IE 浏览器

1
2
3
var isIE11 =
typeof navigator !== "undefined" &&
navigator.userAgent.indexOf("Trident") !== -1;

版本号对比

1
2
3
4
5
6
7
8
const semver = require("semver");

const version1 = "1.2.3";
const version2 = "2.0.0";

if (semver.gt(version1, version2)) {
console.log(`${version1} is greater than ${version2}`);
}

commander 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Command } from "commander";
const program = new Command();

program
.name("sun-cli")
.description("这是一个描述")
.option("--targetCommand", "参数的描述", "team1/command")
.action((options) => {
console.log(options);
});

program
.command("clone <source> [destination]")
.description("子命令的描述")
.action((source, destination) => {
console.log("clone command called");
});

program.parse();
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信