Node.js クイック入門コース: 学習するために入力してください
2 年前、私はモジュール システムを紹介する記事「フロントエンド モジュールの概念を理解する: CommonJs と ES6Module」を書きました。この記事の知識は初心者向けであり、比較的簡単です。ここで、記事内のいくつかの間違いも修正します。
[モジュール]と[モジュールシステム]は別のものです。モジュールはソフトウェアの単位であり、モジュール システムは、開発者がプロジェクトでモジュールを定義して使用できるようにする構文またはツールのセットです。
ECMAScript Module の略称は ES6Module ではなく、ESM または ESModule です。
モジュール システムに関する基本的な知識は前回の記事でほぼ説明されているため、この記事ではモジュール システムの内部原理に焦点を当て、さまざまなモジュール システムの違いについて詳しく説明します。前回の記事の内容はこちらにあります。二度と繰り返されません。
すべてのプログラミング言語にモジュール システムが組み込まれているわけではなく、JavaScript も誕生から長い間モジュール システムを持っていませんでした。
ブラウザ環境では、未使用のコード ファイルを導入するには<script>
タグしか使用できません。この方法はグローバルなスコープを共有するため、フロントエンドの急速な開発と相まって、問題が多いと言えます。現在のニーズを満たします。公式モジュール システムが登場する前に、フロントエンド コミュニティは独自のサードパーティ モジュール システムを作成しました。最も一般的に使用されるものは、非同期モジュール定義AMD 、ユニバーサル モジュール定義UMDなどです。もちろん、最も有名なものはCommonJSです。
Node.js は JavaScript ランタイム環境であるため、基盤となるファイル システムに直接アクセスできます。そこで開発者はそれを採用し、CommonJS 仕様に従ってモジュール システムを実装しました。
当初、CommonJS は Node.js プラットフォーム上でのみ使用できましたが、Browserify や Webpack などのモジュール パッケージ化ツールの登場により、ついに CommonJS がブラウザ側で実行できるようになりました。
2015 年に ECMAScript6 仕様がリリースされてから、この標準に従って構築されたモジュール システムは略してECMAScript モジュール(ESM) と呼ばれるようになりました。それ以来、ESM は統合され始めました。 Node.js環境とブラウザ環境。もちろん、ECMAScript6 は構文とセマンティクスを提供するだけです。実装に関しては、さまざまなブラウザ サービス ベンダーと Node 開発者の努力次第です。他のプログラミング言語がうらやむようなBabelアーティファクトが存在するのは、Node.js のバージョン 13.2 でのみ比較的安定した ESM の実装が簡単な作業ではないのはそのためです。
しかし、何はともあれ、ESM は JavaScript の「息子」であり、それを学ぶことに何の問題もありません。
焼き畑の時代には、アプリケーションの開発には JavaScript が使用され、スクリプト ファイルは script タグを通じてのみ導入できました。より深刻な問題の 1 つは、名前空間メカニズムがないことです。これは、各スクリプトが同じスコープを共有することを意味します。この問題に対するより良い解決策がコミュニティにあります: Revaling モジュール
const myModule = (() => { const _privateFn = () => {} const_privateAttr = 1 戻る { publicFn: () => {}, パブリック属性: 2 } })() console.log(myModule) console.log(myModule.publicFn、myModule._privateFn)
実行結果は次のとおりです。
このパターンは非常に単純で、 IIFE を使用してプライベート スコープを作成し、公開される戻り変数を使用します。内部変数 (_privateFn、_privateAttr など) にはスコープ外からアクセスできません。
[暴露モジュール] はこれらの機能を利用して、個人情報を隠し、外部に公開する必要がある API をエクスポートします。その後のモジュールシステムもこの考え方に基づいて開発されています。
上記の考え方に基づいて、モジュールローダーを開発します。
まずモジュールのコンテンツをロードする関数を作成し、この関数をプライベート スコープでラップし、次に eval() を通じて評価して関数を実行します。
function loadModule (ファイル名、モジュール、require) { const ラップされたSrc = `(関数 (モジュール、エクスポート、要求) { ${fs.readFileSync(ファイル名, 'utf8)} }(モジュール、module.exports、require)` eval(ラップされたSrc) }
[revealing module] と同様に、モジュールのソース コードは関数でラップされています。異なる点は、一連の変数 (module、module.exports、require) も関数に渡されることです。
モジュールのコンテンツは [readFileSync] を通じて読み取られることに注意してください。一般に、ファイル システムに関係する API を呼び出す場合は、同期バージョンを使用しないでください。ただし、今回は異なります。CommonJs システム自体を介したモジュールのロードは、複数のモジュールを正しい依存関係の順序で導入できるように、同期操作として実装する必要があるからです。
次に、require() 関数をシミュレートします。この関数の主な機能はモジュールをロードすることです。
関数 require(モジュール名) { const id = require.resolve(モジュール名) if (require.cache[id]) { require.cache[id].exports を返す } // モジュールのメタデータ const module = { エクスポート: {}、 ID } //キャッシュを更新 require.cache[id] = module //ロードモジュールloadModule(id, module, require) // エクスポートされた変数を返します return module.exports } require.cache = {} require.resolve = (モジュール名) => { // moduleName に基づいて完全なモジュール ID を解析します。 }
(1) 関数は moduleName を受け取った後、まずモジュールの完全なパスを解析し、それを ID に割り当てます。
(2) cache[id]
が true の場合、モジュールがロードされたことを意味し、キャッシュ結果が直接返されます。(3)それ以外の場合は、最初のロード用に環境が設定されます。具体的には、exports (つまり、エクスポートされたコンテンツ) と id (関数は上記のとおり) を含むモジュール オブジェクトを作成します。
(4) 初めてロードしたモジュールをキャッシュします (5)loadModule を通じてモジュールのソースファイルからソースコードを読み込みます (6)最後にreturn module.exports
でエクスポートしたい内容を返します。
require 関数をシミュレートする場合、非常に重要な点があります。それは、require 関数が同期的である必要があるということです。その機能はモジュールのコンテンツを直接返すことだけであり、コールバック メカニズムは使用しません。 Node.js の require にも同じことが当てはまります。したがって、module.exports の割り当て操作も同期する必要があります。非同期を使用すると、次の問題が発生します。
// 何か問題が発生しました setTimeout(() => { module.exports = function () {} }、1000)
require が同期関数であるという事実は、モジュールを定義する方法に非常に重要な影響を与えます。これは、モジュールを定義するときに同期コードのみを使用することを強制するため、Node.js はこの目的のためにほとんどの非同期 API の同期バージョンを提供します。
初期の Node.js には require 関数の非同期バージョンがありましたが、関数が非常に複雑になるため、すぐに削除されました。
ESM は ECMAScript2015 仕様の一部であり、さまざまな実行環境に適応する JavaScript 言語の公式モジュール システムを指定します。
デフォルトでは、Node.js は .js 接尾辞を持つファイルを CommonJS 構文を使用して書かれたものとして扱います。 ESM 構文を .js ファイル内で直接使用すると、インタープリターによってエラーが報告されます。
Node.js インタープリターを ESM 構文に変換するには、次の 3 つの方法があります。
1. ファイル拡張子を .mjs に変更します。
2. 最新の package.json ファイルに type フィールドを値「module」で追加します。
3. 文字列はパラメータとして--eval
に渡されるか、フラグ--input-type=module
を使用して STDIN パイプ経由でノードに送信されます。
例えば:
node --input-type=module --eval "「node:path」から { sep } をインポートします。 コンソール.ログ(sep);"
ESM は解析して URL としてキャッシュできます (これは、特殊文字をパーセントでエンコードする必要があることも意味します)。 file:
node:
、 data:
ファイル:URL
モジュールの解決に使用されるインポート指定子に異なるクエリまたはフラグメントがある場合、モジュールが複数回ロードされる
// 2 つの異なるモジュールとみなされます import './foo.mjs?query=1'; インポート './foo.mjs?query=2';
データ:URL
MIME タイプを使用したインポートをサポートします。
ESモジュールのtext/javascript
JSONの場合はapplication/json
Wasm 用のapplication/wasm
import 'data:text/javascript,console.log("hello!");'; import _ from 'data:application/json,"world!"'assert { type: 'json' };
data:URL
組み込みモジュールのベアおよび絶対指定子のみを解析します。 data:
は特別なプロトコルではなく、相対解析の概念がないため、相対指定子の解析は機能しません。
インポート アサーション<br/>この属性は、モジュール インポート ステートメントにインライン構文を追加して、モジュール指定子の隣に詳細情報を渡します。
import fooData from './foo.json' アサート { type: 'json' }; const { デフォルト: barData } = await import('./bar.json', {assert: { type: 'json' } });
現在、JSON モジュールのみがサポートされており、 assert { type: 'json' }
構文は必須です。
ウォッシュ モジュールのインポート<br/>WebAssembly モジュールのインポートは--experimental-wasm-modules
フラグでサポートされており、任意の .wasm ファイルを通常のモジュールとしてインポートできると同時に、モジュールのインポートもサポートされます。
// インデックス.mjs import * as M from './module.wasm'; コンソールログ(M)
次のコマンドを使用して実行します。
ノード --experimental-wasm-modulesindex.mjs
await キーワードは、ESM のトップレベルで使用できます。
// a.mjs エクスポート const 5 = Promise.resolve(5) を待つ // b.mjs './a.mjs' から { Five } をインポートします console.log(5) // 5
前述したように、import ステートメントによるモジュール依存関係の解決は静的であるため、次の 2 つの有名な制限があります。
モジュール識別子は、実行時まで構築するのを待つことができません。
モジュール インポート ステートメントはファイルの先頭に記述する必要があり、制御フロー ステートメントにネストすることはできません。
ただし、状況によっては、これら 2 つの制限は間違いなく厳しすぎます。たとえば、比較的一般的な要件として、遅延読み込みがあります。
大きなモジュールに遭遇した場合、モジュール内の特定の機能を本当に使用する必要がある場合にのみ、この巨大なモジュールをロードする必要があります。
この目的のために、ESM は非同期導入メカニズムを提供します。この導入操作は、プログラムの実行中にimport()
オペレーターを通じて実行できます。構文の観点からは、モジュール識別子をパラメーターとして受け取り、Promise を返す関数と同等です。Promise が解決された後、解析されたモジュール オブジェクトを取得できます。
循環依存関係の例を使用して、ESM の読み込みプロセスを説明します。
// インデックス.js import * as foo from './foo.js'; import * as bar from './bar.js'; コンソール.log(foo); コンソール.ログ(バー); // foo.js import * as Bar from './bar.js' エクスポートしてロード = false; エクスポート const bar = バー; ロード済み = true; //バー.js import * as Foo from './foo.js'; エクスポートしてロード = false; エクスポート const foo = Foo; ロード済み = true
まずは実行結果を見てみましょう。
ロードを通じて、モジュール foo と bar の両方が、ロードされた完全なモジュール情報をログに記録できることがわかります。しかし、CommonJS は異なります。完全にロードされた後にどのように表示されるかを出力できないモジュールが存在する必要があります。
読み込みプロセスを詳しく見て、この結果が発生する理由を見てみましょう。
読み込みプロセスは 3 つの段階に分けることができます。
最初の段階: 分析
第二段階:宣言
第 3 段階: 実行
解析段階:
インタプリタはエントリファイル (つまり、index.js) から起動し、モジュール間の依存関係を分析し、グラフの形式で表示します。このグラフは依存関係グラフとも呼ばれます。
この段階では、import ステートメントのみに注目し、これらのステートメントが導入するモジュールに対応するソース コードをロードします。そして、詳細な分析を通じて最終的な依存関係グラフを取得します。上の例を使って説明します。
1.index.js から開始して、 import * as foo from './foo.js'
見つけて、foo.js ファイルに移動します。
2. foo.js ファイルからの解析を続行し、 import * as Bar from './bar.js'
ステートメントを見つけて、bar.js に移動します。
3. bar.js からの解析を続行し、 import * as Foo from './foo.js'
を見つけます。これにより、循環依存関係が形成されます。ただし、インタープリターはすでに foo.js モジュールを処理しているため、モジュールには入りません。もう一度バーモジュールを解析して続行します。
4. bar モジュールを解析すると、import 文が存在しないことが判明したため、foo.js に戻って解析を続けます。 import 文は最後まで見つからず、index.js が返されました。
5. import * as bar from './bar.js'
がindex.jsにありますが、bar.jsはすでに解析されているため、スキップされて実行を続行します。
最後に、深さ優先アプローチによって依存関係グラフが完全に表示されます。
宣言フェーズ:
インタプリタは取得した依存関係グラフから開始し、下から上へ順番に各モジュールを宣言します。具体的には、モジュールに到達するたびに、モジュールによってエクスポートされるすべてのプロパティが検索され、エクスポートされた値の識別子がメモリ内で宣言されます。この段階では宣言のみが行われ、代入操作は実行されないことに注意してください。
1. インタプリタは bar.js モジュールから開始され、loaded と foo の識別子を宣言します。
2. foo.js モジュールまでトレースバックし、ロードされた識別子と bar 識別子を宣言します。
3. Index.js モジュールに到達しましたが、このモジュールにはエクスポート ステートメントがないため、識別子は宣言されていません。
すべてのエクスポート識別子を宣言した後、依存関係グラフをもう一度調べて、インポートとエクスポートの間の関係を結び付けます。
import によって導入されたモジュールと、export によってエクスポートされた値の間には、const と同様のバインディング関係が確立されていることがわかります。インポート側は読み取りのみ可能ですが、書き込みはできません。さらに、index.js で読み取られる bar モジュールと foo.js で読み込まれる bar モジュールは本質的に同じインスタンスです。
このため、この例の結果には完全な解析結果が出力されます。
これは、CommonJS システムで使用されるアプローチとは根本的に異なります。モジュールが CommonJS モジュールをインポートする場合、システムは後者のエクスポート オブジェクト全体をコピーし、その内容を現在のモジュールにコピーします。この場合、インポートされたモジュールが独自のコピー変数を変更すると、ユーザーは新しい値を確認できません。 。
実行フェーズ:
この段階で、エンジンはモジュールのコードを実行します。依存関係グラフは依然としてボトムアップ順序でアクセスされ、アクセスされたファイルは 1 つずつ実行されます。実行は bar.js ファイルから始まり、foo.js、最後にindex.js まで行われます。このプロセスでは、エクスポート テーブル内の識別子の値が徐々に改善されます。
このプロセスは CommonJS とあまり変わらないように見えますが、実際には大きな違いがあります。 CommonJS は動的であるため、関連ファイルの実行中に依存関係グラフを解析します。そのため、require ステートメントが表示されている限り、プログラムがこのステートメントに到達した時点で、以前のすべてのコードが実行されていることがわかります。したがって、require ステートメントは必ずしもファイルの先頭に出現する必要はなく、どこにでも出現することができ、モジュール識別子は変数から構築することもできます。
ただし、ESM では、コードを実行する前に、上記の 3 つの段階が互いに分離されているため、モジュールの導入とエクスポートの操作は静的である必要があります。コードが実行されるまで待ちます。
前述のいくつかの違いに加えて、注目に値するいくつかの違いがあります。
ESM で import キーワードを使用して相対指定子または絶対指定子を解決する場合は、ファイル拡張子を指定し、ディレクトリ インデックス ('./path/index.js') を完全に指定する必要があります。 CommonJS の require 関数を使用すると、この拡張機能を省略できます。
ESM はデフォルトで厳密モードで実行され、この厳密モードを無効にすることはできません。したがって、宣言されていない変数を使用したり、非厳密モードでのみ使用できる機能 (with など) を使用したりすることはできません。
CommonJS にはいくつかのグローバル変数が用意されています。これらの変数は ESM では使用できません。これらの変数を使用しようとすると、ReferenceError が発生します。含む
require
exports
module.exports
__filename
__dirname
このうち、 __filename
現在のモジュール ファイルの絶対パスを指し、 __dirname
はファイルが配置されているフォルダーの絶対パスを指します。これら 2 つの変数は、現在のファイルの相対パスを構築するときに非常に役立つため、ESM には 2 つの変数の機能を実装するためのメソッドがいくつか用意されています。
ESM では、 import.meta
オブジェクトを使用して、現在のファイルの URL を参照する参照を取得できます。具体的には、現在のモジュールのファイル パスはimport.meta.url
を通じて取得されます。このパスの形式はfile:///path/to/current_module.js
に似ています。このパスに基づいて、 __filename
と__dirname
で表される絶対パスが構築されます。
'url' から { fileURLToPath } をインポートします 「パス」から { ディレクトリ名 } をインポートします const __filename = fileURLToPath(import.meta.url) const __dirname = ディレクトリ名(__ファイル名)
CommonJS の require() 関数をシミュレートすることもできます
import { createRequire } from 'モジュール' const require = createRequire(import.meta.url)
ESM グローバル スコープでは、これは未定義ですが、CommonJS モジュール システムでは、エクスポートへの参照です。
//ESM console.log(this) // 未定義 // CommonJS console.log(this === エクスポート) // true
前述したように、CommonJS require() 関数を ESM でシミュレートして CommonJS モジュールをロードできます。さらに、標準のインポート構文を使用して CommonJS モジュールを導入することもできますが、このインポート方法では、デフォルトでエクスポートされたもののみをインポートできます。
import packageMain from 'commonjs-package' // 'commonjs-package' から { method } をインポートすることは完全に可能です // エラー
CommonJS モジュールの require は、参照するファイルを常に CommonJS として扱います。 ES モジュールは非同期実行であるため、require を使用した ES モジュールのロードはサポートされていません。ただし、 import()
を使用して CommonJS モジュールから ES モジュールをロードすることはできます。
ESM はリリースされてから 7 年になりますが、node.js も安定してサポートしています。コンポーネント ライブラリを開発する場合、ESM しかサポートできません。ただし、古いプロジェクトとの互換性を保つためには、CommonJS のサポートも不可欠です。コンポーネント ライブラリが両方のモジュール システムからのエクスポートをサポートするようにするには、広く使用されている 2 つの方法があります。
CommonJS でパッケージを作成するか、ES モジュールのソース コードを CommonJS に変換して、名前付きエクスポートを定義する ES モジュール ラッパー ファイルを作成します。条件付きエクスポートを使用し、インポートでは ES モジュール ラッパーを使用し、require では CommonJS エントリ ポイントを使用します。たとえば、サンプルモジュールでは
// パッケージ.json { "タイプ": "モジュール", "エクスポート": { "インポート": "./wrapper.mjs", "require": "./index.cjs" } }
表示拡張子.cjs
および.mjs
を使用します。これは、 .js
のみを使用すると、デフォルトで CommonJS が使用されるか、または"type": "module"
によってこれらのファイルが ES モジュールとして扱われるためです。
// ./index.cjs エクスポート.名前 = '名前'; // ./wrapper.mjs './index.cjs' から cjsModule をインポートします エクスポート const 名 = cjsModule.name;
この例では:
// ESM を使用して import { name } from 'example' を導入します // CommonJS を使用して const { name } = require('example') を導入します
どちらの方法でも導入される名前は同じシングルトンです。
package.json ファイルは、個別の CommonJS および ES モジュール エントリ ポイントを直接定義できます。
// パッケージ.json { "タイプ": "モジュール", "エクスポート": { "インポート": "./index.mjs", "require": "./index.cjs" } }
これは、パッケージの CommonJS バージョンと ESM バージョンが同等である場合、たとえば、一方が他方のトランスパイルされた出力であり、パッケージの状態管理が慎重に分離されている (またはパッケージがステートレスである) 場合に実行できます。
ステータスが問題になる理由は、パッケージの CommonJS バージョンと ESM バージョンの両方がアプリケーションで使用される可能性があるためです。たとえば、依存関係には CommonJS バージョンが必要であるにもかかわらず、ユーザーのリファラー コードは ESM バージョンをインポートできるためです。これが発生すると、パッケージの 2 つのコピーがメモリにロードされるため、2 つの異なる状態が発生します。これにより、解決が困難なエラーが発生する可能性があります。
ステートレス パッケージ (たとえば、JavaScript の Math がパッケージである場合、すべてのメソッドが静的であるためステートレスになります) を作成することに加えて、状態を分離して潜在的にロードされる CommonJS と ESM で使用できるようにする方法があります。パッケージインスタンス:
可能であれば、インスタンス化されたオブジェクトにすべての状態を含めます。たとえば、JavaScript の Date は状態を含めるためにインスタンス化する必要があります。パッケージの場合は次のように使用されます。
「日付」から日付をインポートします。 const someDate = new Date(); // someDate には状態が含まれません。
new キーワードは必須ではありません。パッケージ関数は新しいオブジェクトを返したり、渡されたオブジェクトを変更してパッケージの外の状態を維持したりできます。
パッケージの CommonJS バージョンと ESM バージョン間で共有される 1 つ以上の CommonJS ファイルの状態を分離します。たとえば、CommonJS と ESM のエントリ ポイントは、それぞれ、index.cjs とindex.mjs です。
// インデックス.cjs const state = require('./state.cjs') module.exports.state = 状態; // インデックス.mjs './state.cjs' から状態をインポートします 輸出 { 州 }
example が require および import を介してアプリケーションで使用されている場合でも、example へのすべての参照には同じ状態が含まれており、どちらかのモジュール システムによる状態への変更は両方に適用されます。
この記事が役に立ったら、「いいね!」を押して応援してください。それが私が創作を続ける原動力です。
この記事では次の情報を引用しています。
node.jsの公式ドキュメント
Node.js デザイン パターン