ホームページ ウェブフロントエンド jsチュートリアル node.js require() ソースコードの解釈_node.js

node.js require() ソースコードの解釈_node.js

May 16, 2016 pm 03:26 PM
require() ソースコードの解釈

2009年,Node.js 專案誕生,所有模組一律為 CommonJS 格式。

時至今日,Node.js 的模組倉庫 npmjs.com ,已經存放了15萬個模組,其中絕大部分都是 CommonJS 格式。

這個格式的核心就是 require 語句,模組透過它載入。學習 Node.js ,必學如何使用 require 語句。本文透過原始碼分析,詳細介紹 require 語句的內部運作機制,幫你理解 Node.js 的模組機制。

一、require() 的基本用法

分析原始碼之前,先介紹 require 語句的內部邏輯。如果你只想了解 require 的用法,只看這段就夠了。

下面的內容翻譯自《Node使用手冊》

複製程式碼 程式碼如下:

當 Node 遇到 require(X) 時,請依照下面的順序處理。

(1)如果 X 是內建模組(如 require('http'))
  a. 返回該模組。
   b. 不再繼續執行。

(2)如果 X 以 "./" 或 "/" 或 "../" 開頭
   a. 根據 X 所在的父模組,確定 X 的絕對路徑。
   b. 將 X 當成文件,依序查找下面文件,只要其中有一個存在,就返回該文件,不再繼續執行。

 X
 X.js
 X.json
 X.node

  c. 將 X 當成目錄,依序查找下面文件,只要其中有一個存在,就返回該文件,不再繼續執行。

 X/package.json(main欄位)
 X/index.js
 X/index.json
 X/index.node

(3)如果 X 不含路徑
   a. 根據 X 所在的父模組,確定 X 可能的安裝目錄。
   b. 依序在每個目錄中,將 X 當作檔案名稱或目錄名稱載入。

(4) 拋出 "not found"

請看一個例子。

目前腳本檔 /home/ry/projects/foo.js 執行了 require('bar') ,這屬於上面的第三種情況。 Node 內部運作過程如下。

首先,確定 x 的絕對路徑可能是下面這些位置,依序搜尋每一個目錄。

複製程式碼 程式碼如下:

/home/ry/projects/node_modules/bar
/home/ry/node_modules/bar
/home/node_modules/bar
/node_modules/bar

搜尋時,Node 先將 bar 當成文件名,依序嘗試載入下面這些文件,只要有一個成功就返回。

bar
bar.js
bar.json
bar.node
ログイン後にコピー

如果都不成功,表示 bar 可能是目錄名,於是依序嘗試載入下面這些檔案。

複製程式碼 程式碼如下:

bar/package.json(main欄位)
bar/index.js
bar/index.json
bar/index.node

如果在所有目錄中,都無法找到 bar 對應的檔案或目錄,就拋出錯誤。

二、Module 建構子

了解內部邏輯以後,下面就來看原始碼。

require 的源碼在 Node 的 lib/module.js 檔案。為了便於理解,本文所引用的源碼是簡化過的,並且刪除了原作者的註解。

function Module(id, parent) {
 this.id = id;
 this.exports = {};
 this.parent = parent;
 this.filename = null;
 this.loaded = false;
 this.children = [];
}

module.exports = Module;

var module = new Module(filename, parent);
ログイン後にコピー

上面程式碼中,Node 定義了一個建構子 Module,所有的模組都是 Module 的實例。可以看到,目前模組(module.js)也是 Module 的一個實例。

每個實例都有自己的屬性。下面透過一個例子,看看這些屬性的值是什麼。新建一個腳本檔 a.js 。

// a.js

console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);
ログイン後にコピー

運行這個腳本。

$ node a.js

module.id: .
module.exports: {}
module.parent: null
module.filename: /home/ruanyf/tmp/a.js
module.loaded: false
module.children: []
module.paths: [ '/home/ruanyf/tmp/node_modules',
 '/home/ruanyf/node_modules',
 '/home/node_modules',
 '/node_modules' ]
ログイン後にコピー

可以看到,如果没有父模块,直接调用当前模块,parent 属性就是 null,id 属性就是一个点。filename 属性是模块的绝对路径,path 属性是一个数组,包含了模块可能的位置。另外,输出这些内容时,模块还没有全部加载,所以 loaded 属性为 false 。

新建另一个脚本文件 b.js,让其调用 a.js 。

// b.js

var a = require('./a.js');
ログイン後にコピー

运行 b.js 。

$ node b.js

module.id: /home/ruanyf/tmp/a.js
module.exports: {}
module.parent: { object }
module.filename: /home/ruanyf/tmp/a.js
module.loaded: false
module.children: []
module.paths: [ '/home/ruanyf/tmp/node_modules',
 '/home/ruanyf/node_modules',
 '/home/node_modules',
 '/node_modules' ]
ログイン後にコピー

上面代码中,由于 a.js 被 b.js 调用,所以 parent 属性指向 b.js 模块,id 属性和 filename 属性一致,都是模块的绝对路径。

三、模块实例的 require 方法

每个模块实例都有一个 require 方法。

Module.prototype.require = function(path) {
 return Module._load(path, this);
};
ログイン後にコピー

由此可知,require 并不是全局性命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用 require 命令(唯一的例外是 REPL 环境)。另外,require 其实内部调用 Module._load 方法。

下面来看 Module._load 的源码。

Module._load = function(request, parent, isMain) {

 // 计算绝对路径
 var filename = Module._resolveFilename(request, parent);

 // 第一步:如果有缓存,取出缓存
 var cachedModule = Module._cache[filename];
 if (cachedModule) {
  return cachedModule.exports;

 // 第二步:是否为内置模块
 if (NativeModule.exists(filename)) {
  return NativeModule.require(filename);
 }

 // 第三步:生成模块实例,存入缓存
 var module = new Module(filename, parent);
 Module._cache[filename] = module;

 // 第四步:加载模块
 try {
  module.load(filename);
  hadException = false;
 } finally {
  if (hadException) {
   delete Module._cache[filename];
  }
 }

 // 第五步:输出模块的exports属性
 return module.exports;
};
ログイン後にコピー

上面代码中,首先解析出模块的绝对路径(filename),以它作为模块的识别符。然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块。

因此,Module._load 的关键步骤是两个。

复制代码 代码如下:

◾Module._resolveFilename() :确定模块的绝对路径
◾module.load():加载模块

四、模块的绝对路径

下面是 Module._resolveFilename 方法的源码。

Module._resolveFilename = function(request, parent) {

 // 第一步:如果是内置模块,不含路径返回
 if (NativeModule.exists(request)) {
  return request;
 }

 // 第二步:确定所有可能的路径
 var resolvedModule = Module._resolveLookupPaths(request, parent);
 var id = resolvedModule[0];
 var paths = resolvedModule[1];

 // 第三步:确定哪一个路径为真
 var filename = Module._findPath(request, paths);
 if (!filename) {
  var err = new Error("Cannot find module '" + request + "'");
  err.code = 'MODULE_NOT_FOUND';
  throw err;
 }
 return filename;
};
ログイン後にコピー

上面代码中,在 Module.resolveFilename 方法内部,又调用了两个方法 Module.resolveLookupPaths() 和 Module._findPath() ,前者用来列出可能的路径,后者用来确认哪一个路径为真。

为了简洁起见,这里只给出 Module._resolveLookupPaths() 的运行结果。

复制代码 代码如下:

[ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules'
'/home/ruanyf/.node_modules',
'/home/ruanyf/.node_libraries',
'$Prefix/lib/node' ]

上面的数组,就是模块所有可能的路径。基本上是,从当前路径开始一级级向上寻找 node_modules 子目录。最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了。

有了可能的路径以后,下面就是 Module._findPath() 的源码,用来确定到底哪一个是正确路径。

Module._findPath = function(request, paths) {

 // 列出所有可能的后缀名:.js,.json, .node
 var exts = Object.keys(Module._extensions);

 // 如果是绝对路径,就不再搜索
 if (request.charAt(0) === '/') {
  paths = [''];
 }

 // 是否有后缀的目录斜杠
 var trailingSlash = (request.slice(-1) === '/');

 // 第一步:如果当前路径已在缓存中,就直接返回缓存
 var cacheKey = JSON.stringify({request: request, paths: paths});
 if (Module._pathCache[cacheKey]) {
  return Module._pathCache[cacheKey];
 }

 // 第二步:依次遍历所有路径
 for (var i = 0, PL = paths.length; i < PL; i++) {
  var basePath = path.resolve(paths[i], request);
  var filename;

  if (!trailingSlash) {
   // 第三步:是否存在该模块文件
   filename = tryFile(basePath);

   if (!filename && !trailingSlash) {
    // 第四步:该模块文件加上后缀名,是否存在
    filename = tryExtensions(basePath, exts);
   }
  }

  // 第五步:目录中是否存在 package.json 
  if (!filename) {
   filename = tryPackage(basePath, exts);
  }

  if (!filename) {
   // 第六步:是否存在目录名 + index + 后缀名 
   filename = tryExtensions(path.resolve(basePath, 'index'), exts);
  }

  // 第七步:将找到的文件路径存入返回缓存,然后返回
  if (filename) {
   Module._pathCache[cacheKey] = filename;
   return filename;
  }
 }

 // 第八步:没有找到文件,返回false 
 return false;
};
ログイン後にコピー

经过上面代码,就可以找到模块的绝对路径了。

有时在项目代码中,需要调用模块的绝对路径,那么除了 module.filename ,Node 还提供一个 require.resolve 方法,供外部调用,用于从模块名取到绝对路径。

require.resolve = function(request) {
 return Module._resolveFilename(request, self);
};

// 用法
require.resolve('a.js')
// 返回 /home/ruanyf/tmp/a.js
ログイン後にコピー

五、加载模块

有了模块的绝对路径,就可以加载该模块了。下面是 module.load 方法的源码。

Module.prototype.load = function(filename) {
 var extension = path.extname(filename) || '.js';
 if (!Module._extensions[extension]) extension = '.js';
 Module._extensions[extension](this, filename);
 this.loaded = true;
};
ログイン後にコピー

上面代码中,首先确定模块的后缀名,不同的后缀名对应不同的加载方法。下面是 .js 和 .json 后缀名对应的处理方法。

Module._extensions['.js'] = function(module, filename) {
 var content = fs.readFileSync(filename, 'utf8');
 module._compile(stripBOM(content), filename);
};

Module._extensions['.json'] = function(module, filename) {
 var content = fs.readFileSync(filename, 'utf8');
 try {
  module.exports = JSON.parse(stripBOM(content));
 } catch (err) {
  err.message = filename + ': ' + err.message;
  throw err;
 }
};
ログイン後にコピー

这里只讨论 js 文件的加载。首先,将模块文件读取成字符串,然后剥离 utf8 编码特有的BOM文件头,最后编译该模块。

module._compile 方法用于模块的编译。

Module.prototype._compile = function(content, filename) {
 var self = this;
 var args = [self.exports, require, self, filename, dirname];
 return compiledWrapper.apply(self.exports, args);
};
ログイン後にコピー

上面的代码基本等同于下面的形式。

(function (exports, require, module, __filename, __dirname) {
 // 模块源码
});
ログイン後にコピー

也就是说,模块的加载实质上就是,注入exports、require、module三个全局变量,然后执行模块的源码,然后将模块的 exports 变量的值输出。

(完)

このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

require の用途は何ですか? require の用途は何ですか? Nov 27, 2023 am 10:03 AM

require の使用法: 1. モジュールの導入: 多くのプログラミング言語では、require は外部モジュールまたはライブラリを導入し、それらが提供する関数をプログラム内で使用できるようにするために使用されます。たとえば、Ruby では、require を使用してサードパーティのライブラリまたはモジュールをロードできます。 2. クラスまたはメソッドのインポート: 一部のプログラミング言語では、require を使用して特定のクラスまたはメソッドをインポートし、現在のファイルで使用できるようにします。 ; 3. 特定のタスクを実行する: 一部のプログラミング言語またはフレームワークでは、特定のタスクまたは関数を実行するために require が使用されます。

PHP ヘッダーの致命的なエラーを解決するためのヒント: require(): 必要な 'data/tdk.php' を開けませんでした PHP ヘッダーの致命的なエラーを解決するためのヒント: require(): 必要な 'data/tdk.php' を開けませんでした Nov 27, 2023 pm 01:06 PM

PHP 開発では、fatalerror:require(): Failedopeningrequired'data/tdk.php' のようなエラー プロンプトが頻繁に表示されます。このエラーは通常、PHP アプリケーションでのファイル処理に関連しており、具体的な理由としては、ファイル パスが正しくない、ファイルが存在しない、またはファイルのアクセス許可が不十分であることが考えられます。この記事では、そんなエラーメッセージを解決するためのヒントを紹介します。 「致命的」な場合はファイルパスを確認してください。

致命的なエラー: require(): 必要な「data/tdk.php」エラーの修正に失敗しました 致命的なエラー: require(): 必要な「data/tdk.php」エラーの修正に失敗しました Nov 27, 2023 am 11:40 AM

Fatalerror:require():Failedopeningrequired「data/tdk.php」エラー修復方法 Webサイトの開発や保守の過程で、さまざまなエラーに遭遇することがよくあります。一般的なエラーの 1 つは、「Fatalerror:require():Failedopeningrequired'data/tdk.php'」です。このエラーは

PHP ヘッダーの致命的なエラーを解決する方法: require(): 必要な 'data/tdk.php' を開くことができませんでした (include_path='.;C:\php\pear') PHP ヘッダーの致命的なエラーを解決する方法: require(): 必要な 'data/tdk.php' を開くことができませんでした (include_path='.;C:\php\pear') Nov 27, 2023 am 11:03 AM

PHP ヘッダーの FatalError を解決するメソッドの概要: require():Failedopeningrequired'data/tdk.php'(include_path='.;C:phppear'): PHP を使用して Web サイトを開発する過程で、さまざまな問題に遭遇することがよくあります。問題、そのようなエラー。その中には、「FatalError:require():Failedopeningrequ」

反応使用は必要ですか? 反応使用は必要ですか? Dec 27, 2022 am 09:47 AM

React では require を使用できますが、その正しい使用方法は次のとおりです: 1. "<img src={require('../img/icon1.png')} alt="" />" を通じて画像を読み取ります。 2. "require('~/images/2.png').default" メソッドを使用して画像を読み取ります。 3. img フィールドをファイル名と画像名の 2 つの部分に分割し、"require('@/assets)" を使用します。 「読む方法」 そのまま受け取ってください。

ノードが必要です それはどういう意味ですか ノードが必要です それはどういう意味ですか Oct 18, 2022 pm 05:51 PM

ノードの require はパラメータを受け入れる関数であり、仮パラメータの名前は id で、型は String です。require 関数はモジュール、JSON ファイル、およびローカル ファイルをインポートできます。モジュールは「node_modules」からの相対パスを通じてアクセスできます。 "、"ローカル モジュール"、または "JSON ファイル" の場合、パスは "__dirname" 変数または現在の作業ディレクトリになります。

ソースコード分析を入り口として、3 つの異なる Java ファクトリ パターンの実装方法を明らかにする ソースコード分析を入り口として、3 つの異なる Java ファクトリ パターンの実装方法を明らかにする Dec 28, 2023 am 09:29 AM

ファクトリ パターンはソフトウェア開発で広く使用されており、オブジェクトを作成するための設計パターンです。 Java は、業界で広く使用されている人気のあるプログラミング言語です。 Java には、ファクトリ パターンのさまざまな実装が多数あります。この記事では、Java ファクトリ パターンをソース コードの観点から解釈し、3 つの異なる実装方法を検討します。 Java のファクトリー パターンは、オブジェクトの作成と管理に役立ちます。オブジェクトのインスタンス化プロセスをファクトリ クラスに一元化し、クラス間の結合を減らし、改善します。

関連する PHP ヘッダーの致命的なエラーを解決する方法: require(): Failed opens required 'data/tdk.php' エラー 関連する PHP ヘッダーの致命的なエラーを解決する方法: require(): Failed opens required 'data/tdk.php' エラー Nov 27, 2023 am 10:26 AM

関連する PHP ヘッダーの FatalError:require():Failedopeningrequired'data/tdk.php' エラーを解決する方法 PHP アプリケーションの開発中に、さまざまなエラーが発生することがあります。一般的なエラーの 1 つは、「FatalError:require():Failedopeningrequired'data/tdk.php'」です。これは間違っています

See all articles