はじめに
既存のシステムがある状態で、一部のページのみ動作が複雑なため Vue を採用したい。
この用途の場合、SPA として構築するのではなく、導入するそれぞれのページごとにエントリーポイントが存在することが望ましい。
以下のような記事も見つかったが、ここで紹介されている Vue CLI は既にメンテナンスモード であり、最新の Vue を導入すると CLI 用のツールとして Vite が入ってくる。
そこで、上記記事のように複数エントリーポイントを持つ Vue プロジェクトを Vite ではどのように作成するのかをまとめる。
プロジェクトの初期設定とバージョンなど
公式に倣ってインストール。 インストールできたバージョンも表示。
注意点としてはルーティングは別システムでの管理を行うため、Vue Router をインストールしないこと。
$ 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
となる。
$ tree .
.
├── index.html
└── src
├── App.vue
└── main.js
今回は html
というディレクトリ内にエントリポイント html/index1.html
と html/index2.html
をそれぞれ作成する。
その後、src/index1
と src/index2
というディレクトリを作成し、ページごとのコンテンツはここで扱うこととする。
$ 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
で存在するページを指定し、開発サーバーの起動時に自動的に開くように設定。
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キャッシュで古い値を参照したままにしないような工夫を忘れないように注意。
各ページごとにコンポーネントを作成
テスト用に、エントリーポイントごとに異なる簡単なコンポーネントを作成する。
<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>
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
<!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>
<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>
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
<!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
以下にビルド結果が出力される。
$ 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
にそれぞれのエントリーポイントに対応した出力が行われる。 例えば、以下のようになる。
<!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 が初期で導入されているなどもあり、設定さえ分かってしまえば比較的容易に導入できることが分かったため、適時利用していきたい。