CDN で始める環境構築ゼロの Vue 3 開発

[npm]: https://www.npmjs.com/
[Yarn]: https://yarnpkg.com/
[webpack]: https://webpack.js.org/
[Vite]: https://vitejs.dev/

[jsDelivr]: https://www.jsdelivr.com/
[unpkg]: https://unpkg.com/
[cdnjs]: https://cdnjs.com/

[Vuetify]: https://next.vuetifyjs.com/
[VueUse]: https://vueuse.org/

[Vue Advent Calendar 2022](https://qiita.com/advent-calendar/2022/vue) 8日目の記事です。

## はじめに

この記事では、Vue 3 に使うパッケージ群を CDN から取得することで、パッケージ管理ツール ([npm][], [Yarn][]) やバンドラー/ビルドツール ([webpack][], [Vite][]) を使わずに開発する方法を紹介します。
Vue を CDN から使うだけなら [Vue 公式ページ](https://vuejs.org/guide/quick-start.html#using-vue-from-cdn)で紹介されているため、本記事を読む必要はあまりないので、ここではより実践的な内容として、[Vuetify][] や [VueUse][] といったライブラリと合わせて使う方法や、開発に役立つ Tips を盛り込んで紹介していこうと思います。

### サンプルコードの注意事項

本記事で紹介するコードは説明に必要な部分だけを抽出して書いているため、そのままでは動かない可能性があります。
コードの全貌はサンプルリポジトリを用意していますので、こちらも合わせてご参照ください。

https://github.com/aster-mnch/vue3-cdn-cookbook

## CDN とは

まず CDN について簡単に説明しましょう。
CDN とは、Content Delivery Network の略で、Web コンテンツを高速かつ安定して配信するためのシステムです。
障害が起きると Twitter でよく祭りが起きるあれです。

CDN は自身が作った Web コンテンツの配信にも使われますが、今回は提供されているライブラリを取得する用途で使います。

### CDN から npm パッケージを使う

npm パッケージを配信している CDN サービスはいくつかありますが、この記事では [jsDelivr][] を使って説明します。
CDN から npm パッケージを使う場合は、以下のように `<script src="...">` で指定して呼び出します。

```html
<script src="https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.global.js"></script>
<script>
  console.log(Vue);
</script>
```

1行目の `vue.global.js` 内で `Vue` がグローバルスコープで宣言されるため、2行目以降の `<script>` 内で使うことができます。
リリース時はこれだけでビルドする必要がなく、HTTTP 経由で確認するとブラウザのコンソールに出力されているのが確認できます。
少し前に [Turbopack の実行速度が Vite の10倍](https://vercel.com/blog/turbopack)ということで話題になりましたが、CDN のみで開発する場合のビルド時間は0、つまり **∞倍の開発効率が得られる** わけです。

### CDN から `import` する

ES モジュールのように `import` 構文を使う場合は以下のようになります。

```html
<script type="module">
  import * as Vue from 'https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.js';
  console.log(Vue);
</script>
```

`import` を使うにはそのスクリプトがモジュールであることを宣言しなければならず、`<script>` タグに `type="module"` を指定する必要があります。
より詳細な解説は本筋とは異なるので [JavaScript モジュール - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules) などをご参照ください。

また、ビルドツールのように、`import` 時にパッケージ名だけを指定したい場合は [importmap](https://github.com/WICG/import-maps) を使います。

```html
<script type="importmap">
  {
    "imports": {
      "vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.js"
    }
  }
</script>
<script type="module">
  import * as Vue from 'vue';
  console.log(Vue);
</script>
```

これでだいぶ今風な書き方になってきましたね。
以降の説明では、ES モジュールと `importmap` の組み合わせを使ったコードで解説します。

:::note warn
`importmap` は執筆時点 (2022/12/08) では、動作しないブラウザがあります。
未対応ブラウザでも動作させるには、下の1行目を追加して [es-module-shims](https://github.com/guybedford/es-module-shims) を polyfill する必要があります。

```html
<script async src="https://ga.jspm.io/npm:es-module-shims@1.6.2/dist/es-module-shims.js"></script>
<script type="importmap">
...
```
:::

### :bulb: [Tips] CDN から使うファイルは何を選べばいい?

先の例で CDN から取得しているファイル名が異なっていることにお気づきでしょうか?
基本的に npm パッケージは複数の実行環境に対応できるように、モジュールごとのファイルを生成しているため、CDN から使う場合は自分の目的に合わせてファイルを選択する必要があります。

例えば [jsDelivr で Vue を検索したときのページ](https://www.jsdelivr.com/package/npm/vue)が以下です。
`vue CDN Files` テーブルにいくつかファイル・ディレクトリが並んでいるのが見えます。

![vue_CDN_by_jsDelivr_-_A_CDN_for_npm_and_GitHub.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158991/ec19b1b9-bae3-fb74-19bb-54c3c050e3d2.png)

テーブルの1行目はデフォルトファイルで、URL の指定が `{パッケージ名}(@{version})` までのときに自動的にそのファイルが返ります。
(バージョン指定は省略が可能)

多くの場合、これよりさらに下の `dist/` ディレクトリにモジュールごとの JS ファイルが用意されています。
Vue の場合は以下のようになります。
(画像だと見切れるためコピペってます)

```
.
├── vue.cjs.js
├── vue.cjs.prod.js
├── vue.d.ts
├── vue.esm-browser.js
├── vue.esm-browser.prod.js
├── vue.esm-bundler.js
├── vue.global.js
├── vue.global.prod.js
├── vue.runtime.esm-browser.js
├── vue.runtime.esm-browser.prod.js
├── vue.runtime.esm-bundler.js
├── vue.runtime.global.js
└── vue.runtime.global.prod.js
```

Vue のように[公式で紹介されている](https://github.com/vuejs/core/tree/main/packages/vue#from-cdn-or-without-a-bundler)ライブラリ以外は、この中から必要なファイルを探す必要がありますが、以下のようにファイル名からある程度は推測することができます。

- 名前に min が付く: 配信に最適なように圧縮されたファイル
- 名前に cjs, umd, esm が付く: それぞれ CommonJS, UMD, ES モジュール、どれにも該当しない場合は global スコープ扱いになることが多い

### :bulb: [Tips] `importmap` で `@` パス始まりのエイリアスを作る

`importmap` を使って、[create-vue](https://github.com/vuejs/create-vue) で作ったプロジェクトのように `@` パス始まりのエイリアスを作ることができます。
サンプルが以下です。

```html
<script type="importmap">
  {
    "imports": {
      "@/": "./src/"
    }
  }
</script>
```

`importmap` では[末尾に `/` が付くキーの `import` 時はそれに続くパスを展開してくれる](https://github.com/WICG/import-maps#packages-via-trailing-slashes)ので、

```javascript
import App from '@/app.js';
```

は

```javascript
import App from './src/app.js';
```

と等価となります。
元は [webpack の Code Splitting](https://webpack.js.org/guides/code-splitting/) のように必要なファイルだけを `import` する仕組みですが、相対パスへのエイリアスとして使うこともできます。

ただし、相対パスはローカルのディレクトリ構造ではなく、表示している URL に応じて変化するため、[vue-router](https://router.vuejs.org/) を使う場合は ハッシュモードでないと思ったように動作しない可能性があるのでご注意ください。

## CND から Vue アプリを作る

それでは実際に CDN から Vue アプリを作ってみましょう。

### `Hello, world!` を表示

まずは手始めに `Hello, world!` と表示するだけのアプリを作ってみましょう。
と言っても必要なことは CDN の章でほとんど説明してしまったので、ここではサンプルコードの紹介だけとします。

```html:index.html
<body>
  <script type="importmap">
    {
      "imports": {
        "vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js"
      }
    }
  </script>

  <div id="app">
    Hello, {{ name }}!
  </div>

  <script type="module">
    import { createApp } from 'vue';

    createApp({
      data() {
        return {
          name: 'world',
        };
      },
    }).mount('#app');
  </script>
</body>
```

[コード全体](https://github.com/aster-mnch/vue3-cdn-cookbook/tree/main/examples/hello-world),  [表示画面](https://aster-mnch.github.io/vue3-cdn-cookbook/examples/hello-world/)

### CDN から [Vuetify][] を使う

[Vuetify][] は、[Material Design](https://m3.material.io/) ベースで作られた Vue のコンポーネントライブラリです。
[公式サイトで紹介されている CDN からの利用方法](https://next.vuetifyjs.com/en/getting-started/installation/#cdn)は global スコープでの説明しかないため、ES モジュールで書く場合は以下のようになります。

```html:index.html
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@3.0/dist/vuetify.min.css" rel="stylesheet">
</head>
<body>
  <script type="importmap">
    {
      "imports": {
        "vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js",
        "vuetify": "https://cdn.jsdelivr.net/npm/vuetify@3.0/dist/vuetify.esm.min.js"
      }
    }
  </script>

  <div id="app"></div>

  <script type="module">
    import { createApp, ref } from 'vue';
    import { createVuetify } from 'vuetify';
    import App from './app.js';

    const vuetify = createVuetify();
    const app = createApp(App);
    app.use(vuetify);
    app.mount('#app');
  </script>
</body>
```

```javascript:app.js
import { ref } from 'vue';

export default {
  setup() {
    const drawer = ref(null);
    return {
      drawer,
    };
  },
  template: `
    <v-app id="inspire">
      <v-navigation-drawer v-model="drawer">
        <!--  -->
      </v-navigation-drawer>

      <v-app-bar>
        <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>

        <v-toolbar-title>Application</v-toolbar-title>
      </v-app-bar>

      <v-main>
        <!--  -->
      </v-main>
    </v-app>
  `,
};
```

Vuetify を使用するときは CSS ファイルもインストールする必要があるのでご注意です。
また、必要に応じて viewport の指定や[アイコンフォントをインストール](https://next.vuetifyjs.com/en/features/icon-fonts/#installing-icon-fonts)する必要があります。

:::note info
上記のサンプルコードは Vuetify の[ワイヤーフレーム](https://next.vuetifyjs.com/en/getting-started/wireframes/)を参考にしています。
:::

:::note warn
npm に Vuetify の ES モジュールが用意されたのは3からなので、2で開発する場合は global スコープでしか利用できないので注意。
:::

[コード全体](https://github.com/aster-mnch/vue3-cdn-cookbook/tree/main/examples/vuetify),  [表示画面](https://aster-mnch.github.io/vue3-cdn-cookbook/examples/vuetify/)

### CDN から [VueUse][] を使う

[VueUse][] は、Vue 3 から導入された Composition API 用のユーティリティ集です。
言うなれば、Vue 専用 [Lodash](https://lodash.com/) のような存在です。

こちらも CDN から使う場合は以下のようになります。

```html:index.html
<body>
  <script type="importmap">
    {
      "imports": {
        "vue": "https://cdn.jsdelivr.net/npm/vue@3.2/dist/vue.esm-browser.prod.js",
        "@vueuse/core": "https://cdn.jsdelivr.net/npm/@vueuse/core@9.6/index.min.mjs",
        "@vueuse/shared": "https://cdn.jsdelivr.net/npm/@vueuse/shared@9.6/index.min.mjs",
        "vue-demi": "https://cdn.jsdelivr.net/npm/vue-demi@0.13.11/lib/index.min.mjs"
      }
    }
  </script>

  <div id="app"></div>

  <script type="module">
    import { createApp, ref } from 'vue';
    import App from './app.js';

    createApp(App).mount('#app');
  </script>
</body>
```

```javascript:app.js
import { useMouse } from '@vueuse/core'

export default {
  setup() {
    const { x, y, sourceType } = useMouse()
    return { x, y, sourceType };
  },
  template: `
    <div>
      <p>x: {{ x }}</p>
      <p>y: {{ y }}</p>
      <p>sourceType: {{ sourceType }}</p>
    </div>
  `,
};
```

:::note warn
VueUse は Vue 2, 3 のどちらにも対応できるように、外部ライブラリ [vue-demi](https://github.com/vueuse/vue-demi) を使っているため、ES モジュールで `import` する場合は、上のコードのように `importmap` 内で依存解決する必要があります。
:::

[コード全体](https://github.com/aster-mnch/vue3-cdn-cookbook/tree/main/examples/vueuse),  [表示画面](https://aster-mnch.github.io/vue3-cdn-cookbook/examples/vueuse/)

## さいごに

この記事で紹介した CDN を使う方法ですと気軽に試せる反面、ビルドステップを挟まないため、SFC (Single File Component) による分割や TypeScript による型付けができません。
そのため、アプリの規模が大きくなる場合は [Vite][] などのビルドツールの導入をおすすめします。
CDN での開発は、サクッと動作を確認したいときなどにご活用ください。