概要Node.js の登場により、フロントエンド エンジニアはサーバー上のクライアント間で作業できるようになりました。もちろん、新しいオペレーティング環境の誕生は、新しいモジュール、機能、さらにはイデオロギーの革新ももたらします。この記事では、読者が Node.js (以下、Node と呼びます) のモジュール設計のアイデアを理解し、いくつかのコア ソース コード実装を分析できるようにします。 CommonJS 仕様Node は当初、CommonJS 仕様に従って独自のモジュール システムを実装し、同時に仕様とは異なるカスタマイズをいくつか行いました。 CommonJS 仕様は、JavaScript のスコープ問題を解決するために定義されたモジュール形式であり、各モジュールが独自の名前空間で実行できるようにします。 この仕様では、モジュールは module.exports を通じて外部変数または関数をエクスポートし、require() を通じて他のモジュールの出力を現在のモジュール スコープにインポートし、次の規則に従う必要があることを強調しています。
Node の CommonJS 仕様の実装モジュール内の module.require 関数とグローバル require 関数は、モジュールをロードするために定義されています。 Node モジュール システムでは、各ファイルは個別のモジュールと見なされます。モジュールがロードされると、Module オブジェクトのインスタンスとして初期化されます。Module オブジェクトの基本的な実装とプロパティは次のとおりです。 関数モジュール(id = "", 親) { // モジュール ID。通常はモジュールの絶対パスです。this.id = id; this.path = path.dirname(id); this.exports = {}; //現在のモジュール呼び出し元 this.parent = parent; 親を更新します。 this.filename = null; // モジュールはロードされていますか? this.loaded = false; //現在のモジュールによって参照されるモジュール this.children = []; } 各モジュールは、エクスポート属性をユーザー インターフェイスとして公開します。 モジュールのエクスポートとインポートNode では、module.exports オブジェクトを使用して変数または関数全体をエクスポートすることも、エクスポートする変数または関数を exports オブジェクトの属性にマウントすることもできます。コードは次のとおりです。 // 1. エクスポートを使用する: 通常はツール ライブラリ関数または定数をエクスポートするために使用します。exports.name = 'xiaoxiang'; exports.add = (a, b) => a + b; // 2. module.exports を使用します。オブジェクト全体または単一の関数をエクスポートします... モジュール.エクスポート = { 追加、 マイナス } モジュールはグローバル require 関数を通じて参照されます。モジュール名、相対パス、絶対パスを渡すことができます。モジュール ファイルのサフィックスが js/json/node の場合、次のコードに示すようにサフィックスを省略できます。 // モジュールを参照 const { add, minus } = require('./module'); const a = require('/usr/app/module'); 定数 http = require('http'); 注記: exports 変数はモジュールのファイルレベルのスコープ内で使用可能であり、モジュールが実行される前に module.exports に割り当てられます。 exports.name = 'テスト'; console.log(module.exports.name); // テスト module.export.name = 'テスト'; console.log(exports.name); // テスト exports に新しい値が与えられた場合、module.exports にバインドされなくなり、その逆も同様です。 エクスポート = { 名前: 'テスト' }; console.log(module.exports.name, exports.name); // 未定義、テスト module.exports プロパティが新しいオブジェクトに完全に置き換えられる場合は、通常、エクスポートも再割り当てする必要があります。 module.exports = exports = { 名前: 'test' }; console.log(module.exports.name, exports.name) // テスト、テスト モジュールシステムにより分析モジュールの配置を実現以下は require 関数のコード実装です。 // エントリ関数を要求する Module.prototype.require = function(id) { //... 深さ++が必要です。 試す { return Module._load(id, this, /* isMain */ false); // モジュールをロード } finally { 必要な深さ--; } }; 上記のコードは指定されたモジュール パスを受け取ります。ここで、requireDepth はモジュールの読み込みの深さを記録するために使用されます。 Module クラスのメソッド _load は、Node がモジュールをロードする主なロジックを実装します。Module._load 関数のソースコード実装を解析してみましょう。便宜上、テキストにコメントを追加しました。 Module._load = function(リクエスト、親、isMain) { // ステップ 1: モジュールの完全なパスを解決します。const filename = Module._resolveFilename(request, parent, isMain); // ステップ 2: モジュールをロードします。これは 3 つのケースに分かれています。// ケース 1: キャッシュされたモジュールがある場合は、モジュールの exports プロパティを直接返します。const cachedModule = Module._cache[filename]; (cachedModule !== 未定義)の場合 cachedModule.exports を返します。 // ケース 2: 組み込みモジュールの読み込み const mod = loadNativeModule(filename, request); mod が 'canBeRequiredByUsers' の場合、 mod.exports を返します。 // ケース 3: モジュールをビルドする load const module = new Module(filename, parent); // ロード後、モジュールインスタンスをキャッシュします。Module._cache[filename] = module; // ステップ 3: モジュール ファイルをロードします module.load(filename); // ステップ 4: エクスポート オブジェクトを返します。 return module.exports; }; 積載戦略上記のコードには多くの情報が含まれています。主に以下の問題に注目します。 モジュールのキャッシュ戦略は何ですか? 上記のコードを分析すると、_load 関数が次の 3 つの状況に対して異なる読み込み戦略を提供することがわかります。
Module._resolveFilename(request, parent, isMain) はどのようにしてファイル名を解決しますか? 次のように定義されたクラス メソッドを見てみましょう。 Module._resolveFilename = function(request, parent, isMain, options) { NativeModule.canBeRequiredByUsers(リクエスト)の場合 // 組み込みモジュールの読み込みを優先する return request; } パスを設定します。 // ノード require.resolve 関数で使用されるオプション。options.paths は検索パスを指定するために使用されます。if (typeof options === "object" && options !== null) { ArrayIsArray(options.paths) の場合 定数isRelative = リクエストは("./")で始まります || リクエストは("../")で始まります || (isWindows && request.startsWith(".\\")) || リクエストは("..\\")で始まります。 相対的である場合 パス = オプション.パス; } それ以外 { const fakeParent = 新しいモジュール("", null); パス = []; (i = 0 とします; i < options.paths.length; i++) { 定数パス = options.paths[i]; 偽のParent.paths = Module._nodeModulePaths(path); const lookupPaths = Module._resolveLookupPaths(リクエスト、fakeParent); (j = 0; j < lookupPaths.length; j++) の場合 { paths.includes(lookupPaths[j]) が含まれている場合、paths.push(lookupPaths[j]); } } } } そうでない場合 (options.paths === undefined) { パス = Module._resolveLookupPaths(リクエスト、親); } それ以外 { //... } } それ以外 { // モジュールの存在パスを検索します paths = Module._resolveLookupPaths(request, parent); } // 指定されたモジュールとトラバーサル アドレス配列に基づいてモジュール パスを検索し、エントリ モジュールであるかどうかも検索します。const filename = Module._findPath(request, paths, isMain); if (!ファイル名) { 定数requireStack = []; (カーソル = 親; カーソル; カーソル = カーソル.parent) { requireStack.push(cursor.filename || cursor.id); } // モジュールが見つからないため、例外をスローします (これはよくあるエラーでしょうか?) let message = `モジュール '${request}' が見つかりません`; (requireStack.length > 0)の場合{ message = message + "\nRequire スタック:\n- " + requireStack.join("\n- "); } const err = new Error(メッセージ); err.code = "モジュールが見つかりません"; err.requireStack = requireStack; エラーをスローします。 } //最後にファイル名を含む完全なパスを返します return filename; }; 上記のコードの最も顕著な特徴は、_resolveLookupPaths メソッドと _findPath メソッドの使用です。 _resolveLookupPaths: モジュール名とモジュール呼び出し元を受け入れることで、_findPath によって使用されるトラバーサル スコープの配列を返します。 // モジュールファイルのアドレス指定アドレス配列メソッド Module._resolveLookupPaths = function(request, parent) { NativeModule.canBeRequiredByUsers(リクエスト)の場合 debug("[] 内の %j を探しています", request); null を返します。 } // 相対パスでない場合は( リクエスト.charAt(0) !== "." || (リクエストの長さ > 1 && リクエスト.charAt(1) !== "." && リクエスト.charAt(1) !== "/" && (!isWindows || request.charAt(1) !== "\\")) ){ /** * node_modules フォルダを確認します * modulePaths はユーザー ディレクトリ、node_path 環境変数はディレクトリ、グローバル ノードのインストール ディレクトリを指定します */ パスを modulePaths とします。 親が null ではない場合、parent.paths と parent.paths.length が適用されます。 // 親モジュールの modulePath も子モジュールの modulePath に追加し、遡って paths = parent.paths.concat(paths); を見つける必要があります。 } paths.length > 0 ? paths : null を返します。 } // replインタラクションを使用する場合は、./ ./node_modulesとmodulePathsを順番に検索します if (!親 || !親ID || !親ファイル名) { const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths); mainPaths を返します。 } // 相対パス導入の場合は、検索パスに親フォルダのパスを追加します。const parentDir = [path.dirname(parent.filename)]; 親ディレクトリを返します。 }; _findPath: 対象モジュールと上記の関数で見つかった範囲に基づいて、対応するファイル名を見つけて返します。 // 指定されたモジュールとトラバーサルアドレス配列に基づいてモジュールの実際のパスを検索し、それがトップレベルモジュールであるかどうかも調べます。Module._findPath = function(request, paths, isMain) { 定数 absoluteRequest = path.isAbsolute(リクエスト); if (絶対リクエスト) { // 絶対パス、特定のモジュールを直接検索します paths = [""]; } そうでない場合 (!paths || paths.length === 0) { false を返します。 } 定数キャッシュキー = リクエスト + "\x00" + (paths.length === 1 ? paths[0] : paths.join("\x00")); // キャッシュパス const entry = Module._pathCache[cacheKey]; if (entry) は entry を返します。 拡張子を出力します。 末尾にスラッシュを入れる = リクエストの長さ > 0 && request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; // '/' if (!末尾のスラッシュ) { 末尾のスラッシュ = /(?:^|\/)\.?\.$/.test(request); } // 各パスについて (i = 0 とします; i < paths.length; i++) { 定数curPath = パス[i]; curPath && stat(curPath) < 1 の場合、続行します。 const basePath = resolveExports(curPath, request, absoluteRequest); ファイル名を入力します。 定数rc = stat(basePath); if (!末尾のスラッシュ) { if (rc === 0) { // stat ステータスが 0 を返す場合、それはファイルです // File. もし (!isMain) { if (preserveSymlinks) { // モジュールを解決してキャッシュするときにシンボリック リンクを維持するようにモジュール ローダーに指示します。 ファイル名 = path.resolve(basePath); } それ以外 { // シンボリックリンクを保持しない filename = toRealPath(basePath); } } そうでない場合 (preserveSymlinksMain) { ファイル名 = path.resolve(basePath); } それ以外 { ファイル名 = toRealPath(basePath); } } if (!ファイル名) { exts === undefined の場合、 exts = ObjectKeys(Module._extensions); // サフィックスを解析します。filename = tryExtensions(basePath, exts, isMain); } } if (!ファイル名 && rc === 1) { /** * stat が 1 を返し、ファイル名が存在しない場合は、フォルダーとみなされます * ファイルサフィックスが存在しない場合は、ディレクトリの下の package.json のメインエントリで指定されたファイルの読み込みを試みます * 存在しない場合は、index[.js、.node、.json] ファイルを試します */ exts === undefined の場合、 exts = ObjectKeys(Module._extensions); ファイル名 = tryPackage(basePath, exts, isMain, request); } if (filename) { // ファイルが存在する場合は、ファイル名をキャッシュに追加します。 Module._pathCache[cacheKey] = filename; ファイル名を返します。 } } const selfFilename = trySelf(paths, exts, isMain, trailingSlash, request); if (自己ファイル名) { // パスキャッシュを設定します。Module._pathCache[cacheKey] = selfFilename; selfFilename を返します。 } false を返します。 }; モジュールの読み込み標準モジュール処理 上記のコードを読むと、モジュールがフォルダーの場合、tryPackage 関数のロジックが実行されることがわかります。以下は、具体的な実装の簡単な分析です。 // 標準モジュールのロードを試みる function tryPackage(requestPath, exts, isMain, originalPath) { const pkg = readPackageMain(リクエストパス); もし(!pkg){ // package.json が存在しない場合は、index がデフォルトのエントリ ファイルとして使用されます。 return tryExtensions(path.resolve(requestPath, "index"), exts, isMain); } 定数ファイル名 = path.resolve(requestPath, pkg); 実際 = tryFile(ファイル名、isMain) || tryExtensions(ファイル名、拡張子、isMain) || tryExtensions(path.resolve(ファイル名、"index")、exts、isMain); //... 実際の値を返します。 } // package.jsonのメインフィールドを読み取る function readPackageMain(requestPath) { const pkg = readPackage(リクエストパス); pkg を返します ? pkg.main : undefined; } readPackage 関数は、以下に示すように、package.json ファイルのコンテンツを読み取って解析する役割を担います。 関数 readPackage(requestPath) { 定数 jsonPath = path.resolve(requestPath, "package.json"); 定数 existing = packageJsonCache.get(jsonPath); if (existing !== undefined) は existing を返します。 // libuv uv_fs_open 実行ロジックを呼び出し、package.json ファイルを読み取り、キャッシュします。const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath)); json === 未定義の場合{ // 次にファイルをキャッシュします packageJsonCache.set(jsonPath, false); false を返します。 } //... 試す { 定数解析 = JSONParse(json); 定数フィルタリング = { 名前: 解析された名前、 メイン: 解析済み.main、 エクスポート: parsed.exports、 タイプ: 解析されたタイプ }; packageJsonCache.set(jsonPath, フィルター); フィルタリングされた結果を返します。 } キャッチ (e) { //... } } 上記の 2 つのコード スニペットは、package.json ファイルの役割、モジュールの構成エントリ (package.json のメイン フィールド)、およびモジュールのデフォルト ファイルが index である理由を完璧に説明しています。具体的なプロセスを次の図に示します。 モジュールファイル処理対応するモジュールを見つけたら、それをどのようにロードして解析しますか?具体的なコード分析は次のとおりです。 Module.prototype.load = function(ファイル名) { // モジュールがロードされていないことを確認します assert(!this.loaded); this.filename = ファイル名; // 現在のフォルダの node_modules を見つける this.paths = Module._nodeModulePaths(path.dirname(ファイル名)); const extension = findLongestRegisteredExtension(ファイル名); //... // js / json / node などの特定のファイル拡張子解析関数を実行します Module._extensions[拡張子](this, ファイル名); // モジュールが正常にロードされたことを示します this.loaded = true; // ... esm モジュールのサポートを省略 }; サフィックス処理Node.js は、ファイル サフィックスによって読み込み方が異なることがわかります。以下は、.js、.json、.node の簡単な分析です。 .js サフィックスを持つ js ファイルの読み取りは、主に Node の組み込み API fs.readFileSync を通じて実装されます。 Module._extensions[".js"] = function(モジュール、ファイル名) { // ファイルの内容を読み取る const content = fs.readFileSync(filename, "utf8"); // コードをコンパイルして実行します module._compile(content, filename); }; json サフィックスを持つ JSON ファイルの処理ロジックは比較的単純です。ファイルの内容を読み取った後、JSONParse を実行して結果を取得します。 Module._extensions[".json"] = function(モジュール、ファイル名) { // ファイルを UTF-8 形式で直接読み込みます。const content = fs.readFileSync(filename, "utf8"); //... 試す { // ファイルの内容を JSON オブジェクト形式でエクスポートします。 module.exports = JSONParse(stripBOM(content)); } キャッチ (エラー) { //... } }; 拡張子が .node の .node ファイルは、C/C++ で実装されたネイティブ モジュールであり、process.dlopen 関数によって読み取られます。process.dlopen 関数は実際には C++ コード内の DLOpen 関数を呼び出し、DLOpen は uv_dlopen を呼び出します。これにより、OS がシステム ライブラリ ファイルをロードするのと同様に、.node ファイルがロードされます。 Module._extensions[".node"] = function(モジュール、ファイル名) { //... process.dlopen(module, path.toNamespacedPath(filename)) を返します。 }; 上記の 3 つのソース コードから、最終的に JS サフィックスのみがインスタンス メソッド _compile を実行することがわかります。いくつかの実験的な機能とデバッグ関連のロジックを削除して、このコードを簡単に分析してみましょう。 コンパイルして実行するモジュールがロードされた後、Node は V8 エンジンによって提供されるメソッドを使用してサンドボックスを構築および実行し、関数コードを実行します。コードは次のとおりです。 Module.prototype._compile = function(コンテンツ、ファイル名) { モジュールURLを設定します。 リダイレクトします。 // モジュールにパブリック変数 __dirname / __filename / module / exports / require を挿入し、関数をコンパイルします。constcompiledWrapper = wrapSafe(filename, content, this); const dirname = path.dirname(ファイル名); const require = makeRequireFunction(this, リダイレクト); 結果を出す; const エクスポート = this.exports; const thisValue = エクスポート; const モジュール = this; requireDepth === 0 の場合、 statCache = new Map(); //... // モジュール内の関数を実行する result =compiledWrapper.call( この値、 輸出、 必要とする、 モジュール、 ファイル名、 ディレクトリ名 ); すべてのユーザーCJSModuleがロード済み = true; requireDepth === 0 の場合、 statCache = null; 結果を返します。 }; //変数注入のコアロジック function wrapSafe(filename, content, cjsModuleInstance) { if (パッチ適用) { const ラッパー = Module.wrap(コンテンツ); // vm サンドボックスが実行され、実行結果が直接返されます。env->SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext); vm.runInThisContext(ラッパー、{ ファイル名、 ラインオフセット: 0, 表示エラー: true、 // 動的に読み込む importModuleDynamically: async specifier => { 定数ローダー = asyncESM.ESMLoader; loader.import(specifier, normalizeReferrerURL(filename)) を返します。 } }); } コンパイルします。 試す { コンパイル済み = コンパイル関数( コンテンツ、 ファイル名、 0, 0, 未定義、 間違い、 未定義、 [], ["エクスポート"、"必須"、"モジュール"、"__ファイル名"、"__ディレクトリ名"] ); } キャッチ (エラー) { //... } const { callbackMap } = internalBinding("module_wrap"); callbackMap.set(compiled.cacheKey, { importModuleDynamically: 非同期指定子 => { 定数ローダー = asyncESM.ESMLoader; loader.import(specifier, normalizeReferrerURL(filename)) を返します。 } }); コンパイルされた関数を返します。 } 上記のコードでは、_compile 関数内で wrapwrapSafe 関数が呼び出され、__dirname / __filename / module / exports / require パブリック変数の挿入が実行され、C++ runInThisContext メソッド (src/node_contextify.cc ファイル内) が呼び出されてモジュール コードを実行するためのサンドボックス環境が構築され、compiledWrapper オブジェクトが返されることがわかります。最後に、compiledWrapper.call メソッドを通じてモジュールが実行されます。 上記は、nodejsモジュールシステムのソースコード分析の詳細な内容です。nodejsモジュールシステムのソースコード分析の詳細については、123WORDPRESS.COMの他の関連記事に注目してください。 以下もご興味があるかもしれません:
|
<<: MySQL Bツリーインデックスとインデックス最適化の概要についての簡単な説明
>>: Kubernetes オブジェクトボリュームの詳細な使用方法
デザイナーは独自のフォント ライブラリを持っているため、プロジェクトの設計時にすぐに使用できます。今...
この記事では、複数の画像を切り替えるJavaScriptの具体的なコードを参考までに紹介します。具体...
環境: 1. CentOS6.5 X64 2.mysql-5.6.34-linux-glibc2.5...
1. DOCTYPE は必須です。ブラウザは宣言した DOCTYPE に基づいてページのレンダリング...
MySQL データベースを使用する際、何らかの理由で長期間 MySQL にログインしていない場合、ま...
データのバックアップ操作は非常に簡単です。次のコマンドを実行します。 docker run --vo...
@ ルールは、CSS の実行または動作に関する指示を提供する宣言です。各宣言は @ で始まり、その...
この記事では、ネストされたタブ機能を実装するためのjQueryの具体的なコードを参考までに紹介します...
目次序文1. トリガーの概要2. トリガーの作成2.1 トリガー構文の作成2.2 コード例3. トリ...
1. インストール前の準備: 1.1 JDKをインストールするopenjdkをアンインストールする...
<br />適度に画像を追加すると、Web ページがより美しくなります。 画像タグ &l...
マシンに MySQL バージョン 5.0 がすでに存在する場合は、最新バージョンの MySQL のイ...
Windows で Nginx を使用するには、Nginx サービスの起動、停止、Nginx のリロ...
現在、Redis とコンテナについて学習中なので、Docker を使用して Redis マスタースレ...
Mysql-connector-java ドライバのバージョンの問題私のデータベースのバージョンは ...