1.頁面結構 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> </head> <body> </body> </html> 編碼:charset=“gbk” ;gbk2312,utf-8 註釋:<!-- 註釋內 ...
import-local 概述
當本地和全局同時存在兩個腳手架命令時,使用 import-local 可以優先載入本地腳手架命令
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "正在使用 jinhui-cli 本地版本");
} else {
require(".")(process.argv.slice(2));
}
以上述代碼為例:執行 jinhui 命令時實際執行的應該是
node C:\Program Files\nodejs\jinhui-cli\cli.js
所以將調試程式定位到全局下的 cli 文件,進入 import-local 源碼
// __filename格式化 normalizedFilename => c:\\nvm\\v14.18.0\\node_modules\\jinhui-cli\\cli.js
const normalizedFilename = filename.startsWith("file://") ? fileURLToPath(filename) : filename;
// 向上依次查找package.json文件 如果目錄存在該文件則判定全局主目錄 => c:\\nvm\\v14.18.0\\node_modules\\jinhui-cli
const globalDir = pkgDir.sync(path.dirname(normalizedFilename));
// 全局主目錄相對於運行文件的相對路徑 => cli.js
const relativePath = path.relative(globalDir, normalizedFilename);
// 獲取 package.json 內容
const pkg = require(path.join(globalDir, "package.json"));
// 本地目錄的node_modules下是否存在運行文件
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
// 本地 node_modules 絕對路徑
const localNodeModules = path.join(process.cwd(), "node_modules");
// 執行的文件在當前的node_modules下
// 本地目錄下node_modules 與 運行文件的相對路徑如果以..開頭則不是同一目錄下
// 下列判定為:運行文件在本地目錄下 且 運行文件根目錄與本地目錄根目錄相同
const filenameInLocalNodeModules =
!path.relative(localNodeModules, normalizedFilename).startsWith("..") &&
path.parse(localNodeModules).root === path.parse(normalizedFilename).root;
// 運行文件不在當前node_modules下 且 本地目錄node_modules下存在該運行文件 直接執行當前node_modules下的JS文件
return (
!filenameInLocalNodeModules && localFile && path.relative(localFile, normalizedFilename) !== "" && require(localFile)
);
pkgDir.sync 內部調用 findUp 逐級向上查找
findUp.sync("package.json", { cwd: path.dirname(normalizedFilename) });
module.exports.sync = (name, options = {}) => {
// 相對路徑轉絕對路徑
let directory = path.resolve(options.cwd || "");
// 獲取目錄根路徑
const { root } = path.parse(directory);
// 所有要查找的文件名
const filenames = [].concat(name);
while (true) {
// locatePath 遍曆數組中的文件名在指定目錄下是否存在 如果存在則將查找到的第一個文件名返回
const file = locatePath.sync(filenames, { cwd: directory });
if (file) {
return path.join(directory, name);
}
if (directory === root) {
return null;
}
// 每次遍歷逐級遞減目錄
directory = path.dirname(directory);
}
};
node.js 模塊路徑解析流程
-
Nodejs 模塊路徑解析流程
- Nqde.js 項目模塊路徑解析是通過 require.resolve 方法來實現的
-
require.resolve 就是通過 Module._resolveFileName 方法實現的
-
require.resolve 實現原理:
-
Module._resolveFileName 方法核心流程有 3 點:
- 判斷是否為內置模塊
- 通過 Module._resolveLookupPaths 方法生成 node_modules 可能存在的路徑
- 通過 Module._findPath 查詢模塊的真實路徑
-
Module._findPath 核心流程有 4 點:
- 查詢緩存(將 request 和 paths 通過\x00 合併成 cacheKey)
- 遍歷 paths,將 path 與 request 組成文件路徑 basePath
- 如果 basePath 存在則調用 fs._realPathSync 獲取文件真實路徑
- 將文件真實路徑緩存到 Module._pathCache (key 就是前面生成的 cacheKey)
-
fs._realPathSync 核心流程有 3 點:
- 查詢緩存(緩存的 key 為 p,即 Module.findPath 中生成的文件路徑)
- 從左往右遍歷路徑字元串,查詢到 / 時,拆分路徑,判斷該路徑是否為軟鏈接,如果是軟鏈接則查詢真實鏈接,並生成新路徑 p,然繼續往後遍歷,這裡有 1 個細節需要特別註意:
- 遍歷過程中生成的子路徑 base 會緩存在 knownHard 和 cache 中,避免重覆查詢
- 遍歷完成得到模塊對應的真實路徑,此時會將原始路徑 original 作為 key,真實路徑作為 value,保存到緩存中
-
require.resolve.paths 等價於 Module._resolveLookupPaths,該方法用於獲取所有 node_modules 可能存在的路徑
- require.resolve.paths 實現原理:
- 如果路徑為 / (根目錄),直接返回[/node modules]
- 否則,將路徑字元串從後往前遍歷,查詢到 / 時,拆分路徑,在後面加上 node_modules,並傳入一個 paths 數組,直至查詢不到 /後返回 paths 數組
_resolveFilename 查找文件真實路徑流程圖
resolveCwd.silent 源碼調用 Module._resolveFilename 查找本地目錄是否可載入到指定模塊 如能則返迴文件路徑
module.exports.silent = (moduleId) => resolveFrom.silent(process.cwd(), moduleId);
const resolveFrom = (fromDirectory, moduleId, silent) => {
const fromFile = path.join(fromDirectory, "noop.js");
const resolveFileName = () =>
// 調用Module模塊內置方法_resolveFilename 載入模塊如果存在則返迴文件地址
Module._resolveFilename(moduleId, {
id: fromFile,
filename: fromFile,
// 調用Module模塊內置方法_nodeModulePaths 生成所有可能存在node_modules的目錄數組
paths: Module._nodeModulePaths(fromDirectory),
});
// silent為靜默方式不拋出異常返回undefined
if (silent) {
try {
return resolveFileName();
} catch (error) {
return;
}
}
return resolveFileName();
};
內置模塊 Module._nodeModulePaths 逐級目錄添加 node_modules
const nmChars = [115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110];
const nmLen = nmChars.length;
Module._nodeModulePaths = function (from) {
// 生成絕對路徑
from = path.resolve(from);
// 如果傳入路徑為根路徑直接返回["/node_modules"]
if (from === "/") return ["/node_modules"];
// 定義路徑數組
const paths = [];
// 從最後一位開始遍歷路徑
// p變數用來判斷最後一項目錄長度是否等於node_modules
// last變數用來存儲當前需要截取的路徑長度
for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
const code = StringPrototypeCharCodeAt(from, i);
// 判斷當前遍歷位置是否為 \/字元
if (code === "47") {
// 如果遍歷目錄不是node_modules則添加node_modules
if (p !== nmLen) ArrayPrototypePush(paths, StringPrototypeSlice(from, 0, last) + "/node_modules");
last = i;
p = 0;
} else if (p !== -1) {
// 遍歷字元是否跟nmChars每個charCode相等 nmChars存放了node_modules倒序的charCode 如果相等p++
if (nmChars[p] === code) {
++p;
} else {
p = -1;
}
}
}
// 根目錄添加node_modules
ArrayPrototypePush(paths, "/node_modules");
return paths;
};
內置模塊 Module._resolveFilename 讀取文件真實路徑
// 下列代碼對node.js源碼進行了部分刪減
Module._resolveFilename = function (request, parent, isMain) {
// 判斷是否可以被載入的內置模塊
if (StringPrototypeStartsWith(request, "node:") || NativeModule.canBeRequiredByUsers(request)) {
return request;
}
// 將paths和環境變數node_modules進行合併;
const paths = Module._resolveLookupPaths(request, parent);
// 查找路徑文件
const filename = Module._findPath(request, paths, isMain, false);
if (filename) return filename;
};
內置模塊 Module._findPath 讀取文件真實路徑
Module._findPath = function (request, paths, isMain) {
// 判斷是否為絕對路徑
const absoluteRequest = path.isAbsolute(request);
// 如果為絕對路徑則paths置空 如果paths不存在則返回false
if (absoluteRequest) {
paths = [""];
} else if (!paths || paths.length === 0) {
return false;
}
// 判斷是否存在緩存如果存在返回緩存 緩存key為 模塊名 + 模塊path jinhui-cli/cli.js/C:/Users/xiabin
const cacheKey = request + "\x00" + ArrayPrototypeJoin(paths, "\x00");
// 存在緩存直接返回緩存
const entry = Module._pathCache[cacheKey];
if (entry) return entry;
let exts;
// 判斷結尾是否為 \/字元
let trailingSlash =
request.length > 0 && StringPrototypeCharCodeAt(request, request.length - 1) === CHAR_FORWARD_SLASH;
if (!trailingSlash) {
// 判斷結尾是否為相對路徑 /.. /. .. .
trailingSlash = RegExpPrototypeTest(trailingSlashRegex, request);
}
// 遍歷所有path
for (let i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
// 當前path不是目錄則直接跳過本次迴圈
if (curPath && stat(curPath) < 1) continue;
// 文件路徑
const basePath = path.resolve(curPath, request);
let filename;
const rc = stat(basePath);
// 結尾是不是 \/ 不是相對路徑
if (!trailingSlash) {
// basePath是否為文件
if (rc === 0) {
if (!isMain) {
// 是否阻止做超鏈接
if (preserveSymlinks) {
filename = path.resolve(basePath);
} else {
// 將軟連接轉換為真實文件地址
filename = toRealPath(basePath);
}
// 轉絕對路徑
} else if (preserveSymlinksMain) {
filename = path.resolve(basePath);
} else {
// 將軟連接轉換為真實文件地址
filename = toRealPath(basePath);
}
}
}
// 文件未找到且查詢路徑為目錄 則嘗試生成文件路徑
if (!filename && rc === 1) {
if (exts === undefined) exts = ObjectKeys(Module._extensions);
filename = tryPackage(basePath, exts, isMain, request);
}
// 查詢到文件真實路徑將當前路徑緩存 並返回真實路徑
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
return false;
};
內置模塊 fs.realpathSync 將軟連接轉換為真實路徑
// toRealPath內部調用此方法並將軟連 => 真實路徑的所有緩存傳入
function realpathSync(p, options) {
// options不存在則置空
options = getOptions(options, emptyObj);
// 如果為URL路徑則進行轉換
p = toPathIfFileURL(p);
// 如果不是string字元串則轉為string
if (typeof p !== "string") {
p += "";
}
// 是否為有效路徑
validatePath(p);
// 將相對路徑轉為絕對路徑
p = pathModule.resolve(p);
// 查詢緩存如果存在緩存則直接返回緩存中的真實路徑
const cache = options[realpathCacheKey];
const maybeCachedResult = cache && cache.get(p);
if (maybeCachedResult) {
return maybeCachedResult;
}
// 所有軟連接緩存 通過Object.create(null)創建的對象沒有原型鏈 是一個純粹的對象
const seenLinks = ObjectCreate(null);
const knownHard = ObjectCreate(null);
// 將軟連接路徑作為緩存key p表示真實路徑
const original = p;
let pos;
let current;
let base;
let previous;
// 找到根目錄
current = base = splitRoot(p);
pos = current.length;
while (pos < p.length) {
// 傳入真實路徑 與 查找下一級目錄開始位置
const result = nextPart(p, pos);
previous = current;
// 判斷是否找到下一級目錄 並將本級目錄與之前目錄做拼接
if (result === -1) {
const last = p.slice(pos);
current += last;
base = previous + last;
pos = p.length;
} else {
current += p.slice(pos, result + 1);
base = previous + p.slice(pos, result);
pos = result + 1;
}
// 判斷緩存中是否存在本級目錄 如果存在則跳過本次迴圈
if (knownHard[base] || (cache && cache.get(base) === base)) {
if (isFileType(statValues, S_IFIFO) || isFileType(statValues, S_IFSOCK)) {
break;
}
continue;
}
let resolvedLink;
const maybeCachedResolved = cache && cache.get(base);
if (maybeCachedResolved) {
resolvedLink = maybeCachedResolved;
} else {
const baseLong = pathModule.toNamespacedPath(base);
const ctx = { path: base };
const stats = binding.lstat(baseLong, true, undefined, ctx);
handleErrorFromBinding(ctx);
// 通過文件狀態判斷是否為軟連接 如果不是軟連接則加入緩存並跳過
if (!isFileType(stats, S_IFLNK)) {
knownHard[base] = true;
if (cache) cache.set(base, base);
continue;
}
let linkTarget = null;
let id;
if (!isWindows) {
// 獲取設備ID
const dev = stats[0].toString(32);
// 獲取文件ID
const ino = stats[7].toString(32);
// 生成唯一鍵 作為軟連接緩存的key
id = `${dev}:${ino}`;
// 判斷緩存是否存在
if (seenLinks[id]) {
linkTarget = seenLinks[id];
}
}
// 緩存不存在
if (linkTarget === null) {
// 當前路徑
const ctx = { path: base };
// 獲取當前文件狀態
binding.stat(baseLong, false, undefined, ctx);
handleErrorFromBinding(ctx);
// 獲取軟鏈對應的真實路徑
linkTarget = binding.readlink(baseLong, undefined, undefined, ctx);
handleErrorFromBinding(ctx);
}
// 相對路徑轉絕對路徑
resolvedLink = pathModule.resolve(previous, linkTarget);
// 獲取到的軟鏈對應的真實路徑存入緩存
if (cache) cache.set(base, resolvedLink);
if (!isWindows) seenLinks[id] = linkTarget;
}
// 重新生成文件真實路徑
p = pathModule.resolve(resolvedLink, p.slice(pos));
// 生成新的路徑重覆每個目錄是否為軟鏈驗證流程以確保完整路徑每個目錄都是真實路徑
current = base = splitRoot(p);
pos = current.length;
if (isWindows && !knownHard[base]) {
const ctx = { path: base };
binding.lstat(pathModule.toNamespacedPath(base), false, undefined, ctx);
handleErrorFromBinding(ctx);
knownHard[base] = true;
}
}
if (cache) cache.set(original, p);
return encodeRealpathResult(p, options);
}