モジュール — import / export

ファイルごとに値を公開する export と、それを読み込む import を扱います。名前付きと default の違い、ブラウザの type="module"、Node.js の ESM と CommonJS の見分け方を学べます。

注文画面のコードを 1 つのファイルに書き足すと、税込計算・カート・会計の処理が数百行に混ざり、関数を探すのに時間がかかります。ファイルを分けて HTML に <script>(JavaScript のファイルを読み込むタグ)を並べても、関数の置き場所は使う側から読み取れません。

この記事では、ファイルの間で値を受け渡す exportimport を扱います。

コンソールでは import が使えない

練習問題のコンソールは、コードを 1 つの関数の中に入れて実行します。importexport はファイルの一番外側にしか書けないので、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'
import できる名前とできない名前
import { withTax,formatYen }price.js にexport がある2 つの関数を読み込める5280 円と表示されるimport{ TAX_RATE }TAX_RATE にexport が無いSyntaxErrorで止まるcart-page.js はどの行も動かない
上の段の 2 つには export があり、下の段の TAX_RATE にはありません。import に書けるのは export した名前だけです

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'
波括弧の有無で探す export が変わる
import Cartfrom "./cart.js"波括弧なしdefault を探すexport defaultclass Cart があるnew Cart()が使えるimport { Cart }from "./cart.js"波括弧あり名前付きを探す名前付きの Cartは export していないSyntaxErrorで止まる
下の段は、名前付きの Cart を探して止まります。波括弧が無い import は、名前ではなく default を受け取ります

エラー文の末尾の 'Cart' は、探して見つからなかった名前付き export の名前です。default の Cart があることは書かれないので、波括弧の有無から見直します。下の表は、この記事で使う import の書き方をまとめたものです。

import の書き方受け取るもの受け取る側の名前
import { withTax } from …名前付き exportexport と同じ名前
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 円
checkout.js から始まる依存グラフ
index.htmltype="module"checkout.js③ 最後に実行cart.js② 次に実行price.js① 最初に 1 回だけ
index.html 以外の矢印は import の向きです。実行は price.js から始まり、矢印が 2 本届いても 1 回だけです

モジュールは、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 を使う標準の形式)に対し、CommonJSrequire で読み込み、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
"type": "module" が効くファイル
shop フォルダ — package.json に "type": "module"
  • 拡張子が .js のファイルは ESM として読まれる
checkout.js — ESM
  • import stock from "./stock.cjs" で読める
  • require を書くと ReferenceError
拡張子 .cjs — CommonJS
  • stock.cjsmodule.exports で公開する
  • report.cjsrequire で読み込める
"type": "module" は、フォルダの中の .js を ESM にします。.cjs は、同じフォルダでも CommonJS のままです

import stock は波括弧が無いので、default export と同じ受け取り方で、module.exports のオブジェクトが入ります。1 つのファイルでは importrequire を混ぜず、どちらか一方で書きます。下の表は、2 つの形式の書き方と、形式が決まる規則です。

項目ESMCommonJS
読み込む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" で読み込めるそのままでは読み込めない
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1default export の Cartimport { Cart } from "./cart.js" で読むと?

Q22 か所から import される price.js で、先頭の console.log は何回出ますか?

Q3"type": "module" の下の checkout.js で require を実行すると?