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 变量的值输出。
(完)

JavaScriptは、現代のWeb開発のコア言語であり、その多様性と柔軟性に広く使用されています。 1)フロントエンド開発:DOM操作と最新のフレームワーク(React、Vue.JS、Angularなど)を通じて、動的なWebページとシングルページアプリケーションを構築します。 2)サーバー側の開発:node.jsは、非ブロッキングI/Oモデルを使用して、高い並行性とリアルタイムアプリケーションを処理します。 3)モバイルおよびデスクトップアプリケーション開発:クロスプラットフォーム開発は、反応および電子を通じて実現され、開発効率を向上させます。

JavaScriptの最新トレンドには、TypeScriptの台頭、最新のフレームワークとライブラリの人気、WebAssemblyの適用が含まれます。将来の見通しは、より強力なタイプシステム、サーバー側のJavaScriptの開発、人工知能と機械学習の拡大、およびIoTおよびEDGEコンピューティングの可能性をカバーしています。

JavaScriptは現代のWeb開発の基礎であり、その主な機能には、イベント駆動型のプログラミング、動的コンテンツ生成、非同期プログラミングが含まれます。 1)イベント駆動型プログラミングにより、Webページはユーザー操作に応じて動的に変更できます。 2)動的コンテンツ生成により、条件に応じてページコンテンツを調整できます。 3)非同期プログラミングにより、ユーザーインターフェイスがブロックされないようにします。 JavaScriptは、Webインタラクション、シングルページアプリケーション、サーバー側の開発で広く使用されており、ユーザーエクスペリエンスとクロスプラットフォーム開発の柔軟性を大幅に改善しています。

Pythonはデータサイエンスや機械学習により適していますが、JavaScriptはフロントエンドとフルスタックの開発により適しています。 1. Pythonは、簡潔な構文とリッチライブラリエコシステムで知られており、データ分析とWeb開発に適しています。 2。JavaScriptは、フロントエンド開発の中核です。 node.jsはサーバー側のプログラミングをサポートしており、フルスタック開発に適しています。

JavaScriptは、最新のブラウザにすでに組み込まれているため、インストールを必要としません。開始するには、テキストエディターとブラウザのみが必要です。 1)ブラウザ環境では、タグを介してHTMLファイルを埋め込んで実行します。 2)node.js環境では、node.jsをダウンロードしてインストールした後、コマンドラインを介してJavaScriptファイルを実行します。

Quartzタイマーを使用してタスクをスケジュールする場合、Quartzでタスク通知を事前に送信する方法、タスクの実行時間はCron式によって設定されます。今...

JavaScriptプログラミング、プロトタイプチェーンの関数パラメーターの理解と操作のJavaScriptのプロトタイプチェーンの関数のパラメーターを取得する方法は、一般的で重要なタスクです...

WeChatアプレットWeb-ViewでVue.jsを使用する動的スタイルの変位障害がvue.jsを使用している理由の分析...


ホットAIツール

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

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

Undress AI Tool
脱衣画像を無料で

Clothoff.io
AI衣類リムーバー

AI Hentai Generator
AIヘンタイを無料で生成します。

人気の記事

ホットツール

ZendStudio 13.5.1 Mac
強力な PHP 統合開発環境

AtomエディタMac版ダウンロード
最も人気のあるオープンソースエディター

Safe Exam Browser
Safe Exam Browser は、オンライン試験を安全に受験するための安全なブラウザ環境です。このソフトウェアは、あらゆるコンピュータを安全なワークステーションに変えます。あらゆるユーティリティへのアクセスを制御し、学生が無許可のリソースを使用するのを防ぎます。

SublimeText3 Linux 新バージョン
SublimeText3 Linux 最新バージョン

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