ViteとVue RouterでSPAを作ってみた

シングルページアプリケーションを制作する要件がありました。
色々検討した結果、今回は`Vite`と`Vue Router`でこれを実現することにしました。

## 要件
- シンプルなSPA
今回は最初から最後までプレゼンテーションのように全画面でページ遷移するものです。
ショッピングサイトのような、ページの一部だけ遷移させるSPAではありません。

## 使用するもの
- [Vite](https://ja.vitejs.dev/)
- Vue Router

## 制作手順

### Vite導入

大体公式サイトの手順通りです。→[はじめに｜Vite](https://ja.vitejs.dev/guide/)


```sh:Viteインストール
❯ nodenv glocal 18.16.0
❯ mkdir pjname

❯ npm create vite@latest
✔ Project name: … pjname
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript
```

Node.jsはstableを入れました（本当は18.17.0だったけど、nodeenvにはなかったので18.16.0を）。

PJ作成時にフレームワークや言語を選びます。
今回は`Vue`と`JavaScript`にしています。
（TSにしようか迷ったけど、vueも初めてなので使い慣れている方にしました…）

はい、もうこれだけでViteが動きます。
素晴らしいですよね。ありがたいですよね！！

```sh: Vite起動
❯ cd pjname
❯ npm run dev
```

### Vue Router導入
Viteの基盤ができたら、そこに[Vue Router](https://v3.router.vuejs.org/ja/)君を追加します。

これも大体[公式](https://v3.router.vuejs.org/ja/installation.html)の手順通りです。

```
❯ npm install vue-router
```

### router.jsの作成
`router.js`を作成して、そこにVue Routerの設定を書いていきます。

```js:router.js
// Vue Routerパッケージのインポート
import { createRouter, createWebHistory } from 'vue-router'

const routes = []

// 一般的なルートの追加（動的インポート）
routes.push(
  {
    path: '/test',
    name: 'test',
    component: () => import('Test.vue'),
  }
)

// 存在しないパスは全てTOPへ
routes.push(
  {
    path: '/:catchAll(.*)',
    redirect: '/'
  }
)

const router = createRouter({
  history: createWebHistory(),
  routes,
})

router.beforeEach((to, from) => {
  // URL直接入力の場合はTOPへ
  if (to.path != '/' && from.name == undefined) {
    return '/'
  }
})

export default router
```

ここでポイントがいくつかあります。

#### hashモード or Historyモード
Vue Routerのデフォルトはhashモードです。
色々違いはあるのですが、私がHistoryモードにしたのは、hashモードだとURLが`http://localhost:8080/#/hoge`みたいに#が付いてしまうから。
違いがあるので要件に合う方を選べば良いかと思います。

[HTML5 Historyモード](https://v3.router.vuejs.org/ja/guide/essentials/history-mode.html)

historyモード選択時は注意点があるのですが、それは後述します。

#### 静的インポート/動的インポート
`component: () => import('../components/Test.vue')`のような書き方をすることで、各Vueファイルを動的インポートにしています。
これの何が良いかというと、ルート読み込み時に一気に全部のVueを読み込まず、このパスが呼び出された時にインポートしてくれるということです。
つまり、初回読み込みが速くなります。
今回は各ページがそれぞれ重く、またユーザーが全ページを読み込むとは限らないので基本的にこの動的インポートにしました。

じゃあ静的インポートはどないすりゃええかというと、こんな感じ。
```js:静的インポート
import Test from 'Test.vue'
const routes = [
  {
    path: '/',
    name: 'test',
    component: Test,
  }
]
```

#### 存在しないURLの対応
存在しないURLを指定された場合は、大体Not Foundに飛ばすのかなーと思うのですが、今回は全てTOPにリダイレクトします。

```js:指定外のパスは全てTOPへリダイレクト
routes.push(
  {
    path: '/:catchAll(.*)',
    redirect: '/'
  }
)
```

:::note info
このコードですが、例えば`ルート/path`の場合は正しく制御できても、`ルート/path/path`のような指定では上手くリダイレクトできませんでした（指定URLの真っ白ページが表示される）。調査したのですが未解決事項です。
:::

#### URLの直接指定を拒否
認証後のページを認証前ユーザーに見せたくない、という要件が多そうですが、今回は全てのURL直接指定による遷移を防御したい要件でした。
TOPはOKとして、それ以外の存在するURLが直接指定された場合、パスをTOPに書き換えるという処理をしています。
これは[ナビゲーションガード](https://v3.router.vuejs.org/ja/guide/advanced/navigation-guards.html)という仕組みで実現しました。

```js:URLの直接指定はTOPへリダイレクト
router.beforeEach((to, from) => {
  if (to.path != '/' && from.name == undefined) {
    return '/'
  }
})
```

「Vue Routerを介さないページ遷移では`from.name`に値が入ってこない」ということを利用してガードしています。


### main.jsの設定
インストールしたVue Routerを、アプリ全体に読み込ませます。

```js:main.js
import { createApp } from 'vue'
import router from './router'
import App from '../App.vue'

const app = createApp(App)

app.use(router)

app.mount('#app')
```

### .htaccessの設定
Historyモード使用時の注意点です。
これだけでも開発環境では動くので油断するのですが、サーバに上げた途端、404エラーが出ます。
このままではルーティング制御ができないのです。
使用するWebサーバを確認して、それぞれに適した`.htaccess`ファイルを置いてあげる必要があります。
[history-mode.html#サーバーの設定例](https://v3.router.vuejs.org/ja/guide/essentials/history-mode.html#%E3%82%B5%E3%83%BC%E3%83%8F%E3%82%99%E3%83%BC%E3%81%AE%E8%A8%AD%E5%AE%9A%E4%BE%8B)
私の場合はこのApacheの設定を入れてあげるだけでdev同様の動きになりました。
`mod_rewrite`が使えない場合はもう少し困りそうですね…

### 全画面SPAの実現
htmlとapp.vueはこんな感じにしました。

```html:index.html
<!-- 割愛 -->
<body>
  <div id="app"></div>
  <script type="module" src="js/main.js"></script>
</body>
```

```vue:App.vue
<template>
  <main>
    <router-view v-slot="{ Component }">
      <Transition name="switch-router">
        <component :is="Component" />
      </Transition>
    </router-view>
  </main>
</template>
```

これで`<router-view>`のところに指定されたVueファイルが描画される感じです。

描画したVueファイルでは以下のように指定することで遷移を行っていきます。

```vue:Sample.vue
<router-link to="パスの名前" replace>次へ</router-link>
```

jsで制御する場合はこんな感じ。

```vue:Sample.vue
<script setup>
import { useRouter } from 'vue-router'

const router = useRouter()
router.replace('パスの名前')
</script>
```

Historyモードを使用しているので、`router.push`にしてしまうと履歴が残ります。つまり、戻るボタンが使えてしまいます。
それも制御したかったので`router.replace`を使用しています。
`router-link`を使用する場合は、`replace`を記述してあげればOKです。

### 遷移アニメーションをつける
これで大体動作はOKなのですが、パキパキページが切り替わるのはカッコ悪いのでアニメーションをつけたいです。
遷移ごとにアニメーションを分けたい場合もあるでしょうが、今回は一律同じアニメーションで良かったので、呼び出し元の`App.vue`にトランジションアニメーションをつけてしまいました。

```vue:App.vue
    <router-view v-slot="{ Component }">
      <Transition name="switch-router">
        <component :is="Component" />
      </Transition>
    </router-view>
```

※なんでv-slot使っているのか思い出せません。何か理由はありました…何かが動かなくて変えたんです。。。

```scss:sample.scss
  .switch-router-enter-active {
    transition: opacity .7s;
    opacity: 0;
  }

  .switch-router-enter-to {
    opacity: 1;
  }
```

これでなんとなくふんわりしたページ切り替えになりました。


## あとがき
VueもViteも初めて触りました。
いろんなサイトの知見を参考にさせていただき、やっとやりたいことが実現できました。
ご指摘や修正は歓迎しています。