require loaderの実装原理の深い理解

require loaderの実装原理の深い理解

序文

Node は新しいプログラミング言語ではなく、JavaScript のランタイムに過ぎないとよく言われます。ランタイムとは、JavaScript を実行するための環境と簡単に理解できます。ほとんどの場合、JavaScript はブラウザで実行されます。Node.js の登場により、Node.js で JavaScript を実行できるようになりました。つまり、Node.js またはブラウザがインストールされている場所であればどこでも、そこで JavaScript を実行できるのです。

1. ノードモジュール化の実装

Node には独自のモジュール メカニズムがあります。各ファイルは個別のモジュールであり、CommonJS 仕様に従います。つまり、require を使用してモジュールをインポートし、module.export を通じてモジュールをエクスポートします。
ノードモジュールの動作メカニズムも非常にシンプルです。実際には、各モジュールの外側に関数のレイヤーをラップするだけです。関数のラッピングにより、コード間のスコープ分離を実現できます。

コードを書いたときに関数をラップしなかったと言うかもしれません。はい、その通りです。この関数のレイヤーは、Node によって自動的に実装されます。テストしてみましょう。

新しい js ファイルを作成し、最初の行に存在しない変数を出力します。たとえば、ここでは window を出力しますが、node には window がありません。

コンソールログ(ウィンドウ);

ノードを介してファイルを実行すると、次のようなエラー メッセージが表示されます。 (コマンドを実行するには、システムのデフォルトの cmd を使用してください)。

(関数 (exports、require、module、__filename、__dirname) { console.log(window);
参照エラー: ウィンドウが定義されていません
    Object.<anonymous> (/Users/choice/Desktop/node/main.js:1:75) で
    Module._compile (internal/modules/cjs/loader.js:689:30) で
    Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10) で
    Module.load (internal/modules/cjs/loader.js:599:32) で
    tryModuleLoad (internal/modules/cjs/loader.js:538:12) で
    Function.Module._load (internal/modules/cjs/loader.js:530:3) で
    Function.Module.runMain (internal/modules/cjs/loader.js:742:12) で
    起動時 (internal/bootstrap/node.js:279:19)
    bootstrapNodeJSCore (internal/bootstrap/node.js:752:3) で

エラー レポートの最上位レベルに自己実行関数があり、その中に exports、require、module、__filename、__dirname などのよく使用されるグローバル変数が含まれていることがわかります。

これについては、前回の記事「フロントエンドのモジュール化の開発の歴史」で紹介しました。自己実行関数も、フロントエンドのモジュール化を実装するためのソリューションの 1 つです。フロントエンドにモジュール システムが存在しなかった初期の頃は、自己実行関数によって名前空間の問題をうまく解決でき、モジュールが依存する他のモジュールをパラメーターを通じて渡すことができました。 cmd および amd 仕様も自己実行関数に依存しています。

モジュール システムでは、各ファイルがモジュールです。各モジュールには、その外部に自動的に関数があり、エクスポート メソッド module.exports または exports、およびインポート メソッド require が定義されています。

moduleA = (function() {
    module.exports = プロミス;
    module.exports を返します。
})();

2.ロードモジュールが必要

require はモジュール ファイルをロードするために node 内の fs モジュールに依存し、 fs.readFile は文字列を読み取ります。

JavaScript では、eval または new 関数を使用して、文字列を js コードに変換して実行できます。

評価

定数名 = 'yd';
const str = 'const a = 123; console.log(name)';
eval(str); // yd;

新しい機能

新しい関数は、実行する文字列を受け取り、新しい関数を返します。この新しい関数文字列を呼び出すと、それが実行されます。この関数がパラメータを渡す必要がある場合は、新しい関数を作成するときにパラメータを 1 つずつ渡し、最後に実行する文字列を渡すことができます。たとえば、ここでは、実行する文字列 str であるパラメーター b を渡します。

定数b = 3;
const str = 'let a = 1; return a + b';
const fun = 新しい Function('b', str);
console.log(fun(b, str)); // 4

eval と Function のインスタンス化の両方を使用して JavaScript 文字列を実行できることがわかり、どちらも require モジュールの読み込みを実装できるようです。しかし、Node でモジュール性を実装するためには、これらが選択されませんでした。その理由は非常に単純で、これらにはすべて、それらに属していない変数の影響を受けやすいという致命的な問題があるからです。

以下のように、str 文字列では a が定義されていませんが、上で定義した a 変数は使用できます。これは明らかに誤りです。モジュラー機構では、str 文字列は独自の独立した実行空間を持つ必要があり、それ自体に存在しない変数は直接使用できません。

定数a = 1;

'console.log(a)' を str に代入します。

評価(文字列);

const func = 新しい Function(str);
関数();

Node には VM 仮想環境の概念があり、追加の JS ファイルを実行するために使用されます。これにより、JavaScript 実行の独立性が確保され、外部要因の影響を受けません。

vm 組み込みモジュール

helloを外部で定義しましたが、strは独立したモジュールであり、village hello変数には含まれていないため、エラーが直接報告されます。

// vm モジュールをインポートします。インストールする必要はありません。ノードで独自に構築されたモジュールです。const vm = require('vm');
定数hello = 'yd';
'console.log(hello)' を str に代入します。
wm.runInThisContext(str); // エラー

したがって、ノードは vm を使用して JavaScript モジュールを実行できます。これにより、モジュールの独立性が保証されます。

3.コード実装が必要

必要なコード実装を紹介する前に、以下で使用する 2 つのノード モジュールの使用方法を確認しましょう。

パスモジュール

ファイル パスを処理するために使用されます。

basename: ベースパス。ファイルパスがある場合はベースパスではありません。ベースパスは 1.js です。

extname: 拡張機能名を取得する

dirname: 親ディレクトリ

join: パスを連結する

解決: 現在のフォルダの絶対パス。使用時に末尾に / を追加しないように注意してください。

__dirname: 現在のファイルが保存されているフォルダのパス

__filename: 現在のファイルの絶対パス

定数 path = require('path', 's');
console.log(path.basename('1.js'));
console.log(path.extname('2.txt'));
console.log(path.dirname('2.txt'));
console.log(path.join('a/b/c', 'd/e/f')); // a/b/c/d/e/
console.log(path.resolve('2.txt'));

fsモジュール

読み取り、書き込み、追加、削除など、ファイルやフォルダーを操作するために使用されます。よく使用されるメソッドは、それぞれ非同期ファイル読み取りと同期ファイル読み取りである readFile と readFileSync です。

定数 fs = require('fs');
const buffer = fs.readFileSync('./name.txt', 'utf8'); // エンコーディングが渡されない場合は、バイナリが出力されます console.log(buffer);

fs.access: ファイルが存在するかどうかを判断します。node10 で提供される exists メソッドは、ノード仕様に準拠していないため非推奨になりました。そのため、ファイルが存在するかどうかを判断するには access を使用します。

試す {
    fs.accessSync('./name.txt');
} キャッチ(e) {
    // ファイルが存在しません}

4. 必要なモジュールローダーを手動で実装する

まず、依存モジュール パス fs、vm をインポートし、インポートするファイル パスを示す modulePath パラメーターを受け取る Require 関数を作成します。

// インポート依存関係 const path = require('path'); // パス操作 const fs = require('fs'); // ファイルの読み取り const vm = require('vm'); // ファイル実行 // インポートクラスを定義します。パラメータはモジュールパスです。 function Require(modulePath) {
    ...
}

Require でモジュールの絶対パスを取得します。これは fs を使用してモジュールをロードするのに便利です。ここでは、モジュール コンテンツの読み取りを抽象化するために new Module を使用し、モジュール コンテンツをロードするために tryModuleLoad を使用します。Module と tryModuleLoad は後で実装します。Require の戻り値はモジュールのコンテンツ、つまり module.exports である必要があります。

// インポートクラスを定義します。パラメータはモジュールパスです。function Require(modulePath) {
    // ロードする絶対パスを取得します。let absPathname = path.resolve(__dirname, modulePath);
    // モジュールを作成し、新しい Module インスタンスを作成します。const module = new Module(absPathname);
    // 現在のモジュールをロードします tryModuleLoad(module);
    // エクスポート オブジェクトを返します return module.exports;
}

モジュールの実装は非常にシンプルで、モジュールのエクスポート オブジェクトを作成するだけです。tryModuleLoad が実行されると、コンテンツがエクスポートに追加されます。id はモジュールの絶対パスです。

// モジュールを定義し、ファイルIDとエクスポート属性を追加します。function Module(id) {
    id は、
    // 読み取られたファイルの内容はエクスポートに配置されます。this.exports = {};
}

ノード モジュールは関数内で実行されることを前に説明しました。ここでは、この関数の文字列を定義する静的プロパティ ラッパーをモジュールにマウントします。ラッパーは配列であり、配列の最初の要素は関数のパラメーター部分で、エクスポート、モジュール、Require、__dirname、__filename などが含まれます。これらはすべて、モジュールでよく使用されるグローバル変数です。ここで渡される Require パラメータは、私たち自身が定義した Require であることに注意してください。

2 番目のパラメータは関数の終わりです。どちらの部分も文字列です。これらを使用するときは、モジュール文字列の外側にラップするだけです。

モジュール.ラッパー = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

_extensions は、異なるモジュール拡張機能に対して異なる読み込み方法を使用するために使用されます。たとえば、JSON と JavaScript の読み込み方法は明らかに異なります。 JSON は JSON.parse を使用して解析されます。

JavaScript は vm.runInThisContext を使用して実行されます。fs.readFileSync が module.id を渡していることがわかります。これは、モジュール定義に格納されている id がモジュールの絶対パスであることを意味します。読み取られるコンテンツは文字列です。これをラップするために Module.wrapper を使用します。これは、このモジュールの外部で関数をラップすることと同等であり、プライベート スコープを実現します。

call を使用して fn 関数を実行します。最初のパラメータは、実行中の this を変更します。module.exports を渡します。次のパラメータは、関数の外側にラップされたパラメータ、exports、module、Require、__dirname、__filename です。

モジュール._extensions = {
    '.js'(モジュール) {
        定数コンテンツ = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + コンテンツ + Module.wrapper[1];
        定数 fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
    },
    '.json'(モジュール) {
        const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // ファイルの結果を exports プロパティに格納します}
}

tryModuleLoad 関数はモジュール オブジェクトを受け取り、path.extname を通じてモジュール サフィックスを取得し、Module._extensions を使用してモジュールをロードします。

//モジュール読み込みメソッドを定義する function tryModuleLoad(module) {
    // 拡張機能名を取得します。const extension = path.extname(module.id);
    // サフィックスで現在のモジュールをロードします。Module._extensions[extension](module);
}

この時点で、Require の読み込みメカニズムの記述は基本的に完了しました。もう一度見てみましょう。 Require がモジュールをロードするときは、モジュール名を渡し、Require メソッドで path.resolve(__dirname, modulePath) を使用してファイルの絶対パスを取得します。次に、新しいモジュールのインスタンス化を通じてモジュール オブジェクトを作成し、モジュールの id 属性にモジュールの絶対パスを格納し、モジュール内に json オブジェクトとして exports 属性を作成します。

モジュールをロードするには、tryModuleLoad メソッドを使用します。tryModuleLoad では、path.extname を使用してファイル拡張子を取得し、拡張子に基づいて対応するモジュール ロード メカニズムを実行します。

ロードされたモジュールは最終的に module.exports にマウントされます。 tryModuleLoad が実行されると、module.exports がすでに存在するので、直接戻ります。

// インポート依存関係 const path = require('path'); // パス操作 const fs = require('fs'); // ファイルの読み取り const vm = require('vm'); // ファイル実行 // インポートクラスを定義します。パラメータはモジュールパスです。 function Require(modulePath) {
    // ロードする絶対パスを取得します。let absPathname = path.resolve(__dirname, modulePath);
    // モジュールを作成し、新しい Module インスタンスを作成します。const module = new Module(absPathname);
    // 現在のモジュールをロードします tryModuleLoad(module);
    // エクスポート オブジェクトを返します return module.exports;
}
// モジュールを定義し、ファイルIDとエクスポート属性を追加します。function Module(id) {
    id は、
    // 読み取られたファイルの内容はエクスポートに配置されます。this.exports = {};
}
// モジュールコンテンツをラップする関数を定義します。Module.wrapper = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]
// 拡張機能名を定義します。拡張機能名によって読み込み方法が異なります。js と json を実装します。
モジュール._extensions = {
    '.js'(モジュール) {
        定数コンテンツ = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + コンテンツ + Module.wrapper[1];
        定数 fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,_filename,_dirname);
    },
    '.json'(モジュール) {
        const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // ファイルの結果を exports プロパティに格納します}
}
//モジュール読み込みメソッドを定義する function tryModuleLoad(module) {
    // 拡張機能名を取得します。const extension = path.extname(module.id);
    // サフィックスで現在のモジュールをロードします。Module._extensions[extension](module);
}

5. モジュールにキャッシュを追加する

キャッシュの追加も比較的簡単です。ファイルをロードするときに、ファイルをキャッシュに入れます。モジュールをロードするときに、キャッシュに存在するかどうかを確認します。存在する場合は、直接使用します。存在しない場合は、再ロードして、ロード後にキャッシュに入れます。

// インポートクラスを定義します。パラメータはモジュールパスです。function Require(modulePath) {
    // ロードする絶対パスを取得します。let absPathname = path.resolve(__dirname, modulePath);
    // キャッシュから読み取り、存在する場合は結果を直接返します if (Module._cache[absPathname]) {
        Module._cache[absPathname].exportsを返します。
    }
    // 現在のモジュールをロードしてみます tryModuleLoad(module);
    // モジュールを作成し、新しい Module インスタンスを作成します。const module = new Module(absPathname);
    // キャッシュを追加します。Module._cache[absPathname] = module;
    // 現在のモジュールをロードします tryModuleLoad(module);
    // エクスポート オブジェクトを返します return module.exports;
}

6. パスを自動的に補完する

モジュールにサフィックスを自動的に追加して、サフィックスなしでモジュールをロードします。実際、ファイルにサフィックスがない場合、すべてのサフィックスを走査してファイルが存在するかどうかを確認します。

// インポートクラスを定義します。パラメータはモジュールパスです。function Require(modulePath) {
    // ロードする絶対パスを取得します。let absPathname = path.resolve(__dirname, modulePath);
    // すべてのサフィックス名を取得します。const extNames = Object.keys(Module._extensions);
    インデックスを 0 にします。
    //元のファイル パスを保存します。const oldPath = absPathname;
    関数 findExt(absPathname) {
        if (インデックス === extNames.length) {
           return throw new Error('ファイルが存在しません');
        }
        試す {
            fs.accessSync(absPathname);
            absPathname を返します。
        } キャッチ(e) {
            定数 ext = extNames[index++];
            findExt(古いパス + ext);
        }
    }
    // ファイルが存在するかどうかを判断するためにサフィックス名を再帰的に追加します。absPathname = findExt(absPathname);
    // キャッシュから読み取り、存在する場合は結果を直接返します if (Module._cache[absPathname]) {
        Module._cache[absPathname].exportsを返します。
    }
    // 現在のモジュールをロードしてみます tryModuleLoad(module);
    // モジュールを作成し、新しい Module インスタンスを作成します。const module = new Module(absPathname);
    // キャッシュを追加します。Module._cache[absPathname] = module;
    // 現在のモジュールをロードします tryModuleLoad(module);
    // エクスポート オブジェクトを返します return module.exports;
}

7. 分析と実装の手順

1. 関連モジュールをインポートし、Require メソッドを作成します。

2. モジュールをロードするために使用される Module._load メソッドを通じて抽出します。

3.Module.resolveFilename は相対パスを絶対パスに変換します。

4. キャッシュモジュール Module._cache は、パフォーマンスを向上させるために同じモジュールを繰り返しロードしません。

5. モジュール ID を作成します。保存される内容は exports = {} であり、これはこれと同等です。

6. tryModuleLoad(module, filename) を使用してモジュールのロードを試みます。

7.Module._extensionsは読み取りファイルを使用します。

8.Module.wrap: 読み込んだ js を関数でラップします。

9. runInThisContext を使用して取得した文字列を実行します。

10. 文字列を実行し、これをエクスポートに適合させます。

要約する

これで、require loader の実装原理に関するこの記事は終了です。require loader の原理の詳細については、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • requireJS の詳細な理解 - シンプルなモジュールローダーの実装

<<:  シェルで文字列内のスペースや指定された文字を削除する方法

>>:  「fsck」を使用して Linux のファイルシステムエラーを修正する方法

推薦する

よく使用される入力テキストボックスの内容は自動的に垂直方向に中央揃えされ、クリックするとデフォルトのプロンプトテキストは空になります。

3つの機能: 1. コンテンツの垂直方向の自動中央揃え2. デフォルトのプロンプトテキストは灰色で表...

Vue3 リストインターフェースデータ表示の詳細

目次1. リストインターフェースの表示例2. データを表示する2.1. コンポーネントがリストに表示...

CSS でのナビゲーション バーとドロップダウン メニューの実装

1. CSSナビゲーションバー(1)ナビゲーションバーの機能ナビゲーション バーを使いこなすことは、...

MySQL パフォーマンスの最適化: インデックスを効率的かつ正しく使用する方法

実践こそが真実をテストする唯一の方法です。この記事では、インデックスの全体的な使用法についてのみ説明...

MySQL のジオメトリ型を使用して経度と緯度の距離の問題を処理する方法

テーブルを作成する テーブル `map` を作成します ( `id` int(11) NULLではな...

ページキャッシュを無効にするいくつかの方法を共有する

本日、開発中に、顧客からページをキャッシュしないように要求される方法に遭遇しました。調べたところ、ペ...

HTML フォームタグチュートリアル (2):

このチュートリアルでは、ウェブデザインにおけるFORMフォームタグのさまざまな属性の応用を紹介します...

適応分析と応答分析の違いを専門用語で詳しく説明

日々の開発経験と関連するオンライン情報に基づいて、アダプティブとレスポンシブの違いをシンプルでわかり...

JavaScript エラー処理 try..catch...finally + は throw+TypeError+RangeError をカバーします

目次1. 目的2. 文法3. 練習する1. 目的通常、エラーが発生すると、スクリプトは直ちに停止し、...

Linuxターミナルでの一般的なMySQL操作コマンドの詳細な説明

仕える: # chkconfig --list すべてのシステム サービスを一覧表示します # ch...

dockerfile-maven-plugin 使用ガイドの概要

目次pom 構成Setting.xml 構成ログインステータスログインが必要ですログインは必要ありま...

WeChatアプレットのサイレントログインとカスタムログイン状態の維持の詳細な説明

目次1. 背景2. サイレントログインとは何ですか? 3. カスタムログイン状態を維持する方法4. ...

WeChatアプレットが弾丸画面を送信するビデオプレーヤーを実装

この記事では、WeChatアプレットでビデオプレーヤーの集中砲火を実装するための具体的なコードを参考...

HTMLフォームアプリケーションにはチェックボックスとラジオボタンの使用が含まれます

チェックボックスやラジオボタンの使用を含むコードをコピーコードは次のとおりです。 <!DOCT...

MySQL内部一時テーブルの具体的な使用法

目次連合テーブルの初期化ステートメントの実行連合の結果ユニオンオールグループ化十分なメモリステートメ...