node.js require() ソースコードの解釈_node.js
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 变量的值输出。
(完)

ホットAIツール

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

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

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

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

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

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

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

ホットトピック









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

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

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

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

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

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

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

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