Chapter 09

モジュールパターン

Shinya Fujino
Shinya Fujino
2022.01.07に更新

コードを再利用可能な小さな部品へと分割する


モジュールパターン

アプリケーションとコードベースが大きくなるにつれて、コードを保守しやすく分割しておくことがより重要になります。モジュールパターン (module pattern) は、コードを再利用可能な小さな部品へと分割することを可能にします。

再利用可能な小さな部品へとコードを分割できることに加え、モジュールはファイル内の特定の値を非公開にすることも可能とします。モジュール内の変数の宣言は、デフォルトではそのモジュールにスコープ (カプセル化) されます。ある値を明示的にエクスポートしなければ、その値はモジュールの外では利用できません。これにより、コードベースの他の部分で宣言された値がグローバルスコープで利用できなくなるため、名前が衝突する危険性を減らすことができます。


ES2015 のモジュール

ES2015 では、組み込みの JavaScript モジュールが導入されました。モジュールとは、JavaScript のコードを含むファイルをいい、通常のスクリプトと比較して動作に多少の違いがあります。

数学の関数を含む math.js というモジュールの例を見てみましょう。

math.js
function add(x, y) {
  return x + y;
}
function multiply(x) {
  return x * 2;
}
function subtract(x, y) {
  return x - y;
}
function square(x) {
  return x * x;
}
index.js
console.log(add(2, 3));
console.log(multiply(2));
console.log(subtract(2, 3));
console.log(square(2));

簡単な数学的ロジックを含む math.js ファイルがあります。その内部には、ユーザーが足し算、掛け算、引き算、そして渡した値の二乗を求めるための関数があります。

しかし、ここではこれらの関数を math.js ファイル内で使用したいわけではなく、index.js ファイル内で参照できるようにしたいのです!現在、index.js ファイル内ではエラーが発生しています。index.js ファイル内には addsubtractmultiplysquare という関数が存在しないのです。つまり、index.js ファイル内に存在しない関数を参照しようとしているのです。

math.js の関数を他のファイルから利用できるようにするには、関数をエクスポートする必要があります。モジュールからコードをエクスポートするには、export キーワードを使用します。関数をエクスポートする 1 つの方法は、名前付きエクスポート (named export) を使用することです。そのためには、外部に公開したい部分の前に export キーワードを追加します。ここでは、index.js が 4 つの関数すべてにアクセスできる必要があるため、すべての関数の前に export キーワードを追加したいと思います。

math.js
export function add(x, y) {
  return x + y;
}

export function multiply(x) {
  return x * 2;
}

export function subtract(x, y) {
  return x - y;
}

export function square(x) {
  return x * x;
}

これで addmultiplysubtractsquare 関数をエクスポートできるようになりました。しかし、モジュールから値をエクスポートしただけでは、任意のファイルからそれらの値を使用することはできません。モジュールからエクスポートされた値を使えるようにするためには、値を参照するファイルにおいて明示的にインポートする必要があります。

index.js ファイルの先頭で、import キーワードを使用して値をインポートする必要があります。また、どのモジュールからこれらの関数をインポートしたいのかを JavaScript に知らせるために、from キーワードとモジュールへの相対パスも追加する必要があります。

index.js
import { add, multiply, subtract, square } from "./math.js";

index.js ファイルに math.js モジュールから 4 つの関数をインポートできました。それでは、これらの関数が使えるかどうか試してみましょう!

index.js
import { add, multiply, subtract, square } from "./math";

console.log(add(2, 3));
console.log(multiply(2));
console.log(subtract(2, 3));
console.log(square(2));

参照エラーがなくなり、モジュールからエクスポートされた値を使用できるようになっています!

モジュールの大きな利点は、export キーワードを使って明示的にエクスポートした値のみアクセスできることです。export キーワードにより明示的にエクスポートしなかった値は、そのモジュールの中でしか利用できません。

privateValue という、math.js ファイル内でのみ参照可能な値を作ってみましょう。

math.js
const privateValue = "This is a value private to the module!";

export function add(x, y) {
  return x + y;
}

export function multiply(x) {
  return x * 2;
}

export function subtract(x, y) {
  return x - y;
}

export function square(x) {
  return x * x;
}

privateValue の前に export キーワードを追加していないことに注目してください。変数 privateValue をエクスポートしていないため、この値を math.js モジュールの外から参照することはできません!

index.js
import { add, multiply, subtract, square } from "./math.js";

console.log(privateValue);
/* Error: privateValue is not defined */

値をモジュールの外部に非公開とすることで、誤ってグローバルスコープを汚染するリスクを減らすことができます。あなたのモジュールを使っている別の開発者が作った値を、モジュール内の同じ名前をもつプライベートな値により誤って上書きしてしまう心配はありません。つまり、名前の衝突を防ぐことができるのです。


エクスポートされた名前がローカルの値と衝突してしまうことがあります。

index.js
import { add, multiply, subtract, square } from "./math.js";

function add(...args) {
  return args.reduce((acc, cur) => cur + acc);
}
/* Error: add has  already been declared */

function multiply(...args) {
  return args.reduce((acc, cur) => cur * acc);
}
/* Error: multiply has already been declared */

ここでは、index.jsaddmultiply という関数があります。同じ名前の値をインポートしようとすると、addmultiply はすでに宣言されているため、名前の衝突が起きてしまうのです!幸いなことに、as キーワードを使えば、インポートした値の名前を変更することができます。

インポートされた addmultiply 関数の名前を addValuesmultiplyValues に変更してみましょう。

index.js
import {
  add as addValues,
  multiply as multiplyValues,
  subtract,
  square
} from "./math.js";

function add(...args) {
  return args.reduce((acc, cur) => cur + acc);
}

function multiply(...args) {
  return args.reduce((acc, cur) => cur * acc);
}

/* From math.js module */
addValues(7, 8);
multiplyValues(8, 9);
subtract(10, 3);
square(3);

/* From index.js file */
add(8, 9, 2, 10);
multiply(8, 9, 2, 10);

export キーワードだけで定義される名前付きエクスポートの他に、デフォルトエクスポートを使用することもできます。デフォルトエクスポートは、1 つのモジュールに 1 つだけもつことができます。

他の関数は名前付きエクスポートとしたままで、add 関数をデフォルトエクスポートしてみましょう。デフォルト値をエクスポートするには、値の前に export default を付けます。

math.js
export default function add(x, y) {
  return x + y;
}

export function multiply(x) {
  return x * 2;
}

export function subtract(x, y) {
  return x - y;
}

export function square(x) {
  return x * x;
}

名前付きエクスポートとデフォルトエクスポートの違いは、値がモジュールからエクスポートされる方法であり、これにより値をインポートする方法も変わります。

ここまでは、名前付きエクスポートのために、import { module } from 'module' のように、ブラケットを使用しなければなりませんでした。デフォルトエクスポートでは、import module from 'module' のように、ブラケットなしで値をインポートできます。

index.js
import add, { multiply, subtract, square } from "./math.js";

add(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);

デフォルトエクスポートがある場合、モジュールからブラケットなしでインポートされる値は、常にデフォルトエクスポートの値になります。

JavaScript はこの値が常にデフォルトエクスポートされた値であることを知っているため、インポートされたデフォルト値に、それがエクスポートされたときの名前とは異なる名前を付けることができます。add という名前により add 関数をインポートする代わりに、たとえば addValues と呼ぶことができます。

index.js
import addValues, { multiply, subtract, square } from "./math.js";

addValues(7, 8);
multiply(8, 9);
subtract(10, 3);
square(3);

JavaScript はデフォルトエクスポートをインポートしていることを知っているため、add という名前でエクスポートされた関数であっても、好きな名前でインポートすることができるのです。

また、アスタリスク * を使ってインポートしたいモジュールに名前を指定することで、モジュールからエクスポートされるすべての値、つまり、名前付きエクスポートとデフォルトエクスポートをインポートすることもできます。ここでインポートされる値は、エクスポートされたすべての値を含むオブジェクトとなります。たとえば、モジュール全体を math としてインポートしてみます。

index.js
import * as math from "./math.js";

インポートされた値は、math オブジェクトのプロパティとなります。

index.js
import * as math from "./math.js";

math.default(7, 8);
math.multiply(8, 9);
math.subtract(10, 3);
math.square(3);

この場合、モジュールからすべてのエクスポートをインポートしていることになります。このとき、不必要な値をインポートしてしまうことがあるため、注意してください。

* を使用してインポートされるのは、エクスポートされた値だけです。モジュール内のプライベートな値は、明示的にエクスポートしない限り、モジュールをインポートするファイルからは使用できません。


React

React でアプリケーションを作成する際、大量のコンポーネントを扱わなければならないことがよくあります。これらのコンポーネントを 1 つのファイルにまとめて書くのではなく、コンポーネントごとにそれぞれのファイルへと分割することができますが、これは本質的には個々のコンポーネントのモジュールを作成しているのです。

リストリストアイテム入力フィールドボタンを含む簡単な Todo リストを考えます。

index.js
import React from "react";
import { render } from "react-dom";

import { TodoList } from "./components/TodoList";
import "./styles.css";

render(
  <div className="App">
    <TodoList />
  </div>,
  document.getElementById("root")
);
Button.js
import React from "react";
import Button from "@material-ui/core/Button";

const style = {
  root: {
    borderRadius: 3,
    border: 0,
    color: "white",
    margin: "0 20px"
  },
  primary: {
    background: "linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)"
  },
  secondary: {
    background: "linear-gradient(45deg, #2196f3 30%, #21cbf3 90%)"
  }
};

export default function CustomButton(props) {
  return (
    <Button {...props} style={{ ...style.root, ...style[props.color] }}>
      {props.children}
    </Button>
  );
}
Input.js
import React from "react";
import Input from "@material-ui/core/Input";

const style = {
  root: { padding: "5px", backgroundColor: "#434343", color: "#fff" }
};

export default function CustomInput(props, { variant = "standard" }) {
  return (
    <Input
      style={style.root}
      {...props}
      variant={variant}
      placeholder="Type..."
    />
  );
}

コンポーネントを別々のファイルに分割しました:

  • List コンポーネント用の TodoList.js
  • カスタマイズされた Button コンポーネント用の Button.js
  • カスタマイズされた Input コンポーネント用の Input.js

アプリケーション全体を通じて、material-ui ライブラリからインポートされたデフォルトの ButtonInput コンポーネントを使用したくありません。代わりに、それらをカスタマイズしたコンポーネントを使用し、各ファイルの style オブジェクトにおいて定義されるカスタムスタイルを追加したいと思います。これにより、アプリケーションの中で毎回デフォルトの ButtonInput コンポーネントをインポートして、カスタムスタイルを何度も追加するのではなく、デフォルトの ButtonInput コンポーネントを一度だけインポートしてスタイルを追加し、そのカスタムコンポーネントをエクスポートするだけでよくなります。

Button.jsInput.js の両方に style というオブジェクトがあることに注目してください。この値はモジュールにスコープされているため、名前の衝突のリスクを負うことなく、同じ変数名を再利用することができるのです。


ダイナミックインポート

ファイルの先頭でモジュールをインポートする場合、それらはファイルの残りの部分よりも先にロードされます。ところで、ある条件に基づいてモジュールをインポートする必要があるケースも存在します。ダイナミックインポートを使えば、オンデマンドにモジュールをインポートすることができます。

import("module").then(module => {
  module.default();
  module.namedExport();
});

// Or with async/await
(async () => {
  const module = await import("module");
  module.default();
  module.namedExport();
})();

上で使用した math.js の例を動的にインポートしてみましょう。

このモジュールは、ユーザーがボタンをクリックした場合にのみ読み込まれます。

index.js
const button = document.getElementById("btn");

button.addEventListener("click", () => {
  import("./math.js").then((module) => {
    console.log("Add: ", module.add(1, 2));
    console.log("Multiply: ", module.multiply(3, 2));

    const button = document.getElementById("btn");
    button.innerHTML = "Check the console";
  });
});

/*************************** */
/**** Or with async/await ****/
/*************************** */
// button.addEventListener("click", async () => {
//   const module = await import("./math.js");
//   console.log("Add: ", module.add(1, 2));
//   console.log("Multiply: ", module.multiply(3, 2));
// });

モジュールを動的にインポートすることで、ページの読み込み時間を短縮することができます。ユーザーが本当に必要なコードを、必要になってから読み込みパースし、コンパイルすればよいのです。


モジュールパターンにより、コードの中で公開してはいけない部分をカプセル化することができます。これにより、意図しない名前の衝突やグローバルスコープの汚染を防ぐことができ、複数の依存関係や名前空間を扱う際に生じるリスクが軽減されます。ES2015 モジュールをすべての JavaScript ランタイムで使用できるようにするためには、Babel のようなトランスパイラが必要です。