命令行工具通常有两种文件:用户指定的文件,以及工具自己带的模板。两者都写成相对路径时,很容易在换一个目录启动后出问题。
比如用户输入 ./notes,一般是指命令行所在目录下的 notes;工具自己的 templates,则应该跟着工具安装位置走。它们的起点不同。
resolve 只计算路径,require.resolve 会查找模块
创建 paths.cjs,直接用 node paths.cjs 运行:
const path = require("path");
const assert = require("assert").strict;
console.log("工作目录:", process.cwd());
console.log("脚本目录:", __dirname);
console.log("用户文件:", path.resolve("./notes"));
console.log("工具模板:", path.resolve(__dirname, "templates"));
const missing = path.resolve(__dirname, "definitely-missing.cjs");
assert.ok(path.isAbsolute(missing));
assert.throws(() => require.resolve("./definitely-missing.cjs"), {
code: "MODULE_NOT_FOUND",
});
assert.equal(require.resolve("./paths.cjs"), __filename);
assert.equal(
path.win32.join("a", "b", "c"),
["a", "b", "c"].join(String.fromCharCode(92)),
);
assert.equal(path.posix.join("a", "b", "c"), "a/b/c");
console.log(new URL("../image.png", "https://example.com/posts/a/").href);
console.log("检查通过");
path.resolve 不读取磁盘,不存在的文件照样能算出绝对路径。require.resolve 按 CommonJS 的规则查找目标,找不到会抛错,找到以后返回路径,但不会执行目标文件。
它也不是一个通用的文件存在判断。模块解析会涉及后缀、package.json、node_modules 和包入口,判断普通文件是否存在应该使用文件系统接口。
把 paths.cjs 放进子目录,在父目录执行 node 子目录/paths.cjs,再进入子目录执行 node paths.cjs。工作目录相关的输出会变化,脚本目录相关的输出不会跟着变。
磁盘路径、模块和 URL 分开处理
path.win32 和 path.posix 可以在同一台机器上调用,用来检查各自的字符串规则。这不等于在两个操作系统上都做过文件读写。
本机文件一般用默认的 path。明确采用 POSIX 格式的标识才用 path.posix。网页地址用 URL,path.join 不负责协议、查询参数和 URL 编码。
ES Module 没有 __dirname,可以从当前模块 URL 得到文件路径:
import path from "node:path";
import { fileURLToPath } from "node:url";
const directory = path.dirname(fileURLToPath(import.meta.url));
console.log(path.join(directory, "templates"));
这段存为 paths.mjs 运行即可。用户输入的相对路径仍然按工作目录处理,不能因为用了 ESM 就把它一起改成模块目录。
还有个边界:resolve 可以把 ../ 算到上级目录,它不会帮接口限制访问范围。涉及用户可控路径时,需要另外检查最终位置和符号链接。路径 API 的语义参考 Node path 文档。