Q1default export の Cart を import { Cart } from "./cart.js" で読むと?
モジュール — import / export
ファイルごとに値を公開する export と、それを読み込む import を扱います。名前付きと default の違い、ブラウザの type="module"、Node.js の ESM と CommonJS の見分け方を学べます。
注文画面のコードを 1 つのファイルに書き足すと、税込計算・カート・会計の処理が数百行に混ざり、関数を探すのに時間がかかります。ファイルを分けて HTML に <script>(JavaScript のファイルを読み込むタグ)を並べても、関数の置き場所は使う側から読み取れません。
この記事では、ファイルの間で値を受け渡す export と import を扱います。
コンソールでは import が使えない
練習問題のコンソールは、コードを 1 つの関数の中に入れて実行します。import と export はファイルの一番外側にしか書けないので、SyntaxError になります。この記事のコードは、ファイルを分けて Node.js やブラウザで動かす例で、結果は行末のコメントに書いてあります。
関数をファイル間で共有する — 名前付き export
カート画面の cart-page.js と会計画面の checkout.js で、同じ税込計算を使いたいとします。関数を両方のファイルに書き写すと、税率が変わったときに 2 か所を直すことになり、片方を直し忘れると画面ごとに金額がずれます。
モジュール(import と export で値を受け渡す JavaScript ファイル。読み込ませ方は後の章で扱います)で宣言した変数や関数は、ほかのファイルから見えません。宣言に export を付けて公開するのが名前付き export で、使う側は import { withTax } from "./price.js"; と同じ名前を波括弧に書きます。
// ---- price.js ----
const TAX_RATE = 0.1; // export なし: price.js の中だけで使う
export function withTax(price) { // export あり: ほかのファイルから読める
return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
return `${price} 円`;
}
// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js"; // 使う名前を波括弧に並べる
console.log(formatYen(withTax(4800))); // 5280 円
// export していない名前を import に書いた場合
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
TAX_RATE を読むのは price.js の中の withTax なので、cart-page.js は税率を知らなくても税込の金額を受け取れます。税率が変わっても直すのは price.js の TAX_RATE の 1 行だけで、import したファイルすべてに反映されます。
class を 1 つだけ公開する — default export
カートの class を cart.js に置くと、そのファイルが公開する主なものは Cart の 1 つです。ほかの人のコードでよく見る import Cart from "./cart.js"; に、前の章の書き方どおり波括弧を付けると SyntaxError で読み込めません。
default export(1 ファイルに 1 つだけ置ける、名前を指定せずに受け取れる公開の値)は export default class Cart { ... } と書きます。読み込む側は波括弧を付けずに書き、Cart の位置には好きな名前を付けられます。
// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20; // 名前付き export も同じファイルに置ける
export default class Cart { // default export は 1 ファイルに 1 つ
#prices = [];
add(price) { this.#prices.push(price); }
get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}
// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js"; // default は波括弧の外、名前付きは中
import ShoppingCart from "./cart.js"; // default は別の名前でも受け取れる
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS); // 5280 20
console.log(Cart === ShoppingCart); // true(同じ class を受け取っている)
// 波括弧を付けると、名前付きの Cart を探す
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
エラー文の末尾の 'Cart' は、探して見つからなかった名前付き export の名前です。default の Cart があることは書かれないので、波括弧の有無から見直します。下の表は、この記事で使う import の書き方をまとめたものです。
| import の書き方 | 受け取るもの | 受け取る側の名前 |
|---|---|---|
| import { withTax } from … | 名前付き export | export と同じ名前 |
| import Cart from … | default export | 好きな名前を付けられる |
| import Cart, { MAX_ITEMS } from … | default と名前付き | Cart と MAX_ITEMS(default を先に書く) |
ブラウザで import をたどらせる — type="module"
会計画面の HTML から checkout.js を読み込むとします。src にファイルを指定した <script src="./checkout.js"></script> だけでは、Chrome は 1 行目の import で SyntaxError: Cannot use import statement outside a module を出し、処理が 1 行も動きません。
type="module"(読み込むファイルをモジュールとして実行させる <script> の指定)を付けると、ブラウザは入口のファイルの import をたどって、残りのファイルも取得します。この import のつながりを 依存グラフ(どのファイルがどのファイルを import しているかの関係)と呼びます。
// ---- index.html: 入口の checkout.js だけを書く ----
// <script type="module" src="./checkout.js"></script>
// ---- price.js ----
console.log("price.js を実行"); // 2 か所から import されても 1 回だけ出る
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }
// ---- cart.js ----
import { withTax } from "./price.js";
console.log("cart.js を実行");
export const cartTotal = withTax(4800);
// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js"; // cart.js と同じ price.js を読む
console.log(`支払い: ${cartTotal + withTax(500)} 円`);
// コンソールに出る順: price.js を実行 → cart.js を実行 → 支払い: 5830 円
モジュールは、import しているファイルより先に、import されたファイルを実行します。入口の checkout.js は、import した cart.js と price.js が終わるまで待つので最後です。順番は import で決まるので、HTML にファイルを並べる順番を考える必要はありません。
file:// で開いた HTML では動かない
HTML をダブルクリックし file:// で開くと、Chrome は同じフォルダでも別のオリジン(取得元)とみなし、CORS(異なるオリジンからの読み込みを制限する仕組み)で type="module" の読み込みを止めます。VS Code の拡張機能 Live Server などで Web サーバーを起動して開きます。
Node.js で形式を見分ける — ESM と CommonJS
Node.js 向けのコードでは、import の代わりに const { reserve } = require("./stock"); と書かれたファイルに出会います。これをまねて、import で書いたファイルに require を足すと、実行時に ReferenceError で止まります。
ESM(ES Modules の略。import と export を使う標準の形式)に対し、CommonJS(require で読み込み、module.exports で公開する Node.js 独自の形式)があります。Node.js は、拡張子と package.json(プロジェクトの設定ファイル)の "type" でどちらかを決めます。
// ---- shop/package.json(Node.js のプロジェクトの設定を書くファイル) ----
// { "type": "module" }
// ---- shop/stock.cjs: 拡張子が .cjs なので CommonJS として読まれる ----
function reserve(count) { return `${count} 点を確保`; }
module.exports = { reserve: reserve, LIMIT: 3 }; // 公開する値をまとめて入れる
// ---- shop/report.cjs: CommonJS のファイルは require で読む ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT); // 2 点を確保 3
// ---- shop/checkout.js: "type": "module" なので ESM として読まれる ----
import stock from "./stock.cjs"; // 波括弧なしで、module.exports の値を受け取る
console.log(stock.reserve(1)); // 1 点を確保
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
- 拡張子が
.jsのファイルは ESM として読まれる
import stock from "./stock.cjs"で読めるrequireを書くとReferenceError
stock.cjs—module.exportsで公開するreport.cjs—requireで読み込める
import stock は波括弧が無いので、default export と同じ受け取り方で、module.exports のオブジェクトが入ります。1 つのファイルでは import と require を混ぜず、どちらか一方で書きます。下の表は、2 つの形式の書き方と、形式が決まる規則です。
| 項目 | ESM | CommonJS |
|---|---|---|
| 読み込む | import { reserve } from "./stock.js" | const { reserve } = require("./stock.js") |
| 公開する | export function reserve() { ... } | module.exports = { reserve: reserve } |
| 拡張子 | Node.js とブラウザでは省けない("./stock" では見つからない) | 省ける("./stock" で stock.js を探す) |
| その形式で読まれるファイル | .mjs と、"type": "module" の下の .js | .cjs と、"type" が "commonjs" か未指定の下の .js(未指定で import を含む .js は ESM として読み直される) |
| ブラウザ | type="module" で読み込める | そのままでは読み込めない |
理解度チェック
まずは1問ずつ答えてみましょう。
Q22 か所から import される price.js で、先頭の console.log は何回出ますか?
Q3"type": "module" の下の checkout.js で require を実行すると?