Vue + Vite で既存システムの各ページに部分的に配置するためのスクリプトを生成する

# はじめに

既存のシステムがある状態で、一部のページのみ動作が複雑なため Vue を採用したい。 
この用途の場合、SPA として構築するのではなく、導入するそれぞれのページごとにエントリーポイントが存在することが望ましい。

以下のような記事も見つかったが、ここで紹介されている [Vue CLI は既にメンテナンスモード](https://cli.vuejs.org/) であり、最新の Vue を導入すると CLI 用のツールとして Vite が入ってくる。

https://qiita.com/kamicop/items/c5c0917ed226234ee288

そこで、上記記事のように複数エントリーポイントを持つ Vue プロジェクトを Vite ではどのように作成するのかをまとめる。

# プロジェクトの初期設定とバージョンなど

公式に倣ってインストール。 インストールできたバージョンも表示。
注意点としてはルーティングは別システムでの管理を行うため、Vue Router をインストールしないこと。

https://ja.vuejs.org/guide/quick-start.html#creating-a-vue-application

```bash
$ npm init vue@latest

Vue.js - The Progressive JavaScript Framework

✔ Project name: … mpa-vue
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No

Scaffolding project in /home/tatsuya/dev/qiita/mpa-vue...

Done. Now run:

  cd mpa-vue
  npm install
  npm run dev

$ cd mpa-vue/
$ yarn
$ cat package.json 
{
  "name": "mpa-vue",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.3.4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "vite": "^4.3.9"
  }
}
```

## プロジェクト構造の変更

エントリーポイントに関係するリソースのみ抜き出すと、以下のようになる。
初期ではプロジェクト直下の `index.html` がページのエントリーポイントとなり、ここで利用する Vue のリソースが `src/App.vue` および `src/main.js` となる。

```bash
$ tree .
.
├── index.html
└── src
    ├── App.vue
    └── main.js
```

今回は `html` というディレクトリ内にエントリポイント `html/index1.html` と `html/index2.html` をそれぞれ作成する。
その後、`src/index1` と `src/index2` というディレクトリを作成し、ページごとのコンテンツはここで扱うこととする。

```bash
$ tree .
.
├── README.md
├── html
│   ├── index1.html
│   └── index2.html
└── src
    ├── index1
    │   ├── App.vue
    │   └── main.js
    └── index2
        ├── App.vue
        └── main.js
```

## vite.config.js の修正

デフォルトで生成される `vite.config.js` に `build` 区を書き足す。 ここの `build.rollupOptions.input` によって、複数のエントリーポイントを指定する。

また、これまでルート直下だったページは存在しなくなるので、 `server.open` で存在するページを指定し、開発サーバーの起動時に自動的に開くように設定。

```javascript
import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
  build: {
    rollupOptions: {
      input: {
        index1: "./html/index1.html",
        index2: "./html/index2.html",
      },
      output: {
        // 出力ファイルに付与される hash を取り除く
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
      },
    },
  },
  server: {
    open: "/html/index1.html",
  },
});
```

出力を考えると、デフォルトではビルドした場合に各ファイル名にハッシュ値を付与した内容が出力される。 これを既存システムの単純置き換えとして済ませるため output についてはファイル名にハッシュ値がつかないような設定にしている。
このような設定をした場合は、たとえば `?v=....` のようにクエリパラメータを用いてブラウザ・CDNキャッシュで古い値を参照したままにしないような工夫を忘れないように注意。

## 各ページごとにコンポーネントを作成

テスト用に、エントリーポイントごとに異なる簡単なコンポーネントを作成する。

```vue:src/index1/App.vue
<style lang="scss"></style>

<script setup>
import { ref, reactive } from "vue";
const count = ref(0);

function increment() {
  count.value++;
}

function large() {
  return count.value >= 10;
}
</script>

<template>
  <div>
    <p>Entry: Index 1</p>
    <button @click="increment">Count is: {{ count }}</button>
    <div v-show="large()">Count is Large now!</div>
  </div>
</template>
```

```javascript:src/index1/main.js
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');
```

```html:html/index1.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index1/main.js"></script>
  </body>
</html>
```

```vue:src/index2/App.vue
<style lang="scss"></style>

<script setup>
import { ref, reactive } from "vue";
const count = ref(0);

function increment() {
  count.value++;
}

function large() {
  return count.value >= 20;
}
</script>

<template>
  <div>
    <p>Entry: Index 2</p>
    <button @click="increment">Count is: {{ count }}</button>
    <div v-show="large()">Count is Large now!</div>
  </div>
</template>
```

```javascript:src/index2/main.js
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');
```

```html:html/index2.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index2/main.js"></script>
  </body>
</html>
```

この後、`yarn dev` として開発サーバーを起動して http://localhost:5173/html/index1.html と http://localhost:5173/html/index2.html とにアクセスし、異なる出力結果が得られればOK。

## 静的ファイルへのビルド

`yarn build` を実施すれば `dist` 以下にビルド結果が出力される。

```bash
$ yarn build
yarn run v1.22.17
$ vite build
vite v4.4.6 building for production...
✓ 12 modules transformed.
dist/html/index1.html                    0.44 kB │ gzip:  0.29 kB
dist/html/index2.html                    0.44 kB │ gzip:  0.29 kB
dist/assets/index1.js                    0.39 kB │ gzip:  0.29 kB
dist/assets/index2.js                    0.39 kB │ gzip:  0.29 kB
dist/assets/runtime-dom.esm-bundler.js  50.78 kB │ gzip: 20.51 kB
```

`dist/html` にそれぞれのエントリーポイントに対応した出力が行われる。 例えば、以下のようになる。

```html:dist/html/index1.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <script type="module" crossorigin src="/assets/index1.js"></script>
    <link rel="modulepreload" crossorigin href="/assets/runtime-dom.esm-bundler.js">
  </head>
  <body>
    <div id="app"></div>
    
  </body>
</html>
```

## 既存システムへの組み込み

上記で出力された `assets` 以下を既存システムの static リソース領域にコピーした後、既存システムのページで今回出力したHTMLに記載されている script/link 部分を転記し、また、Vue の動作を行いたい場所に `<div id="app"></div>` を挿入する。

こうすることで、既存システムのページが呼び出されたとき、ブラウザでビルド済の JavaScript 一式を読み込み、`<div id="app"></div>` の部分に Vue による動的ページを実現できる。


# まとめ

jQuery は導入が楽 ( `<script>` で CDN の URL を指定するだけで導入できてしまう）だが、Vue や React については SPA としての説明が多く、ピンポイントに既存システムにページごとに導入することは難しい印象があった。 

しかし、現時点では vite が初期で導入されているなどもあり、設定さえ分かってしまえば比較的容易に導入できることが分かったため、適時利用していきたい。