import.meta
– 現在のモジュールのメタデータ [ES2020]import.meta.url
import.meta.url
とクラスURL
import.meta.url
import()
による動的なモジュールのロード [ES2020] (高度)import
文の制限import()
演算子による動的なインポートimport()
のユースケースawait
[ES2022] (高度)await
のユースケースawait
はどのように内部で動作するのか?await
の長所と短所// Named exports
export function f() {}
export const one = 1;
export {foo, b as bar};
// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;
// Re-exporting from another module
export {foo, b as bar} from './some-module.mjs';
export * from './some-module.mjs';
export * as ns from './some-module.mjs'; // ES2020
// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';
// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';
// Empty import (for modules with side effects)
import './some-module.mjs';
JavaScriptモジュールの現在の状況は非常に多様です。ES6で組み込みモジュールが導入されましたが、それ以前のソースコード形式もまだ存在しています。後者を理解することは、前者を理解するのに役立つため、調べてみましょう。次のセクションでは、JavaScriptソースコードを配信する以下の方法について説明します。
表. 18は、これらのコード形式の概要を示しています。CommonJSモジュールとECMAScriptモジュールでは、2つのファイル名拡張子が一般的に使用されることに注意してください。どちらが適切かは、ファイルをどのように使用したいかによって異なります。詳細については、この章で後述します。
実行環境 | 読み込み | ファイル名拡張子 | |
---|---|---|---|
スクリプト | ブラウザ | 非同期 | .js |
CommonJSモジュール | サーバー | 同期 | .js .cjs |
AMDモジュール | ブラウザ | 非同期 | .js |
ECMAScriptモジュール | ブラウザとサーバー | 非同期 | .js .mjs |
組み込みモジュール(ES6で導入された)に入る前に、これから見るすべてのコードはES5で記述されます。特に
const
とlet
がなく、var
のみがありました。当初、ブラウザにはスクリプトだけがありました。これは、グローバルスコープで実行されるコードの断片です。例として、次のHTMLを介してスクリプトファイルをロードするHTMLファイルを考えてみましょう。
<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>
メインファイルはmy-module.js
で、モジュールをシミュレートします。
var myModule = (function () { // Open IIFE
// Imports (via global variables)
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
// Body
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports (assigned to global variable `myModule`)
return {
exportedFunc: exportedFunc,
;
}; // Close IIFE })()
myModule
は、関数式を即時呼び出した結果が代入されるグローバル変数です。関数式は1行目で始まり、最終行で呼び出されます。
コードの断片をラップするこの方法は、即時実行関数式(IIFE、ベン・アルマンが命名)と呼ばれます。IIFEから何が得られるのでしょうか?var
は(const
やlet
のように)ブロックスコープではなく、関数スコープです。つまり、var
で宣言された変数の新しいスコープを作成する唯一の方法は、関数またはメソッドを使用することです(const
とlet
では、関数、メソッド、またはブロック{}
を使用できます)。したがって、例のIIFEは、次のすべての変数をグローバルスコープから隠し、名前の衝突を最小限に抑えます:importedFunc1
、importedFunc2
、internalFunc
、exportedFunc
。
特定の形でIIFEを使用していることに注意してください。最後に、エクスポートしたいものを選択し、オブジェクトリテラルを介してそれを返します。これは、リビールモジュールパターン(クリスチャン・ハイルマンが命名)と呼ばれます。
モジュールをシミュレートするこの方法には、いくつかの問題があります。
ECMAScript 6より前は、JavaScriptには組み込みのモジュールはありませんでした。したがって、言語の柔軟な構文を使用して、言語内にカスタムモジュールシステムを実装しました。2つの人気のあるものは次のとおりです。
モジュールに関するオリジナルのCommonJS標準は、サーバーおよびデスクトッププラットフォーム向けに作成されました。これは、元のNode.jsモジュールシステムの基礎であり、そこで絶大な人気を博しました。その人気に貢献したのは、Nodeのnpmパッケージマネージャーと、クライアント側(browserify、webpackなど)でNodeモジュールを使用できるようにするツールでした。
今後、CommonJSモジュールとは、この標準のNode.jsバージョン(いくつかの追加機能があります)を意味します。これはCommonJSモジュールの例です。
// Imports
var importedFunc1 = require('./other-module1.js').importedFunc1;
var importedFunc2 = require('./other-module2.js').importedFunc2;
// Body
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports
.exports = {
moduleexportedFunc: exportedFunc,
; }
CommonJSは次のように特徴づけられます。
AMDモジュール形式は、CommonJS形式よりもブラウザで使いやすいように作成されました。最も人気のある実装は、RequireJSです。以下は、AMDモジュールの例です。
define(['./other-module1.js', './other-module2.js'],
function (otherModule1, otherModule2) {
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
function internalFunc() {
// ···
}function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
return {
exportedFunc: exportedFunc,
;
}; })
AMDは次のように特徴づけられます。
良い点として、AMDモジュールは直接実行できます。対照的に、CommonJSモジュールは、デプロイ前にコンパイルするか、カスタムソースコードを生成して動的に評価する必要があります(eval()
を考えてください)。これはWeb上で常に許可されているとは限りません。
CommonJSとAMDを見ると、JavaScriptモジュールシステムの間には類似点があります。
ECMAScriptモジュール (ESモジュールまたはESM) は、ES6で導入されました。これらは、JavaScriptモジュールの伝統を継承し、前述のすべての特性を備えています。さらに
ESモジュールには新しい利点もあります。
これはESモジュールの構文の例です。
import {importedFunc1} from './other-module1.mjs';
import {importedFunc2} from './other-module2.mjs';
function internalFunc() {
···
}
export function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
今後、「モジュール」とは「ECMAScriptモジュール」を意味します。
ESモジュールの完全な標準には、次の部分が含まれます。
パート1と2はES6で導入されました。パート3の作業は進行中です。
各モジュールには、ゼロ個以上の名前付きエクスポートを含めることができます。
例として、次の2つのファイルを考えてみましょう。
lib/my-math.mjs
main.mjs
モジュールmy-math.mjs
には、2つの名前付きエクスポートがあります: square
とLIGHTSPEED
。
// Not exported, private to module
function times(a, b) {
return a * b;
}export function square(x) {
return times(x, x);
}export const LIGHTSPEED = 299792458;
何かをエクスポートするには、宣言の前にキーワードexport
を置きます。エクスポートされないエンティティはモジュールに対してプライベートであり、外部からアクセスすることはできません。
モジュールmain.mjs
は、square
という名前付きインポートを1つ持っています。
import {square} from './lib/my-math.mjs';
.equal(square(3), 9); assert
インポート名を変更することもできます。
import {square as sq} from './lib/my-math.mjs';
.equal(sq(3), 9); assert
名前付きインポートと分割代入は見た目が似ています。
import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring
しかし、これらはまったく異なります。
インポートはエクスポートと接続されたままです。
分割代入パターンの中で再度分割代入を行うことはできますが、インポートステートメント内の{}
をネストすることはできません。
名前変更の構文は異なります。
import {foo as f} from './bar.mjs'; // importing
const {foo: f} = require('./bar.mjs'); // destructuring
理由: 分割代入はオブジェクトリテラル(ネストを含む)を連想させる一方、インポートは名前変更の考えを想起させます。
演習: 名前付きエクスポート
exercises/modules/export_named_test.mjs
名前空間インポートは、名前付きインポートの代替手段です。モジュールを名前空間インポートすると、そのプロパティが名前付きエクスポートとなるオブジェクトになります。名前空間インポートを使用した場合のmain.mjs
は次のようになります。
import * as myMath from './lib/my-math.mjs';
.equal(myMath.square(3), 9);
assert
.deepEqual(
assertObject.keys(myMath), ['LIGHTSPEED', 'square']);
これまで見てきた名前付きエクスポートのスタイルはインラインでした。エンティティの前にキーワードexport
を付けることでエクスポートしました。
しかし、別のエクスポート節を使用することもできます。たとえば、エクスポート節を使用したlib/my-math.mjs
は次のようになります。
function times(a, b) {
return a * b;
}function square(x) {
return times(x, x);
}const LIGHTSPEED = 299792458;
export { square, LIGHTSPEED }; // semicolon!
エクスポート節を使用すると、エクスポート前に名前を変更したり、内部で異なる名前を使用したりできます。
function times(a, b) {
return a * b;
}function sq(x) {
return times(x, x);
}const LS = 299792458;
export {
as square,
sq as LIGHTSPEED, // trailing comma is optional
LS ; }
各モジュールは、最大で1つのデフォルトエクスポートを持つことができます。モジュールがデフォルトエクスポートされた値であるというのがアイデアです。
名前付きエクスポートとデフォルトエクスポートを混在させない
モジュールは名前付きエクスポートとデフォルトエクスポートの両方を持つことができますが、通常はモジュールごとに1つのエクスポートスタイルに固執する方が良いでしょう。
デフォルトエクスポートの例として、次の2つのファイルを考えてみましょう。
my-func.mjs
main.mjs
モジュールmy-func.mjs
はデフォルトエクスポートを持っています。
const GREETING = 'Hello!';
export default function () {
return GREETING;
}
モジュールmain.mjs
はエクスポートされた関数をデフォルトインポートします。
import myFunc from './my-func.mjs';
.equal(myFunc(), 'Hello!'); assert
構文上の違いに注意してください。名前付きインポートの周りの波括弧は、モジュール内にアクセスしていることを示し、デフォルトインポートはモジュールそのものです。
デフォルトエクスポートのユースケースは何ですか?
デフォルトエクスポートの最も一般的なユースケースは、単一の関数または単一のクラスを含むモジュールです。
デフォルトエクスポートを行うには、2つのスタイルがあります。
まず、既存の宣言にexport default
というラベルを付けることができます。
export default function myFunc() {} // no semicolon!
export default class MyClass {} // no semicolon!
次に、値を直接デフォルトエクスポートできます。このexport default
のスタイルは、宣言に非常によく似ています。
export default myFunc; // defined elsewhere
export default MyClass; // defined previously
export default Math.sqrt(2); // result of invocation is default-exported
export default 'abc' + 'def';
export default { no: false, yes: true };
理由は、export default
を使用してconst
にラベルを付けることができないためです。const
は複数の値を定義できますが、export default
は正確に1つの値を必要とします。次の仮想コードを考えてみましょう。
// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;
このコードでは、3つの値のうちどれがデフォルトエクスポートであるかわかりません。
演習: デフォルトエクスポート
exercises/modules/export_default_test.mjs
内部的には、デフォルトエクスポートは、名前がdefault
である名前付きエクスポートにすぎません。例として、デフォルトエクスポートを持つ前のモジュールmy-func.mjs
を考えてみましょう。
const GREETING = 'Hello!';
export default function () {
return GREETING;
}
次のモジュールmy-func2.mjs
は、そのモジュールと同等です。
const GREETING = 'Hello!';
function greet() {
return GREETING;
}
export {
as default,
greet ; }
インポートの場合、通常のデフォルトインポートを使用できます。
import myFunc from './my-func2.mjs';
.equal(myFunc(), 'Hello!'); assert
または、名前付きインポートを使用できます。
import {default as myFunc} from './my-func2.mjs';
.equal(myFunc(), 'Hello!'); assert
デフォルトエクスポートは、名前空間インポートのプロパティ.default
からも利用できます。
import * as mf from './my-func2.mjs';
.equal(mf.default(), 'Hello!'); assert
default
は変数名として違法ではありませんか?
default
は変数名にはできませんが、エクスポート名やプロパティ名にはできます。
const obj = {
default: 123,
;
}.equal(obj.default, 123); assert
これまで、インポートとエクスポートを直感的に使用しており、すべてが期待どおりに機能しているように見えます。しかし、今度はインポートとエクスポートが実際どのように関連しているかを詳しく見ていく必要があります。
次の2つのモジュールを考えてみましょう。
counter.mjs
main.mjs
counter.mjs
は、(可変の!)変数と関数をエクスポートします。
export let counter = 3;
export function incCounter() {
++;
counter }
main.mjs
は両方のエクスポートを名前付きインポートします。incCounter()
を使用すると、counter
への接続がライブであることがわかります。つまり、常にその変数のライブ状態にアクセスできます。
import { counter, incCounter } from './counter.mjs';
// The imported value `counter` is live
.equal(counter, 3);
assertincCounter();
.equal(counter, 4); assert
接続はライブであり、counter
を読み取ることができますが、この変数(例: counter++
を使用)を変更することはできないことに注意してください。
このようにインポートを処理することには、2つの利点があります。
ESMは循環インポートを透過的にサポートします。それをどのように実現しているかを理解するために、次の例を考えてみましょう。図 7は、他のモジュールをインポートするモジュールの有向グラフを示しています。PがMをインポートすることが、この場合のサイクルです。
解析後、これらのモジュールは2つのフェーズで設定されます。
このアプローチは、ESモジュールの2つの機能により、循環インポートを正しく処理します。
ESモジュールの静的構造により、エクスポートは解析後にすでにわかっています。これにより、子Mの前にPをインスタンス化することができます。PはすでにMのエクスポートを調べることができます。
Pが評価されるとき、Mはまだ評価されていません。ただし、PのエンティティはすでにMからのインポートに言及できます。インポートされた値は後で入力されるため、まだ使用することはできません。たとえば、P内の関数はMからのインポートにアクセスできます。唯一の制限は、その関数を呼び出す前に、Mの評価後まで待機する必要があることです。
インポートが後で入力されるのは、それらがエクスポートに対する「ライブの不変のビュー」であることによって有効になります。
npmソフトウェアレジストリは、Node.jsおよびWebブラウザ向けのJavaScriptライブラリおよびアプリを配布する主要な方法です。これは、npmパッケージマネージャー (略してnpm) を介して管理されます。ソフトウェアは、いわゆるパッケージとして配布されます。パッケージは、任意のファイルと、パッケージを記述するトップレベルのファイルpackage.json
を含むディレクトリです。たとえば、npmがディレクトリmy-package/
内に空のパッケージを作成すると、次のpackage.json
が得られます。
{
"name": "my-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
これらのプロパティの一部には、単純なメタデータが含まれています。
name
は、このパッケージの名前を指定します。npmレジストリにアップロードされると、npm install my-package
を使用してインストールできます。version
はバージョン管理に使用され、3つの数字でセマンティックバージョニングに従います。description
、keywords
、author
を使用すると、パッケージを見つけやすくなります。license
は、このパッケージの使用方法を明確にします。その他のプロパティを使用すると、高度な構成が可能になります。
main
: パッケージであるモジュールを指定します(この章で後ほど説明します)。scripts
: npm run
を使用して実行できるコマンドです。たとえば、スクリプトtest
は、npm run test
を使用して実行できます。package.json
の詳細については、npmドキュメントを参照してください。
node_modules/
内にインストールされるnpmは常にパッケージをディレクトリnode_modules
内にインストールします。通常、これらのディレクトリは多数あります。npmが使用するディレクトリは、現在いるディレクトリによって異なります。たとえば、ディレクトリ/tmp/a/b/
内にいる場合、npmは現在のディレクトリ、その親ディレクトリ、親の親ディレクトリなどでnode_modules
を見つけようとします。つまり、次のロケーションのチェーンを検索します。
/tmp/a/b/node_modules
/tmp/a/node_modules
/tmp/node_modules
パッケージsome-pkg
をインストールする場合、npmは最も近いnode_modules
を使用します。たとえば、/tmp/a/b/
内にいて、そのディレクトリにnode_modules
がある場合、npmはパッケージをディレクトリ内に配置します。
/tmp/a/b/node_modules/some-pkg/
モジュールをインポートする場合、インストールされたパッケージからインポートしたいことをNode.jsに伝えるための特別なモジュール指定子を使用できます。それがどのように機能するかは後で説明します。今のところ、次の例を考えてみましょう。
// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';
the-module.mjs
(Node.jsは、ESモジュールにファイル拡張子.mjs
を優先します)を見つけるために、Node.jsはnode_module
チェーンをたどり、次の場所を検索します。
/home/jane/proj/node_modules/the-package/the-module.mjs
/home/jane/node_modules/the-package/the-module.mjs
/home/node_modules/the-package/the-module.mjs
node_modules
ディレクトリにインストールされたモジュールを見つけることは、Node.jsでのみサポートされています。では、なぜnpmを使用してブラウザ用のライブラリもインストールできるのでしょうか?
これは、オンラインにデプロイする前にコードをコンパイルおよび最適化するwebpackなどのバンドルツールを介して有効になります。このコンパイルプロセス中に、npmパッケージのコードはブラウザで動作するように調整されます。
モジュールファイルの名前と、それらがインポートされる変数には、確立されたベストプラクティスはありません。
この章では、次の命名スタイルを使用しています。
モジュールファイルの名前は、ダッシュケースで小文字で始まります。
./my-module.mjs
./some-func.mjs
名前空間インポートの名前は、小文字でキャメルケースになります。
import * as myModule from './my-module.mjs';
デフォルトインポートの名前は、小文字でキャメルケースになります。
import someFunc from './some-func.mjs';
このスタイルの背後にある理論的根拠は何ですか?
npmはパッケージ名に大文字を使用することを許可していません(出典)。したがって、「ローカル」ファイルの名前がnpmパッケージの名前と一致するように、キャメルケースを避けています。小文字のみを使用すると、大文字と小文字を区別するファイルシステムと区別しないファイルシステムとの間の競合も最小限に抑えられます。前者は名前が同じ文字で構成されているが、大文字と小文字が異なるファイルを区別します。後者は区別しません。
ダッシュケースのファイル名をキャメルケースのJavaScript変数名に変換するための明確なルールがあります。名前空間インポートの命名方法により、これらのルールは名前空間インポートとデフォルトインポートの両方に適用されます。
また、アンダースコアケースのモジュールファイル名も気に入っています。これらの名前を名前空間インポートに(変換なしで)直接使用できるからです。
import * as my_module from './my_module.mjs';
しかし、そのスタイルはデフォルトインポートには適していません。名前空間オブジェクトにはアンダースコアケースが好きですが、関数などには適切な選択ではありません。
モジュール指定子は、モジュールを識別する文字列です。ブラウザとNode.jsでは動作が若干異なります。違いを見る前に、モジュール指定子のさまざまなカテゴリについて学ぶ必要があります。
ESモジュールでは、次のカテゴリの指定子を区別します。これらのカテゴリは、CommonJSモジュールに由来しています。
相対パス:ドットで始まります。例
'./some/other/module.mjs'
'../../lib/counter.mjs'
絶対パス:スラッシュで始まります。例
'/home/jane/file-tools.mjs'
URL:プロトコルが含まれます(技術的には、パスもURLです)。例
'https://example.com/some-module.mjs'
'file:///home/john/tmp/main.mjs'
ベアパス:ドット、スラッシュ、またはプロトコルで始まらず、拡張子のない単一のファイル名で構成されます。例
'lodash'
'the-package'
ディープインポートパス:ベアパスで始まり、少なくとも1つのスラッシュがあります。例
'the-package/dist/the-module.mjs'
ブラウザはモジュール指定子を次のように処理します
text/javascript
で提供されている限り、重要ではありません。バンドルツール(webpackなど、モジュールをより少ないファイルに結合するもの)は、ブラウザよりも指定子に厳密でないことが多いことに注意してください。これらは(実行時ではなく)ビルド/コンパイル時に動作し、ファイルシステムをトラバースしてファイルを検索できるためです。
Node.jsはモジュール指定子を次のように処理します
相対パスは、Webブラウザーでの場合と同じように、現在のモジュールのパスを基準に解決されます。
現在、絶対パスはサポートされていません。回避策として、file:///
で始まるURLを使用できます。このようなURLは、url.pathToFileURL()
を使用して作成できます。
URL指定子のプロトコルとしてサポートされているのはfile:
のみです。
ベアパスはパッケージ名として解釈され、最も近いnode_modules
ディレクトリを基準に解決されます。どのモジュールをロードする必要があるかは、パッケージのpackage.json
のプロパティ"main"
を参照することで決定されます(CommonJSと同様)。
ディープインポートパスも、最も近いnode_modules
ディレクトリを基準に解決されます。それらにはファイル名が含まれているため、どのモジュールが意味されているかは常に明確です。
ベアパスを除くすべての指定子は、実際のファイルを参照する必要があります。つまり、ESMは次のCommonJS機能をサポートしていません
CommonJSは、欠落しているファイル名拡張子を自動的に追加します。
CommonJSは、"main"
プロパティを持つdir/package.json
がある場合、ディレクトリdir
をインポートできます。
CommonJSは、モジュールdir/index.js
がある場合、ディレクトリdir
をインポートできます。
すべての組み込みNode.jsモジュールは、ベアパスを介して使用でき、名前付きのESMエクスポートがあります。たとえば
import * as assert from 'assert/strict';
import * as path from 'path';
.equal(
assert.join('a/b/c', '../d'), 'a/b/d'); path
Node.jsは、次のデフォルトのファイル名拡張子をサポートしています
.mjs
.cjs
ファイル名拡張子.js
は、ESMまたはCommonJSのいずれかを表します。どちらであるかは、「最も近い」package.json
(現在のディレクトリ、親ディレクトリなど)を介して構成されます。この方法でのpackage.json
の使用は、パッケージとは独立しています。
そのpackage.json
には、2つの設定を持つプロパティ"type"
があります
"commonjs"
(デフォルト):拡張子.js
を持つファイルまたは拡張子のないファイルは、CommonJSモジュールとして解釈されます。
"module"
:拡張子.js
を持つファイルまたは拡張子のないファイルは、ESMモジュールとして解釈されます。
Node.jsによって実行されるすべてのソースコードがファイルから来ているわけではありません。stdin、--eval
、および--print
を介してコードを送信することもできます。コマンドラインオプション--input-type
を使用すると、そのようなコードをどのように解釈するかを指定できます。
--input-type=commonjs
--input-type=module
import.meta
– 現在のモジュールのメタデータ [ES2020]オブジェクトimport.meta
は、現在のモジュールのメタデータを保持します。
import.meta.url
import.meta
の最も重要なプロパティは、現在のモジュールのファイルのURLを含む文字列である.url
です。たとえば
'https://example.com/code/main.mjs'
import.meta.url
とクラスURL
クラスURL
は、ブラウザーとNode.jsでグローバル変数として使用できます。Node.jsドキュメントでそのすべての機能を確認できます。import.meta.url
を使用する場合、そのコンストラクターは特に役立ちます
new URL(input: string, base?: string|URL)
パラメーターinput
には、解析されるURLが含まれています。2番目のパラメーターbase
が提供されている場合、相対パスにすることができます。
言い換えれば、このコンストラクターを使用すると、ベースURLに対して相対パスを解決できます。
> new URL('other.mjs', 'https://example.com/code/main.mjs').href'https://example.com/code/other.mjs'
> new URL('../other.mjs', 'https://example.com/code/main.mjs').href'https://example.com/other.mjs'
これが、現在のモジュールの隣にあるファイルdata.txt
を指すURL
インスタンスを取得する方法です
const urlOfData = new URL('data.txt', import.meta.url);
import.meta.url
Node.jsでは、import.meta.url
は常にfile:
URLを含む文字列です。たとえば
'file:///Users/rauschma/my-module.mjs'
多くのNode.jsファイルシステム操作は、パスを含む文字列またはURL
のインスタンスを受け入れます。これにより、現在のモジュールの兄弟ファイルdata.txt
を読み取ることができます
import * as fs from 'fs';
function readData() {
// data.txt sits next to current module
const urlOfData = new URL('data.txt', import.meta.url);
return fs.readFileSync(urlOfData, {encoding: 'UTF-8'});
}
fs
とURLモジュールfs
のほとんどの関数では、次の方法でファイルを参照できます
Buffer
のインスタンス。URL
(プロトコルfile:
)のインスタンス。このトピックの詳細については、Node.js APIドキュメントを参照してください。
file:
URLとパス間の変換Node.jsモジュールurl
には、file:
URLとパスを変換するための2つの関数があります
fileURLToPath(url: URL|string): string
file:
URLをパスに変換します。pathToFileURL(path: string): URL
file:
URLに変換します。ローカルファイルシステムで使用できるパスが必要な場合、URL
インスタンスのプロパティ.pathname
は常に機能するとは限りません
.equal(
assertnew URL('file:///tmp/with%20space.txt').pathname,
'/tmp/with%20space.txt');
したがって、fileURLToPath()
を使用することをお勧めします
import * as url from 'url';
.equal(
assert.fileURLToPath('file:///tmp/with%20space.txt'),
url'/tmp/with space.txt'); // result on Unix
同様に、pathToFileURL()
は、絶対パスに'file://'
を追加するだけではありません。
import()
を介したモジュールの動的ロード [ES2020] (高度)
import()
演算子はPromiseを使用します
Promiseは、非同期的に(つまり、すぐにではない)計算される結果を処理するための手法です。これらは、§40「非同期プログラミングのためのPromise [ES6]」で説明されています。それらを理解するまで、このセクションの読み取りを延期するのが理にかなっているかもしれません。
import
ステートメントの制限これまでのところ、モジュールをインポートする唯一の方法はimport
ステートメントを使用することでした。そのステートメントにはいくつかの制限があります
if
ステートメント内では何かをインポートすることはできません。import()
演算子による動的インポートimport()
演算子には、import
ステートメントの制限はありません。次のようになります
import(moduleSpecifierStr)
.then((namespaceObject) => {
console.log(namespaceObject.namedExport);
; })
この演算子は関数のように使用され、モジュール指定子を含む文字列を受け取り、名前空間オブジェクトに解決されるPromiseを返します。そのオブジェクトのプロパティは、インポートされたモジュールのエクスポートです。
import()
は、await
を介して使用するとさらに便利です
const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);
await
はモジュールの最上位レベルで使用できることに注意してください(次のセクションを参照)。
import()
を使用する例を見てみましょう。
次のファイルを考えてください
lib/my-math.mjs
main1.mjs
main2.mjs
モジュールmy-math.mjs
はすでに見てきました
// Not exported, private to module
function times(a, b) {
return a * b;
}export function square(x) {
return times(x, x);
}export const LIGHTSPEED = 299792458;
import()
を使用して、このモジュールをオンデマンドでロードできます
// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';
function mathOnDemand() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.LIGHTSPEED;
.equal(result, 299792458);
assertreturn result;
;
})
}
mathOnDemand()
.then((result) => {
.equal(result, 299792458);
assert; })
このコードには、import
ステートメントでは実行できない2つのことがあります
次に、main1.mjs
と同じ機能を実装しますが、非同期関数またはasync/awaitと呼ばれる機能を使用します。これにより、Promiseのより優れた構文が提供されます。
// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';
async function mathOnDemand() {
const myMath = await import(moduleSpecifier);
const result = myMath.LIGHTSPEED;
.equal(result, 299792458);
assertreturn result;
}
なぜ
import()
は関数ではなく演算子なのですか?
import()
は関数のように見えますが、関数として実装することはできませんでした
import()
が関数である場合、この情報(たとえば、パラメーターを介して)を明示的に渡す必要がありました。import()
のユースケースWebアプリの一部の機能は、起動時に存在する必要はなく、必要に応じてロードできます。その際、import()
が役立ちます。なぜなら、そのような機能をモジュールに入れることができるからです。例えば、
.addEventListener('click', event => {
buttonimport('./dialogBox.mjs')
.then(dialogBox => {
.open();
dialogBox
}).catch(error => {
/* Error handling */
}); })
条件が真であるかどうかに応じてモジュールをロードしたい場合があります。例えば、ポリフィルを含むモジュールは、レガシープラットフォームで新しい機能を利用可能にするものです。
if (isLegacyPlatform()) {
import('./my-polyfill.mjs')
.then(···);
}
国際化などのアプリケーションでは、モジュール指定子を動的に計算できると便利です。
import(`messages_${getLocale()}.mjs`)
.then(···);
await
[ES2022] (高度)
await
は非同期関数の機能です
await
については、§41「非同期関数」で説明しています。非同期関数を理解するまで、このセクションの読解を後回しにするのも良いでしょう。
モジュールのトップレベルで await
演算子を使用できます。その場合、モジュールは非同期になり、異なる動作をします。ありがたいことに、これは言語によって透過的に処理されるため、プログラマーとして通常はそれを意識することはありません。
await
のユースケースモジュールのトップレベルで await
演算子を使用する理由はなんでしょうか?これにより、非同期でロードされたデータでモジュールを初期化できます。次の3つのサブセクションでは、それが役立つ3つの例を示します。
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)
console.log(messages.welcome);
A行では、モジュールを動的にインポートしています。トップレベルの await
のおかげで、通常の静的なインポートを使用するのとほとんど同じくらい便利です。
let lodash;
try {
= await import('https://primary.example.com/lodash');
lodash catch {
} = await import('https://secondary.example.com/lodash');
lodash }
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
; ])
Promise.any()
により、変数 resource
は、最初にダウンロードが完了したリソースで初期化されます。
await
はどのように動作するのか?次の2つのファイルを考えてみましょう。
first.mjs
:
const response = await fetch('http://example.com/first.txt');
export const first = await response.text();
main.mjs
:
import {first} from './first.mjs';
import {second} from './second.mjs';
.equal(first, 'First!');
assert.equal(second, 'Second!'); assert
どちらも、おおよそ次のコードと同等です
first.mjs
:
export let first;
export const promise = (async () => { // (A)
const response = await fetch('http://example.com/first.txt');
= await response.text();
first ; })()
main.mjs
:
import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
await Promise.all([firstPromise, secondPromise]); // (C)
.equal(first, 'First content!');
assert.equal(second, 'Second content!');
assert; })()
モジュールが非同期になるのは、次の場合です
await
を直接使用している場合 (first.mjs
)。main.mjs
)。各非同期モジュールは、本体が実行された後に完了するPromiseをエクスポートします(A行とB行)。その時点で、そのモジュールのエクスポートに安全にアクセスできます。
(2)の場合、インポートするモジュールは、すべてのインポートされた非同期モジュールのPromiseが完了するまで待機してから、本体に入ります(C行)。同期モジュールは通常どおり処理されます。
await
で拒否された場合や、同期的な例外は、非同期関数と同じように処理されます。
await
の長所と短所トップレベルの await
の2つの最も重要な利点は次のとおりです
短所としては、トップレベルの await
は、インポートするモジュールの初期化を遅らせます。したがって、控えめに使用するのが最適です。時間がかかる非同期タスクは、後で必要に応じて実行する方が良いでしょう。
ただし、トップレベルの await
を使用していないモジュールでさえ、インポートする側をブロックする可能性があります(例えば、トップレベルでの無限ループなど)。したがって、ブロックすること自体が反対の理由にはなりません。
バックエンドにもポリフィルがあります
このセクションは、フロントエンド開発とWebブラウザーに関するものですが、同様の考え方はバックエンド開発にも当てはまります。
ポリフィルは、JavaScriptでWebアプリケーションを開発する際に直面する課題を解決するのに役立ちます。
Webプラットフォームの機能Xがあると仮定します
Webアプリケーションが起動するたびに、まず、すべての場所で利用できない可能性のある機能に対するすべてのポリフィルを実行する必要があります。その後、それらの機能がネイティブで利用可能であることが保証されます。
クイズ
クイズアプリを参照してください。