フロントエンド技術の探求 - NodejsのCommonJS仕様実装原理 | JD Logistics Technology Team

Node.js について学ぶ

Node.js は、ChromeV8 エンジンに基づく JavaScript 実行環境です。イベント駆動型のノンブロッキング I/O モデルを使用して、JavaScript をサーバー側で実行できるようにします。これにより、JavaScript を PHP、Python、 Perl、Ruby など。端末言語と同等のスクリプト言語。Node にはさまざまな機能を提供するために多くの組み込みモジュールが追加されており、多くのサードパーティ モジュールも提供されています。

モジュールの問題

なぜモジュールがあるのでしょうか?

複雑なフロントエンド プロジェクトは、機能、ビジネス、コンポーネントに応じて階層化し、モジュールに分割する必要があります。モジュール型プロジェクトには、少なくとも次の利点があります。

  1. 単体テストを容易にする
  2. 同僚間のコラボレーションを促進する
  3. パブリックメソッドを抽出すると開発が高速化されます
  4. ロードオンデマンド、優れたパフォーマンス
  5. 高い凝集性と低い結合性
  6. 変数の競合を防ぐ
  7. コードプロジェクトのメンテナンスを容易にする

いくつかのモジュール仕様

  • CMD (SeaJS は CMD を実装します)
  • AMD (RequireJS は AMD を実装します)
  • UMD (AMD と CMD の両方をサポート)
  • IIFE(自己実行機能)
  • CommonJS (ノードは CommonJS を使用します)
  • ESモジュール仕様書(JS公式モジュール化ソリューション)

ノード内のモジュール

NodeにCommonJS仕様を採用

実装原則:

ノードはファイルを読み取り、モジュール化を実装するためのコンテンツを取得し、Require メソッドを使用してそれを同期的に参照します。

ヒント: Node 内のすべての js ファイルはモジュールであり、すべてのファイルはモジュールです。

ノード内のモジュールの種類

  1. 組み込みモジュールはコア モジュールであり、インストールする必要はありません。プロジェクト内での相対パス参照は必要ありません。ノード自体がそれらを提供します。
  2. ファイル モジュール。プログラマ自身が作成した js ファイル モジュール。
  3. サードパーティのモジュールをインストールする必要がありますが、インストール後にパスを追加する必要はありません。

Nodeの組み込みモジュール

fs ファイルシステム

ファイルを操作するにはこのモジュールが必要です

const path = require('path'); // 处理路径
const fs = require('fs'); // file system
// // 同步读取
let content = fs.readFileSync(path.resolve(__dirname, 'test.js'), 'utf8');
console.log(content);

let exists = fs.existsSync(path.resolve(__dirname, 'test1.js'));
console.log(exists);

パス パス処理

const path = require('path'); // 处理路径


// join / resolve 用的时候可以混用

console.log(path.join('a', 'b', 'c', '..', '/'))

// 根据已经有的路径来解析绝对路径, 可以用他来解析配置文件
console.log(path.resolve('a', 'b', '/')); // resolve 不支持/ 会解析成根路径

console.log(path.join(__dirname, 'a'))
console.log(path.extname('1.js'))
console.log(path.dirname(__dirname)); // 解析父目录

vm はコードを実行します

文字列を実行用の JS に変換するにはどうすればよいでしょうか?

1.評価

eval のコードが実行されるときのスコープは現在のスコープです。関数内のローカル変数にアクセスできます。

let test = 'global scope'
global.test1 = '123'
function b(){
  test = 'fn scope'
  eval('console.log(test)'); //local scope
  new Function('console.log(test1)')() // 123
  new Function('console.log(test)')() //global scope
}
b()

2.新機能

new Function() が関数を作成するとき、現在のレキシカル環境ではなくグローバル環境が参照されます。Function の式で使用される変数は、パラメーターまたはグローバル値で渡されます。

関数はグローバル変数を取得できるため、変数汚染が依然として存在する可能性があります。

function getFn() {
  let value = "test"
  let fn = new Function('console.log(value)')
  return fn
}

getFn()()

global.a = 100 // 挂在到全局对象global上
new Function("console.log(a)")() // 100

3.vm

前の 2 つの方法では、変数の汚染という 1 つの概念を常に強調してきました。

VMは環境に影響されないのが特徴で、サンドボックス環境とも言えます

Node では、グローバル変数は複数のモジュール間で共有されるため、グローバルにプロパティを定義しないようにしてください。

したがって、 vm.runInThisContext はglobal のグローバル変数にアクセスできますが、カスタム変数にはアクセスできません。vm.runInNewContext はグローバル変数またはカスタム変数にアクセスできません。新しい実行コンテキストに存在します

const vm = require('vm')
global.a = 1
// vm.runInThisContext("console.log(a)")
vm.runInThisContext("a = 100") // 沙箱,独立的环境
console.log(a) // 1
vm.runInNewContext('console.log(a)')
console.log(a) // a is not defined

ノードのモジュール実装

ノードには独自のモジュール化メカニズムがあり、各ファイルは個別のモジュールであり、CommonJS 仕様に従っています。つまり、require を使用してモジュールをインポートし、module.export を介してモジュールをエクスポートします。

ノード モジュールの実行メカニズムも非常にシンプルです。実際、各モジュールは関数の層でラップされています。関数のラップにより、コード間のスコープ分離を実現できます。

まず引数をjsファイルに直接出力すると、結果は下図のようになりますので、まずはこれらのパラメータを覚えておきましょう。

console.log(arguments) // exports, require, module, __filename, __dirname

Node では、modules.export を通じてエクスポートされ、require によって導入されます。このうち、requireはnodeのfsモジュールに依存してモジュールファイルをロードしており、fs.readFileで読み込まれるのは文字列です。

javascrpt では、eval または new Function を使用して文字列を実行用の JS コードに変換できます。しかし、前述したように、それらはすべて、変動する汚染という致命的な問題を抱えています

必要なモジュールローダーを実装する

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

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);
    // 返回exports对象
    return module.exports;
}

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

// 定义模块, 添加文件id标识和exports属性
function Module(id) {
    this.id = id;
    // 读取到的文件内容会放在exports中
    this.exports = {};
}

ノード モジュールは関数内で実行されます。ここでは、静的属性ラッパーがモジュールにマウントされ、この関数の文字列を定義します。ラッパーは配列です。配列の最初の要素は関数のパラメーター部分であり、これには以下が含まれますimports、module、Require、__dirname、__filename はすべて、モジュールで一般的に使用されるグローバル変数です。

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

// 定义包裹模块内容的函数
Module.wrapper = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

_extensions は、モジュール拡張機能ごとに異なる読み込みメソッドを使用するために使用されます。たとえば、JSON と JavaScript の読み込みメソッドは明らかに異なります。JSON は JSON.parse を使用して実行します。

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

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

// 定义扩展名,不同的扩展名,加载方式不同,实现js和json
Module._extensions = {
    '.js'(module) {
        const content = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
        const fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
    },
    '.json'(module) {
        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) を使用してファイルの絶対パスを取得します。次に、新しい Module をインスタンス化してモジュール オブジェクトを作成し、モジュールの id 属性にモジュールの絶対パスを格納し、モジュール内に json オブジェクトとしてエクスポート属性を作成します。

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

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

次に、モジュールにキャッシュを追加します。つまり、ファイルがロードされると、ファイルはキャッシュに置かれます。モジュールをロードするときは、まずキャッシュに存在するかどうかを確認します。存在する場合は、それを直接使用します。存在しない場合は、再ロードしてから、ロード後にキャッシュに入れます。

// 定义导入类,参数为模块路径
function Require(modulePath) {
  // 获取当前要加载的绝对路径
  let absPathname = path.resolve(__dirname, modulePath);
  // 从缓存中读取,如果存在,直接返回结果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 创建模块,新建Module实例
  const module = new Module(absPathname);
  // 添加缓存
  Module._cache[absPathname] = module;
  // 加载当前模块
  tryModuleLoad(module);
  // 返回exports对象
  return module.exports;
}

追加機能: モジュールのサフィックス名を省略します。

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

// 定义导入类,参数为模块路径
function Require(modulePath) {
  // 获取当前要加载的绝对路径
  let absPathname = path.resolve(__dirname, modulePath);
  // 获取所有后缀名
  const extNames = Object.keys(Module._extensions);
  let index = 0;

  // 存储原始文件路径
  const oldPath = absPathname;
  function findExt(absPathname) {
      if (index === extNames.length) {
         return throw new Error('文件不存在');
      }
      try {
          fs.accessSync(absPathname);
          return absPathname;
      } catch(e) {
          const ext = extNames[index++];
          findExt(oldPath + ext);
      }
  }
  
  // 递归追加后缀名,判断文件是否存在
  absPathname = findExt(absPathname);
  // 从缓存中读取,如果存在,直接返回结果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 创建模块,新建Module实例
  const module = new Module(absPathname);
  // 添加缓存
  Module._cache[absPathname] = module;
  // 加载当前模块
  tryModuleLoad(module);
  // 返回exports对象
  return module.exports;
}

ソースコードのデバッグ

VSCode を通じて Node.js をデバッグできます

ステップ

ファイルa.jsを作成する

module.exports = 'abc'

1.test.jsファイル

let r = require('./a')

console.log(r)

1. デバッグの構成は基本的に .vscode/launch.json ファイルを構成することであり、このファイルの本質は複数の起動コマンド エントリ オプションを提供することです。

いくつかの一般的なパラメータは次のとおりです。

  • プログラムは起動ファイル (つまり、エントリ ファイル) のパスを制御します。
  • 名前ドロップダウン メニューに表示される名前 (このコマンドに対応するエントリ名)
  • リクエストは起動とアタッチに分かれています(プロセスは開始されています)
  • SkipFiles は、シングルステップ デバッグ中にスキップするコードを指定します
  • runtimeExecutable は、ランタイム実行可能ファイルを設定します。デフォルトは、node です。nodemon、ts-node、npm などに設定できます。

launch.jsonを変更し skipFiles でシングルステップ デバッグでスキップされるコードを指定します

  1. test.js ファイルの require メソッドが配置されている行の前にブレーク ポイントを置きます。
  2. デバッグを実行し、ソースコード関連の入力方法を入力します。

コードのステップを整理する

1. まず、require メソッドを入力します: Module.prototype.require

2. module.exports を返す Module._load メソッドをデバッグします。Module._resolveFilename メソッドは、処理後にファイル アドレスを返し、ファイルを絶対アドレスに変更し、ファイルにサフィックスがない場合はファイル サフィックスを追加します。

3. ここで Module クラスを定義します。id はファイル名です。このクラスではexports属性が定義されています

4. 次に、ストラテジー モードを使用する module.load メソッドをデバッグします。Module._extensions[extension](this, filename) は、渡されたさまざまなファイル サフィックス名に応じてさまざまなメソッドを呼び出します。

5. メソッドを入力し、コア コードを確認し、受信ファイル アドレス パラメーターを読み取り、ファイル内の文字列コンテンツを取得し、module._compile を実行します。

6. このメソッド内でwrapSafeメソッドが実行されます。文字列の前後に関数サフィックスを追加し、Node の vm モジュールの runInthisContext メソッドを使用して文字列を実行します。これにより、受信ファイルの console.log コード行の内容が直接実行されます。

この時点で、require メソッドを Node 全体に実装するためのプロセス コード全体がデバッグされています。ソース コードをデバッグすることで、実装アイデア、コード スタイル、仕様を学習し、ツール ライブラリの実装や改善に役立ちます。コードのアイデアと同時に、関連する原則も理解しており、日常の開発作業で遭遇する問題の解決にも役立ちます。

著者: JD Logistics Qiao Panpan

出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください

 

OpenAI が ChatGPT Voice Vite 5 をすべてのユーザーに無料で公開、正式にリリース オペレーターの魔法の操作: バックグラウンドでネットワークを切断、ブロードバンド アカウントを非アクティブ化、ユーザーに光モデムの変更を強制 Microsoft オープン ソースの ターミナル チャット プログラマーが ETC 残高を改ざんし、年間 260 万元以上を横領 Redis の父が使用する Pure C 言語コードは、Telegram Bot フレームワークを実装しています あなたがオープンソース プロジェクトのメンテナである場合、この種の返答にどこまで耐えることができますか? Microsoft Copilot Web AI は 12 月 1 日に正式にリリースされ、中国の OpenAI をサポートします 元 CEO 兼社長の Sam Altman 氏と Greg Brockman 氏が Microsoft に加わりました Broadcom は VMware の買収に成功したと発表しました
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10150940