🐶

個人的Vue3+Vite+Quasar Framework備忘録(脱Vuetify)

2022/09/19に公開

はじめに

Quasar Framework とは、Vue ベースの Framework です。長く Vuetify を使っていたのですが、ずっと vue3 対応が遅れてる状態だったため、引っ越し先として quasar を選択したため、その備忘録。Vue3 対応しているフレームワークで使いやすそうな印象を自分は感じました。基本的なところは、他フレームワークと違いはないので、他フレームワークを利用していれば習熟はそれほど難しくないと感じます。ポジションとしては Nuxt あたりになる感じがしますが、使ったことないので比較できないです。Nuxt には UI コンポーネントがなかったように思うので、全部入り Nuxt なのかなというのが個人的な感想です(Nuxt の時点で結構いっぱい色々入っていると思うので、何が何やらですが)。

軽く特記事項を列挙します。

  • Vue3 に対応済み
  • UI コンポーネントの提供
  • 各種環境に対応するための build コマンドの提供
    • SPA/PWA/Mobile App(Capasitor)/Desktop App(Electron)
  • Vite 利用可
  • ドキュメントサイトにて、各種ツールを提供
  • ドキュメントは英語のみ

一応、Vuetify も beta 版ですが vue3 対応しているようですが(2022 年 9 月時点)、Vueitfy2 にあった全てのコンポーネントの対応はしばらくされないようです。気になった人は、一度いじってもらえるといいかもしれないですね。

準備

CLI インストール+プロジェクト作成

quasar のコマンドを実行するために Cli をインストールします。

npm i -g @quasar/cli

以下のコマンドで quasar のプロジェクトフォルダを作成します。
上記の cli を使用しなくても以下コマンドは利用できます。
いくつか設定を聞かれますが、その中で Vite を利用したものを選択します。

npm init quasar

これでプロジェクトフォルダの作成とあらかたのファイル生成がされます。

記述が面倒なので、いくつか省いていますが、大体は以下の通りなフォルダ構造をしています。

./
 |-public/
 |-src/
 | |-assets/ //静的素材
 | |-boot/ //初期化処理
 | |-components/ //コンポーネント
 | |-css/ //css
 | |-i18n/ //ローカライズ処理
 | |-layouts/ //レイアウト用コンポーネント
 | |-pages/ // ページコンポーネント
 | |-router/ //ルーター
 | |-stores/ //状態管理
 | |-App.vue
 | |-env.d.ts
 | |-quasar.d.ts
 | `-shims-vue.d.ts
 |-index.html
 |-quasar.config.js
 `tsconfig.json

認証用ライブラリ(firebase)

認証にはよく firebase を利用しているため、以下にコマンドも記述します。

npm i firebase

これの初期化は「boot」フォルダ以下に「firebase.ts」を作成し、以下のように記述します。ここで firebase ライブラリの初期化します。

firebase.ts
import { boot } from 'quasar/wrappers';
import { initializeApp } from 'firebase/app';

export const config = {
  apiKey: '',
  authDomain: '',
};

export default boot(({}) => {
  initializeApp(config);
});

以上のコードが動くように「quasar.config.js」に以下の記述を追加します。

quasar.config.js
module.exports = configure(function (/* ctx */) {

    // ------
    boot: [
        'i18n',
        'firebase' // 追加
    ]

});

pinia データ永続化用ライブラリ

自分は以下のライブラリを使用しています。一応コマンドも記述します。

  • localforage
  • pinia-plugin-persistedstate-2
npm i localforage pinia-plugin-persistedstate-2

その後、「stores/index.ts」を以下のように書き換えます。

import { store } from "quasar/wrappers";
import { createPinia } from "pinia";
import { Router } from "vue-router";
import { createPersistedStatePlugin } from "pinia-plugin-persistedstate-2";
import localForage from "localforage";

/*
 * When adding new properties to stores, you should also
 * extend the `PiniaCustomProperties` interface.
 * @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties
 */
declare module "pinia" {
  export interface PiniaCustomProperties {
    readonly router: Router;
  }
}

/*
 * If not building with SSR mode, you can
 * directly export the Store instantiation;
 *
 * The function below can be async too; either use
 * async/await or return a Promise which resolves
 * with the Store instance.
 */

export default store((/* { ssrContext } */) => {
  const pinia = createPinia();
  pinia.use(
    createPersistedStatePlugin({
      storage: {
        getItem: async (key) => {
          return localForage.getItem(key);
        },
        setItem: async (key, value) => {
          return localForage.setItem(key, value);
        },
        removeItem: async (key) => {
          return localForage.removeItem(key);
        },
      },
    })
  );

  // You can add Pinia plugins here
  // pinia.use(SomePiniaPlugin)

  return pinia;
});

stores について

ここは pinia による状態監視用のフォルダになります。説明が難しいので、例で説明します。

ここでは仮に「data」というテーブルを追加したいとします。サーバにも同様のテーブルがあることとし、
基本的には pinia のテーブルと同期をとるような形にしたいとします。
「stores/data.ts」を追加し、以下のようなファイルを作成します。

data.ts
import { defineStore } from 'pinia';

export const dataState = defineStore('data', {
  state: () => ({
    data: [] as string[],
  }),
  getters: {
    get_filter_data: (state) => (filter_data: string) => {
      const tmp = state.data.filter(
        (v) => v == filter_data
      );
      return tmp;
    },
  },
  actions: {
    async init(): Promise<void> {
      this.data = [];
    },
    async getData(): Promise<boolean> {
        // サーバからデータを取得
    },
    async createData(): Promise<boolean> {
        // サーバにデータを追加
    },
    async updateData(): Promise<boolean> {
        // サーバのデータを変更
    },
    async deleteData(): Promise<boolean> {
        // サーバのデータを削除
    },
  },
});

以上のようにして、「data」に対する直接的な操作は他ではしないようにします。他コンポーネント側では「state.data」の参照のみ許すこととします。自分は使ったことがないのですが、React の Redux の概念図が上記のようなイメージだったので、それを真似ています(多分細かいところは違うと思いますが)。

routers について

以下は、「routers/routes.ts」の例です。具体的にはプロジェクト作成時に example が生成されるので、ファイルの細かな説明は省きます。簡単に述べると、レイアウトを定義するコンポーネントを定義したのち、それの chidren コンポーネントではレイアウトを共通したコンポーネントを定義します。

また、「meta」のところでは、認証状態によって表示を許可するかどうかの定義をしています。

import { RouteRecordRaw } from "vue-router";

const routes: RouteRecordRaw[] = [
  {
    path: "/",
    component: () => import("layouts/MainLayout.vue"),
    meta: {
      requiresAuth: true, // 「認証が必要」ということを示すフラグ
    },
    children: [
      {
        name: "home",
        path: "",
        component: () => import("pages/IndexPage.vue"),
      },
    ],
  },
  {
    path: "/login/",
    component: () => import("layouts/LoginLayout.vue"),
    meta: {
      requiresAuth: false, // 「認証が必要」ということを示すフラグ
    },
    children: [
      {
        name: "login",
        path: "",
        component: () => import("pages/MainLogin.vue"),
      },
    ],
  },
  // Always leave this as last one,
  // but you can also remove it
  {
    path: "/:catchAll(.*)*",
    component: () => import("pages/ErrorNotFound.vue"),
  },
];

export default routes;

この後、自分は「App.vue」にナビゲーションガードを定義しています。ついでに認証状態に応じた処理も記述しています。

<template>
  <router-view />
</template>

<script setup lang="ts">
import { useRouter, useRoute } from "vue-router";
// store
import { dataState } from "./stores/data";

const dataStore = dataState();

// Lifecycle
router.beforeEach((to, from, next) => {
  const to_name: any = to.name;
  if (
    to.matched.some((record) => record.meta.requiresAuth) &&
    to_name !== "login"
  ) {
    onAuthStateChanged(getAuth(), (user) => {
      if (user) {
        next();
      } else {
        next({ name: "login" });
      }
    });
  } else {
    next();
  }
});

// login状態に応じてstoresの中を初期化する。
onAuthStateChanged(getAuth(), async (user) => {
  if (user) {
    dataStore.getData();
  } else {
    dataStore.init();
  }
});
</script>

dev サーバの起動/buld の実行

dev サーバの起動は以下の通りです。

quasar dev

ビルドコマンドはの起動は以下の通りです。モードを指定しない場合は「spa」としてビルドします。

quasar build
quasar build -m <mode>

dev サーバの proxy 設定

dev サーバの proxy 設定は以下の通りです。

quasar.config.js
module.exports = configure(function (/* ctx */) {

    // ------
    devServer: {
      open: true, // opens browser window automatically
      port: 8080,
      proxy: {
        '/api': {
          target:
            'https://~~/',
          changeOrigin: true,
        },
      },
    },

});

さいごに

雑多になりますが、簡単に記述しました。気が向いたら編集を続けます。単純に Vue3 対応を済ませているという点で好印象を受けますし、スマフォ用デザインも用意されているのが良いですね。これを採用してから新規プロジェクトは Vue3 に移行していますが、記述しやすくて良いです。

個人的に結構好きなフレームワークなので、使う人が増えていくとうれしいですね。

参考

Discussion