import-local執行流程與node模塊路徑解析流程

来源:https://www.cnblogs.com/tengx/archive/2023/07/12/17546876.html
-Advertisement-
Play Games

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);
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:華為開發者大會2023(Cloud)期間,華為雲面向全球行業領袖、技術專家、社區大咖,舉辦“99%安全事件5分鐘內閉環——華為雲安全運營創新與實踐”專題論壇。 在華為開發者大會2023(Cloud)期間,華為雲面向全球行業領袖、技術專家、社區大咖,舉辦“99%安全事件5分鐘內閉環——華為雲安全 ...
  • 摘要:GaussDB (for Redis)通過賬號管理、許可權隔離、高危命令禁刪/重命名、安全IP免密登錄、實例回收站等企業級特性,保障用戶資料庫數據和信息安全。 本文分享自華為雲社區《數據安全沒保證?GaussDB(for Redis)為你保駕護航》,作者: GaussDB 資料庫。 近日,一些用 ...
  • 總結一下Hive面試寶典中的要點,方便讀者快速過一遍Hive面試所需要的知識點。本文請搭配 2023 Hive 面試寶典 來食用更美味喲 ...
  • #### 先說一些廢話 總結一下Hive面試寶典,方便讀者快速過一遍Hive面試所需要的知識點 ## Hive的介紹 ### Hive和Hadoop的關係 1. Hive利用hdfs存儲數據,利用MapReduce查詢數據 2. Hive的數據存儲在hdfs上,簡單的說Hive就是hdfs的簡單一種 ...
  • 基於tauri+vite4+pinia2跨端後臺管理系統應用實例TauriAdmin。 tauri-admin 基於最新跨端技術 Tauri Rust webview2 整合 Vite4 構建桌面端通用後臺管理解決方案。搭載輕量級ve-plus組件庫、支持多視窗切換管理、vue-i18n多語言包、動 ...
  • 數組的32中方法=>{ 1.push(): 在數組末尾添加一個或多個元素,並返回修改後的數組。 let fruits = ['apple', 'banana', 'orange']; fruits.push('mango'); console.log(fruits); // 輸出: ['apple' ...
  • Flutter是Google推出的一個開源的、高性能的移動應用開發框架,可以用一套代碼庫開發Android和iOS應用。Dart則是Flutter所使用的編程語言。讓我們來看看如何搭建Flutter開發環境,並瞭解Dart語言的基礎知識。 ...
  • # flex佈局 ## 上節複習 選擇器進階: 偽類選擇器: 當滿足特定條件時,激活對應的樣式 元素:hover{} 當滑鼠經過元素時,激活樣式 偽元素選擇器: 創建一個虛假的元素.不能被選中.不存在網頁dom中(安全性/性能) 元素::before{content:'內容'} 在元素前面添加內容 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...