Vuetify v3.0(Beta) 導入メモ

## はじめに

2022年4月頃に見たら、Vuetify 3がそろそろリリース予定（当時は2022年6月予定と書かれていたような）だったので、Vue 3をぼちぼち触り始めたのですが、なかなかリリースされませんね。。。
ドキュメンテーションに時間がかかっているのでしょうか。

https://vuetifyjs.com/ja/introduction/roadmap/


痺れを切らしたので、まだBeta版ですが使い始めてみました。
Vue 2 の頃のコードのコピペでは上手くいかなかったところをメモしておきます。


## インストール

Vite / Vue3 でセットアップ済のプロジェクトに Vuetify 3 を後から追加してます。

```bash
$ vue add vuetify
```

いくつかファイルの追加や上書きがされています。

```bash
$ git status
On branch add_vuetify
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   index.html
        modified:   package-lock.json
        modified:   package.json
        modified:   src/App.vue
        modified:   src/main.ts
        modified:   vite.config.ts

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/assets/logo.svg
        src/components/HelloWorld.vue
        src/plugins/vuetify.ts
        src/plugins/webfontloader.ts
        src/views/HomeView.vue

no changes added to commit (use "git add" and/or "git commit -a")
```

特に上書きされているものは、元のコードが消されてしまっていたりするので要修正でした。

```json-doc:package.json
{
  "name": "firebase-test",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "serve": "vite preview",  // 追記されたが、previewと被ってるので後から削除した
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "preview": "vite preview",
    "test": "vitest"
  },
  "dependencies": {
    "@mdi/font": "5.9.55", // 追記された
    "firebase": "^9.8.3",
    "roboto-fontface": "*", // 追記された
    "vue": "^3.2.25",
    "vue-router": "^4.0.16",
    "vuetify": "^3.0.0-beta.0", // 追記された
    "webfontloader": "^1.0.0" // 追記された
  },
  "devDependencies": {
    "@testing-library/vue": "^6.6.1",
    "@types/webfontloader": "^1.0.0", // 追記された
    "@vitejs/plugin-vue": "^3.0.3",
    "@vue/test-utils": "^2.0.2",
    "jsdom": "^20.0.0",
    "typescript": "^4.5.4",
    "vite": "^3.0.8", // アップデートされた
    "vite-plugin-vuetify": "^1.0.0-alpha.12", // 追記された
    "vitest": "^0.22.1",
    "vue-cli-plugin-vuetify": "~2.5.5", // 追記された
    "vue-tsc": "^0.29.8"
  }
}
```

```typescript:vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
import vuetify from 'vite-plugin-vuetify' // 追記された

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
		vue(),
		vuetify({ autoImport: true }), // 追記された
	],
})
```

```typescript:src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router' // named exportにしてたらdefault export前提({}なし)に変えられたので後から調整した
import vuetify from './plugins/vuetify'  // 追記された
import { loadFonts } from './plugins/webfontloader'  // 追記された

loadFonts()  // 追記された

createApp(App) // 自作pluginのuseが消されてしまったので後から追記した
  .use(router)
  .use(vuetify)
  .mount('#app')  
```

```vue:src/App.vue
// まるっと以下に置き換わってたので後から修正した
<template>
  <v-app>
    <v-main>
      <router-view/>
    </v-main>
  </v-app>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',

  data () {
    return {
      //
    }
  },
})
</script>
```

また、実行すると以下のWARNが出力されました。

```
DevTools がソースマップの読み込みに失敗しました（http://localhost:5173/materialdesignicons.css.map のコンテンツを読み込めませんでした（HTTP エラー: ステータス コード 404、net::ERR_HTTP_RESPONSE_CODE_FAILURE））
```

これは以下を追記したら解消しました。

```typescript:vite.config.ts
export default defineConfig({
  plugins: [
	vue(),
	vuetify({ autoImport: true }),
  ],
  // 以下追加
  css: {
    devSourcemap: true,
  },
})
```


## Upgrade Guide

ドキュメントに少しだけガイドの記載があったので確認しましたが、やけに少ないので、今後追記されていくのでは。

> ## Components
> 
> ### [#](https://next.vuetifyjs.com/en/getting-started/upgrade-guide/#general-changes) General changes
> 
> - **light** and **dark** props have been removed from all components. Use **theme** prop instead
> - **value** prop has been replaced by **model-value** on components that support `v-model` usage
> - **@input** event has been replaced by **@update:model-value** on components that support `v-model` usage
> 
> ### [#](https://next.vuetifyjs.com/en/getting-started/upgrade-guide/#v-alert) v-alert
> 
> - **border** prop values `left` and `right` have been replaced by `start` and `end`
> - **colored-border** prop has been replaced by **border-color**
> - **dismissable** prop has been replaced by **closeable**
> - **outlined** and **text** props have been replaced by single prop **variant**
> - **text** prop has new purpose. It represents the text content of the alert, if default slot is not used
> 
> ### [#](https://next.vuetifyjs.com/en/getting-started/upgrade-guide/#v-app) v-app
> 
> - **id** prop has been removed

[https://next.vuetifyjs.com/en/getting-started/upgrade-guide/#general-changes](https://next.vuetifyjs.com/en/getting-started/upgrade-guide/#general-changes)

- `light`や`dark`を使ってた部分は、個別の色の設定はやめてthemeベースで設定するように変えました。
- `v-alert`に限らず、`dismissable`→`closable`の変更や`variant`でのタイプ指定はよく出てきました。


## [Vue warn]: Failed to resolve component

Vuetify 3 では削除されたり置き換わったcomponentを使用しているとWARNが出たので、1つ1つ修正していきました。例えば以下。

- `v-content`
    - `v-main`に置き換え。以前からdeprecatedになってたらしい。
        - [https://next.vuetifyjs.com/en/features/application-layout/](https://next.vuetifyjs.com/en/features/application-layout/)
- `v-flex`
    - `v-row`,`v-col`に置き換え。
        - [https://next.vuetifyjs.com/en/components/grids/](https://next.vuetifyjs.com/en/components/grids/)
- `v-list-item-content`
    - titleやsubtitleは、代わりに`v-list-item`の直下に記述したり、`v-list-item`のプロパティとして指定。


## Formのバリデーション

Vue 2 の頃はこんな感じで書いていました。

```vue
<template>
...
  <v-form ref="form">
    ...
  </v-form>
...
</template>

<script lang="ts">
import { Vue, Component, Emit } from 'vue-property-decorator';

type VForm = Vue & {
  validate: () => boolean;
  reset: () => void;
  resetValidation: () => void;
};

@Component({})
export default class ItemForm extends Vue {

  ...

  public $refs!: {
    form: VForm;
  };

  private submitItem(): void {
    if (this.$refs.form.validate()) {
      this.submit(this.item));
    }
  }

  @Emit('submit')
  private submit(item): void {
    this.$refs.form.resetValidation();
  }
}
</script>
```

setupだとthisが使えないので、validateなどをどう呼ぶのかな？と思いましたが、setup内でrefで定義した変数をv-formのrefプロパティにセットすればよいようです。

```vue
<script setup lang="ts">
import { ref } from 'vue';

import { useItem } from '../composables/useItem';

const { submit } = useItem();

const item = ref('');

const form = ref();

const onSubmit = async () => {
  const result = await form.value.validate();
  if(result.valid) {
    submit(item.value);
    form.value.reset();
  }
};
</script>

<template>
...
  <v-form ref="form">
    ...
  </v-form>
...
</template>
```


## バンドルサイズが大きい

Vuetify対応後にビルドすると以下のWARNが出ました。

```bash
$ npm run build

> firebase-test@0.0.0 build
> vue-tsc --noEmit && vite build

vite v3.1.0 building for production...
✓ 368 modules transformed.
dist/assets/materialdesignicons-webfont.9bdfaf27.eot     1002.34 KiB
dist/assets/materialdesignicons-webfont.da7fba3c.woff2   317.62 KiB
dist/assets/materialdesignicons-webfont.8bded8f4.woff    454.29 KiB
dist/assets/materialdesignicons-webfont.c14484cf.ttf     1002.13 KiB
dist/index.html                                          0.42 KiB
dist/assets/webfontloader.b777d690.js                    12.42 KiB / gzip: 4.98 KiB
dist/assets/index.63c32017.css                           573.56 KiB / gzip: 82.09 KiB
dist/assets/index.1005b1e3.js                            628.15 KiB / gzip: 173.54 KiB

(!) Some chunks are larger than 500 KiB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/guide/en/#outputmanualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
```

ちなみにVuetifyを入れる前は以下です。

```bash
$ npm run build

> firebase-test@0.0.0 build
> vue-tsc --noEmit && vite build

vite v3.1.0 building for production...
✓ 97 modules transformed.
dist/index.html                 0.36 KiB
dist/assets/index.e91a8232.js   466.58 KiB / gzip: 121.12 KiB
```

気にしてませんでしたが、もともと結構大きかったようです。
以下を参考に確認したところ、Firebaseまわりが大きすぎですね。。。
[https://miyauchi.dev/ja/posts/firebase-bundle-size/](https://miyauchi.dev/ja/posts/firebase-bundle-size/)
[https://zenn.dev/meijin/articles/vite-bundle-analyzer](https://zenn.dev/meijin/articles/vite-bundle-analyzer)

Firestore Liteというのもあるようですが、リアルタイムアップデートが使えないのが厳しいです。
[https://firebase.google.com/docs/firestore/solutions/firestore-lite](https://firebase.google.com/docs/firestore/solutions/firestore-lite)


とは言え、`materialdesignicons-webfont`もかなり大きいので、以下を参考に、アイコンは必要なものだけ個別にimportするようにしてみました。
[https://next.vuetifyjs.com/en/features/icon-fonts/#material-design-icons-js-svg](https://next.vuetifyjs.com/en/features/icon-fonts/#material-design-icons-js-svg)

```bash
$ npm i -D @mdi/js
```

```typescript:src/plugins/vuetify.ts
// まずこれを削除。冒頭のソースマップの設定も不要になる
// import '@mdi/font/css/materialdesignicons.css'

// Styles
import 'vuetify/styles'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg' // 追加

// Vuetify
import { createVuetify } from 'vuetify'

export const vuetify = createVuetify({
  // 以下を追加
  icons: {
    defaultSet: 'mdi',
    aliases,
    sets: {
      mdi,
    },
  },
});
```

各コンポーネントで使用するアイコンをimportします。

```vue:src/components/ItemLine.vue
<script setup lang="ts">
import { mdiDelete } from '@mdi/js'; // これを追加

import { Item } from '../domain/models/Item';

import { useItem } from '../composables/useItem';

const props = defineProps<{
  item: Item
}>();

const { deleteItem } = useComic();
</script>

<template>
  <v-list-item>
    <v-list-item-title>
      {{ item.name }}
    </v-list-item-title>
    
    <template v-slot:append>
      <v-btn
        icon
        variant="plain"
        @click.stop="deleteItem(comic)"
      >
        <!-- <v-icon>mdi-delete</v-icon> -->
        <v-icon>{{ mdiDelete }}</v-icon>
      </v-btn>
    </template>
  </v-list-item>
</template>
```

修正後のビルド結果です。

```bash
$ npm run build

> firebase-test@0.0.0 build
> vue-tsc --noEmit && vite build

vite v3.1.0 building for production...
✓ 369 modules transformed.
dist/index.html                         0.42 KiB
dist/assets/webfontloader.b777d690.js   12.42 KiB / gzip: 4.98 KiB
dist/assets/index.b127324a.css          311.74 KiB / gzip: 38.67 KiB
dist/assets/index.aa7ed990.js           633.31 KiB / gzip: 175.68 KiB

(!) Some chunks are larger than 500 KiB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/guide/en/#outputmanualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
```

`materialdesignicons-webfont`はなくなりましたが、メインのバンドルはアイコンを組みこんだせいか更に大きくなりましたね。

あとはviewの単位でdynamic importにしたりすれば、viewごとにファイルが分割されて、1ファイルあたりのサイズは小さくなりますが、シンプルなアプリだとあまり分けられる要素もなく、なかなか500KBはきれませんでした。。。
やはりFirebaseが大きい。


https://v3.router.vuejs.org/ja/guide/advanced/lazy-loading.html#同じチャンク内でのコンポーネントグループ化

単に上記のWARNを出さなくするだけであれば、メッセージ記載のとおり、`build.chunkSizeWarningLimit`を上げればよいのですが、根本的な解決ではないので微妙なところです。ビルドエラーになるわけではないので、いったんそのままにしました。


## おわりに

シンプルなアプリで試したので、あまり使いこめてはいませんが、それほど大きな変更の印象はなかったです。
ただ、まだドキュメントが出揃っておらず、APIリファレンスへのリンクもほぼ404だったりするので、細かいプロパティ設定の仕方などは調べづらかったです。それでも2.0と同じように設定すれば大体動いたりしました。
正式リリース時に大きな変更が発生したりしなければ大丈夫そうな気がします。