Node.js 開発者であれば、おそらく cjs モジュールと esm モジュールについて聞いたことがあるでしょうが、なぜ 2 つあるのか、Node.js アプリケーションでこれらがどのように共存するのかはよくわからないかもしれません。このブログ投稿では、Node.js の JavaScript モジュールの歴史を簡単に説明します (例付き ?)。これにより、これらの概念をより自信を持って扱うことができます。
当初、JavaScript にはグローバル スコープのみがあり、すべてのメンバーが宣言されていました。 2 つの独立したファイルがメンバーに同じ名前を使用する可能性があるため、コードを共有する場合、これは問題でした。例:
greet-1.js
function greet(name) { return `Hello ${name}!`; }
greet-2.js
var greet = "...";
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
Node.js は、CommonJS (cjs とも呼ばれる) を使用した JavaScript モジュールの概念を正式に導入しました。これにより、開発者が何をエクスポートするか (module.exports 経由)、何をインポートするか (require() 経由) を決定できるため、共有グローバル スコープの衝突問題が解決されました。例:
src/greet.js
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
src/main.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
Node.js 開発は、開発者が再利用可能な JavaScript コードを公開および利用できるようにした npm パッケージのおかげで人気が爆発的に高まりました。 npm パッケージは、デフォルトでは、node_modules フォルダーにインストールされます。すべての npm パッケージに存在する package.json ファイルは、「main」プロパティを介してどのファイルがエントリ ポイントである Node.js を示すことができるため、特に重要です。例:
node_modules/greeter/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/greeter/entry-point.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
src/main.js
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
npm パッケージは、他の開発者の作業を活用できるため、開発者の生産性が劇的に向上しました。ただし、cjs は Web ブラウザと互換性がないという大きな欠点がありました。この問題を解決するために、バンドラーの概念が生まれました。 browserify は、基本的にエントリ ポイントをトラバースし、require() で処理されたすべてのコードを Web ブラウザと互換性のある単一の .js ファイルに「バンドル」することで機能する最初のバンドラーでした。時間が経つにつれて、追加の機能と差別化要因を備えた他のバンドラーが導入されました。最も注目すべきは、webpack、parcel、rollup、esbuild、vite (時系列順)。
Node.js および cjs モジュールが主流になるにつれて、ECMAScript 仕様の管理者はモジュールの概念を含めることを決定しました。これが、ネイティブ JavaScript モジュールが ESModules または esm (ECMAScript モジュールの略) とも呼ばれる理由です。
esm は、メンバーをエクスポートおよびインポートするための新しいキーワードと構文を定義し、デフォルトのエクスポートなどの新しい概念を導入します。時間が経つにつれ、esm モジュールには動的 import() やトップレベルの await などの新しい機能が追加されました。例:
src/greet.js
function greet(name) { return `Hello ${name}!`; }
src/part.js
var greet = "...";
src/main.js
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Collision example</title> </head> <body> <!-- After this script, `greet` is a function --> <script src="greet-1.js"></script> <!-- After this script, `greet` is a string --> <script src="greet-2.js"></script> <script> // TypeError: "greet" is not a function greet(); </script> </body> </html>
esm は、esm 構文を cjs に変換できるため、バンドラーや TypeScript などの言語のおかげで、開発者に広く採用されるようになりました。
需要の高まりにより、Node.js はバージョン 12.x で正式に esm のサポートを追加しました。 cjs との下位互換性は次のように実現されました:
npm パッケージの互換性に関しては、esm モジュールは cjs および esm エントリ ポイントを使用して npm パッケージをインポートできます。ただし、その逆にはいくつかの注意点があります。次の例を見てみましょう:
node_modules/cjs/package.json
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported function greet(name) { return `${GREETING_PREFIX} ${name}!`; } // `exports` is a shortcut to `module.exports` exports.greet = greet;
node_modules/cjs/entry.js
// notice the `.js` suffix is missing const { greet } = require("./greet"); // logs: Hello Alice! console.log(greet("Alice"));
node_modules/esm/package.json
{ "name": "greeter", "main": "./entry-point.js" // ... }
node_modules/esm/entry.js
module.exports = { greet(name) { return `Hello ${name}!`; } };
以下は問題なく動作します:
src/main.mjs
// notice there's no relative path (e.g. `./`) const { greet } = require("greeter"); // logs: Hello Bob! console.log(greet("Bob"));
ただし、次のコマンドは実行できません:
src/main.cjs
// this remains "private" const GREETING_PREFIX = "Hello"; // this will be exported export function greet(name) { return `${GREETING_PREFIX} ${name}!`; }
これが許可されない理由は、esm モジュールではトップレベルの await が許可されるのに対し、require() 関数は同期であるためです。コードは動的 import() を使用するように書き直すこともできますが、Promise を返すため、次のようなものにする必要があります:
src/main.cjs
// default export: new concept export default function part(name) { return `Goodbye ${name}!`; }
この互換性の問題を軽減するために、一部の npm パッケージは、条件付きエクスポートで package.json の "exports" プロパティを利用することにより、cjs と mjs の両方のエントリ ポイントを公開します。例:
node_modules/esm/entry.cjs:
// notice the `.js` suffix is required import part from "./part.js"; // dynamic import: new capability // top-level await: new capability const { greet } = await import("./greet.js"); // logs: Hello Alice! console.log(greet("Alice")); // logs: Bye Bob! console.log(part("Bob"));
node_modules/esm/package.json:
{ "name": "cjs", "main": "./entry.js" }
「exports」プロパティをサポートしていない Node.js バージョンとの下位互換性のために、「main」が cjs バージョンをどのように指しているかに注目してください。
cjs および esm モジュールについて知っておく必要があるのは (ほぼ) これだけです (2024 年 12 月時点?)。以下からご意見をお聞かせください!
以上がNode.js: CJS、バンドラー、ESM の簡単な歴史の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。