JavaScriptのimportmapを味方に付けよう



# 概要

今日、ご紹介するのは、JavaScriptの`importmap`です。

`importmap`は、ブラウザで実行する`import`構文によって読み込まれるパッケージのURLを指定することができるようにします。

つまり、`import`構文にエイリアスをアサインすることが可能になります。

すなわち、ブラウザで`import React from "react";`というふうに書けるようにできるのです。

これを実現するためには、パッケージをダウンロードするためのURLと、`import`構文で指定するエイリアス名を関連づける必要があります。

そのやり方を説明していきましょう！

# importmapのJSONを書く

どこか任意なところに、新しいダイレクトリを作って、`index.html`というファイルを作成してください。

そこに、以下のようなHTMLテンプレートを使います。

```html:import-maps-demo/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>

</body>

</html>
```

次、`<head>`要素の中に`<script>`要素を追加します。

しかし！これは普通の`<script>`要素ではありません。

これは、`importmap`のスクリプト要素だと言って、読み込まれるJavaScriptパッケージとこのHTMLドキュメントで使う`import`構文のエイリアスを関連づけるためのものです。

中身は、JSON形式で記入します。

```html
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type="importmap">
    {
      "imports": {
        "lit": "https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js",
        "react": "https://unpkg.com/react@18/umd/react.development.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js"
      }
    }
  </script>
</head>
```

JSONの`"imports"`の中に、このドキュメントで使いたいパッケージのURLとエイリアス名を記入します。

上記のように複数のURLを同時に指定することができます。

また、指定しても、すぐにはダウンロードされません。実際にJavaScriptの中で必要になったときに初めてダウンロードされます。あくまでも関連づけるための`importmap`なのです。

# スクリプト要素の中で使用する場合

`importmap`を一度指定すると、ドキュメント全体でエイリアスを使った`import`構文が書けるようになります。

ただし、**スクリプト要素の<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-type">type属性</a>は`module`と指定する必要があります**。

試しに、[Lit](https://lit.dev/docs/components/overview/)を使って、簡単な部品を`<script type="module">`の中で書いてみましょう。

```html
<body>
  <simple-greeting name="Austin"></simple-greeting>
  
  <script type="module">
    import { LitElement, css, html } from 'lit';

    export class SimpleGreeting extends LitElement {
      static properties = {
        name: {},
      };
      static styles = css`
      :host {
        color: blue;
      }`;

      constructor() {
        super();
        this.name = 'World';
      }

      #handleClick = () => (this.name = "World");

      render() {
        return html`<p @click=${this.#handleClick}>Hello, ${this.name}!</p>`;
      }
    }
    customElements.define('simple-greeting', SimpleGreeting);
  </script>
</body>
```

### 結果

上記の`index.html`のローカルファイルをChromeで開いて試してみた結果は以下の通りです。

![ezgif.com-gif-maker (14).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/7f3dcaac-f1e9-ce23-fbdc-d0c2572b2720.gif)

無事にダウンロードしてLitを読み込んでくれましたね！

留意していただきたいのは、
 - `lit-core.min.js`しかダウンロードされていないこと
 - ローカルで開いているけれど、問題なくダウンロードできていること
 - スクリプト要素では`lit`というエイリアスで`import`していること


# JavaScriptファイルの中で使う場合

次は、[src属性](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-src)を使って`import`構文を使う場合を紹介します。

進む前に、ローカルでファイルを開くではなく、HTTPを通してファイルを開けるようにしないといけないので簡単な`Node.js`サーバーを書きます。

## 簡単なNode.jsサーバーでローカルHTTPホスティングをする

ChromeはHTTPプロトコルでないと、ESModuleによるインポートができないようになっているので、簡単なサーバーを書きます。

```javascript:server.js
const http = require("http");
const fs = require("fs");
const path = require("path");

const server = http.createServer((req, res) => {
  const { url } = req;
  if (url.includes(".js")) {
    res.writeHead(200, { "Content-Type": "text/javascript" });
    const jsFilePath = path.resolve(__dirname, "." + url);
    const buffer = fs.readFileSync(jsFilePath);
    res.write(buffer);
    return res.end();
  }
  res.writeHead(200, { "Content-Type": "text/html" });
  const htmlFilePath = path.resolve(__dirname, "./index.html");
  const buffer = fs.readFileSync(htmlFilePath);
  res.write(buffer);
  return res.end();
});

server.listen(3000);
```

これを以下のように実行します。なお、[Node.js](https://nodejs.org/en/download/)をローカルにインストールする必要があります。

```
node server.js
```

ここまでできれば問題なく進めます。

## JavaScriptファイルで`import`構文を書いてみる

まず、`index.html`では以下のように外部JavaScriptファイルを指定するスクリプト要素を追加します。

```html:index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type="importmap">
    {
      "imports": {
        "lit": "https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js",
        "react": "https://unpkg.com/react@18/umd/react.development.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js"
      }
    }
  </script>
  <script src="./simple-greeting.js" type="module" defer></script>
</head>

<body>
  <simple-greeting name="Austin"></simple-greeting>
</body>

</html>
```

そして、同じダイレクトリに`simple-greeting.js`というファイルを作ります。

そこに、以下のロジックを入れます。

```javascript:simple-greeting.js
import { LitElement, css, html } from "lit";

export class SimpleGreeting extends LitElement {
  static properties = {
    name: {},
  };
  static styles = css`
    :host {
      color: blue;
    }
  `;

  constructor() {
    super();
    this.name = "World";
  }

  #handleClick = () => (this.name = "World");

  render() {
    return html`<p @click=${this.#handleClick}>Hello, ${this.name}!</p>`;
  }
}
customElements.define("simple-greeting", SimpleGreeting);
```

### 結果

`http://localhost:3000`を開いてみると、先ほどと全く同じ結果が出ますが、`simple-greeting.js`もインポートされます。

![ezgif.com-gif-maker (15).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/26c787d6-4328-d664-0ee6-083323bcd39f.gif)

このようにすると、最近巷でよく聞く<q>バンドル化不要説</q>が少し有力に感じてきますね。

# 相対的パスを使って`public`のパッケージをエイリアス化する

最後に、この`importmap`を使って、スクリプト要素でサーバーの`public`ダイレクトリに置かれているJavaScriptファイルのURLを指定してエイリアスを付ける方法を紹介したいです。

まず、`index.html`の`importmap`を以下のように更新します。

```html:index.html
<head>
  <script type="importmap">
    {
      "imports": {
        "lit": "https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js",
        "react": "https://unpkg.com/react@18/umd/react.development.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js",
        "simple-greeting": "./simple-greeting.js"
      }
    }
  </script>
</head>
```

そして`simple-greeting.js`では以下のようにロジックを変更します。

```javascript:simple-greeting.js
import { LitElement, css, html } from "lit";

export const tagName = "simple-greeting";

class SimpleGreeting extends LitElement {
  static properties = {
    name: {},
  };
  static styles = css`
    :host {
      color: blue;
    }
  `;

  constructor() {
    super();
    this.name = "World";
  }

  #handleClick = () => (this.name = "World");

  render() {
    return html`<p @click=${this.#handleClick}>Hello, ${this.name}!</p>`;
  }
}

export default SimpleGreeting;
```

また、`<body>`では以下のようにスクリプト要素を追加します。

```html:index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script type="importmap">
    {
      "imports": {
        "lit": "https://cdn.jsdelivr.net/gh/lit/dist@2/core/lit-core.min.js",
        "react": "https://unpkg.com/react@18/umd/react.development.js",
        "react-dom": "https://unpkg.com/react-dom@18/umd/react-dom.development.js",
        "simple-greeting": "./simple-greeting.js"
      }
    }
  </script>
</head>

<body>
  <simple-greeting name="Austin"></simple-greeting>
  <script type="module">
    import SimpleGreeting, { tagName } from "simple-greeting";
    window.customElements.define(tagName, SimpleGreeting)
  </script>
</body>

</html>
```

### 結果

先ほどの例と全く同じようにできます。

![ezgif.com-gif-maker (16).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/38414fa2-8019-2ebe-7fc7-7dbd6ceeaa8d.gif)

# まとめ

この機能って最高に面白くないですか？？

正直、筆者が最近発掘してきたブラウザ機能の中でこれは結構画期的に感じて好きです。

バンドル化が不要なプロジェクトにおいて、将来、こういう手法で外部パッケージの管理が一般的になるのかもしれません。

より詳しい情報を知りたい方は以下のREADMEをご覧ください。

https://github.com/WICG/import-maps#multiple-import-map-support

## 注意点

**UPDATE 2024/1/11**

**全てのエバーグリーンブラウザにシムなしでサポートされるようになった**


https://caniuse.com/?search=importmap

Shimを使えば94%のブラウザをカバーできます。

https://github.com/guybedford/es-module-shims