ホームページ  >  記事  >  ウェブフロントエンド  >  Node でのモジュール実装プロセスの詳細な紹介 (例付き)

Node でのモジュール実装プロセスの詳細な紹介 (例付き)

不言
不言転載
2019-03-08 17:34:272606ブラウズ

この記事では、Node でのモジュール実装プロセスを詳しく紹介します (例を示します)。一定の参考価値があります。必要な友人は参照できます。お役に立てれば幸いです。

CommonJS はモジュールを定義し、エクスポートし、モジュールの仕様を要求します。この単純な標準を実装するために、Node.js は基盤となる C/C 組み込みモジュールから JavaScript コア モジュールまで、パス分析、ファイルの配置からコンパイルまでを行います。一連の複雑なプロセスを経ました。 Node モジュールの原理を簡単に理解すると、Node に基づくフレームワークを再理解するのに役立ちます。

1. CommonJS モジュールの仕様

CommonJS の仕様または標準は単なる理論であり、JavaScript にはクライアントの開発だけでなく、ホスト環境全体で実行できる機能が期待されています。アプリケーションは、サーバー アプリケーション、コマンド ライン ツール、デスクトップ グラフィカル インターフェイス アプリケーションなどを開発することもできます。

CommonJS 仕様は、モジュールを 3 つの部分で定義します:

モジュール定義

モジュール オブジェクトは、モジュール自体を表すためにモジュール内に存在します。モジュール コンテキストは、exports 属性を提供します。およびメソッド エクスポート メソッドは、エクスポート オブジェクトにマウントすることで定義できます。たとえば、次のようになります。

    // math.js
   exports.add = function(){ //...}

モジュール リファレンス

module は、外部モジュールの API を導入するための require() メソッドを提供します。現在のコンテキストに:

   var math = require('math')
モジュール識別

モジュール識別は、実際には require() メソッドに渡されるパラメータであり、キャメルケースに従って名前を付けた文字列またはファイル パスにすることができます。

Node.js は CommonJS 仕様、特に CommonJS の Modules 仕様の設計を利用してモジュール システムを実装します。同時に、NPM は CommonJS の Packages 仕様を実装します。モジュールとパッケージは Node アプリケーション開発の基礎を形成します。

2. Node モジュールのロード原理

上記のモジュール仕様は module、exports、require だけで非常に単純に見えますが、Node はどのように実装されるのでしょうか?

パス分析 (モジュールの完全パス)、ファイルの場所 (ファイル拡張子またはディレクトリ)、コンパイルと実行の 3 つの手順を実行する必要があります。

2.1 パス解析

Review require() はモジュールを導入するためのモジュール識別子をパラメータとして受け取り、ノードはこの識別子を元にパス解析を行います。異なる識別子は異なる分析方法を使用し、主に次のカテゴリに分類されます。

http、fs、path など、Node によって提供されるコア モジュール

コア モジュールは、Node がコンパイルされるときにコンパイルされます。ソースコードはコンパイルされます バイナリの実行可能ファイルとして保存され、ノードの起動時にメモリに直接ロードされます パス解析が優先されるため、ロード速度が非常に速く、その後のファイルの配置、コンパイル、実行は必要ありません。

カスタム http モジュールなど、コア モジュールと同じ名前のカスタム モジュールをロードする場合は、別の識別子を使用するか、代わりにパスを使用する必要があります。

パス、.、..相対パス モジュールおよび /絶対パス モジュールの形式のファイル モジュール

.、..、または / で始まる識別子はファイル モジュールとして扱われ、ノードrequire() 内のパスはインデックスとして実際のパスに変換され、コンパイルされて実行されます。

ファイルモジュールはファイルの場所を明確にするため、パス解析時間は短縮されますが、ロード速度はコアモジュールより遅いだけです。

カスタム モジュール、つまり非パス形式のファイル モジュール

コア モジュールでも、パス形式のファイル モジュールでもありません。カスタム ファイルは特殊なファイル モジュールです。パスの場合、モジュール パス内のノード パスが一度に 1 レベルずつ検索されます。
モジュール パス検索戦略の例は次のとおりです。

// paths.js
console.log(module.paths)

// Terminal
$ node paths.js
[ '/Users/tong/WebstormProjects/testNode/node_modules',
'/Users/tong/WebstormProjects/node_modules',
'/Users/tong/node_modules',
'/Users/node_modules',
'/node_modules' ]

上記の例のモジュール パス配列出力からわかるように、モジュールを検索する場合、node_modules ディレクトリは、 JS プロトタイプ チェーンまたはスコープ チェーンと同様に、ターゲット パスに到達するまで現在のパスを保持します。パスが深くなるほど速度が遅くなるため、カスタム モジュールの読み込みが最も遅くなります。

キャッシュ優先メカニズム: ノードはインポートされたモジュールをキャッシュしてパフォーマンスを向上させます。ファイルをキャッシュするブラウザとは異なり、ノードはコンパイルおよび実行されたオブジェクトをキャッシュするため、require() は同じモジュールをキャッシュします。二次ロードではキャッシュ優先メソッドが採用されます。このキャッシュ優先度は第 1 優先度であり、コア モジュールの優先度よりも高くなります。

2.2 ファイルの配置

モジュール パスの分析が完了すると、主にファイル拡張子の分析、ディレクトリおよびパッケージの処理を含むファイルの配置が完了します。より明確に表現するために、ファイルの配置を 4 つのステップに分けます:

step1: 拡張子を補足する

通常、require() 内の識別子にはファイル拡張子は含まれません。 , Nodeは.js、.json、.nodeの順に拡張子を補完しようとします。

拡張子を追加しようとすると、fs モジュールを呼び出して同期的にブロックし、ファイルが存在するかどうかを確認する必要があります。そのため、ここでのパフォーマンスを向上させるちょっとしたトリックは、.json と .json を渡すときに拡張子を取り込むことです。ノードファイルを require() に追加すると、一部の速度が向上します。

step2: ディレクトリの処理と検索 pakage.json

拡張機能を追加した後に対応するファイルが見つからず、ディレクトリが取得された場合、Node はそのディレクトリをパッケージとして扱います。 CommonJS パッケージ仕様の実装に従って、Node はディレクトリ内で pakage.json (パッケージ説明ファイル) を検索し、JSON.parse() を通じてそれをパッケージ説明オブジェクトに解析し、main 属性で指定されたファイル名を取得します。 。

ステップ 3: デフォルトでインデックス ファイルの検索を続行します

如果没有pakage.json或者main属性指定的文件名错误,那 Node 会将 index 当做默认文件名,依次查找 index.js、index.json、index.node

step4: 进入下一个模块路径

在上述目录分析过程中没有成功定位时,自定义模块按路径查找策略进入上一层node_modules目录,当整个模块路径数组遍历完毕后没有定位到文件,则会抛出查找失败异常。

缓存加载的优化策略使得二次引入不需要路径分析、文件定位、编译执行这些过程,而且核心模块也不需要文件定位的过程,这大大提高了再次加载模块时的效率

2.3 编译执行

Node 中每个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,然后根据路径载入并编译。不同的文件扩展名载入方法为:

.js 文件: 通过 fs 模块同步读取后编译执行.json 文件: 通过 fs 模块同步读取后,用JSON.parse()解析并返回结果.node 文件: 这是用 C/C++ 写的扩展文件,通过process.dlopen()方法加载最后编译生成的其他扩展名: 都被当做 js 文件载入

载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()解析得到对象后直接赋值给模块对象的exports,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()载入执行就可以,下面重点介绍 .js 文件的编译:

在 CommonJS 模块规范中有module、exports 和 require 这3个变量,在 Node API 文档中每个模块还有 __filename、__dirname这两个变量,但是在模块中没有定义这些变量,那它们是怎么产生的呢?

事实上在编译过程中,Node 对每个 JS 文件都被进行了封装,例如一个 JS 文件会被封装成如下:

(function (exports, require, module, __filename, __dirname) {
    var math = require('math')
    export.add = function(){ //... }
})

首先每个模块文件之间都进行了作用域隔离,通过vm原生模块的runInThisContext()方法(类似 eval)返回一个具体的 function 对象,最后将当前模块对象的exports属性、require()方法、模块对象本身module、文件定位时得到的完整路径__filename和文件目录__dirname作为参数传递给这个 function 执行。模块的exports属性上的任何方法和属性都可以被外部调用,其余的则不可被调用。

至此,module、exports 和 require的流程就介绍完了。

曾经困惑过,每个模块都可以使用exports的情况下,为什么还必须用module.exports。

这是因为exports在编译过程中时通过形参传入的,直接给exports形参赋值只改变形参的引用,不能改变作用域外的值,例如:

let change = function (exports) {
  exports = 100
  console.log(exports)
}

var exports = 2
change(exports) // 100
console.log(exports) // 2

所以直接赋值给module.exports对象就不会改变形参的引用了。

编译成功的模块会将文件路径作为索引缓存在 Module._cache 对象上,路径分析时优先查找缓存,提高二次引入的性能。

三、Node 核心模块

总结来说 Node 模块分为Node提供的核心模块和用户编写的文件模块。文件模块是在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程,核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,所以不用文件定位和编译执行。

核心模块分为 C/C++ 编写的和 JavaScript 编写的两部分,在编译所有 C/C++ 文件之前,编译程序需要将所有的 JavaScript 核心模块编译为 C/C++ 可执行代码,编译成功的则放在 NativeModule._cache对象上,显然和文件模块 Module._cache的缓存位置不同。

在核心模块中,有些模块由纯 C/C++ 编写的内建模块,主要提供 API 给 JavaScript 核心模块,通常不能被用户直接调用,而有些模块由  C/C++ 完成核心部分,而 JavaScript 实现封装和向外导出,如 buffer、fs、os 等。

所以在Node的模块类型中存在依赖层级关系:内建模块(C/C++)—> 核心模块(JavaScript)—> 文件模块。

使用require()十分的方便,但从 JavaScript 到 C/C++ 的过程十分复杂,总结来说需要经历 C/C++ 层面内建模块的定义、(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块的引入。

四、前端模块规范

对比前后端的 JavaScript,浏览器端的 JavaScript 需要经历从同一个服务器端分发到多个客户端执行,通过网络加载代码,瓶颈在于宽带;而服务器端 JavaScript 相同代码需要多次执行,通过磁盘加载,瓶颈在于 CPU 和内存,所以前后端的 JavaScript 在 Http 两端的职责完全不用。

Node 模块的引入几乎是同步的,而前端模块如果同步引入,那脚本加载需要太长的时间,所以 CommonJS 为后端 JavaScript 制定的规范不适合前端。而后出现 AMD 和 CMD 用于前端应用场景。

4.1 AMD 规范

AMD 即异步模块定义(Asynchronous Module Definition),模块定义为:

define(id?, dependencies?, factory);

AMD 模块需要用define明确定义一个模块,其中模块id与依赖dependencies是可选的,factory的内容就是实际代码的内容。例如指定一些依赖到模块中:

define(['dep1', 'dep2'], function(){
    // module code
});

require.js 实现 AMD 规范的模块化,感兴趣的可以查看 require.js 的文档。

4.2 CMD 规范

CMD 模块的定义更加简单:

 define(factory);

定义的模块同 Node 模块一样是隐式包装,在依赖部分支持动态引入,例如:

 define(function(require, exports, module){
     // module code
 });

requireexportsmodule通过形参传递给模块,需要依赖模块时直接使用require()引入。

sea.js 实现 AMD 规范的模块化,感兴趣的可以查看 sea.js 的文档。

以上がNode でのモジュール実装プロセスの詳細な紹介 (例付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。