Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

エラーハンドラーからrouterを参照できない #1612

Closed
KentaHizume opened this issue Aug 21, 2024 · 11 comments
Closed

エラーハンドラーからrouterを参照できない #1612

KentaHizume opened this issue Aug 21, 2024 · 11 comments
Assignees
Labels
target: ConsoleAppWithDI コンソールアプリケーションの要件別サンプルに関係がある target: Dressca サンプルアプリケーションDresscaに関係がある
Milestone

Comments

@KentaHizume
Copy link
Contributor

KentaHizume commented Aug 21, 2024

概要

フロントエンドのerrorHandler内でrouterを使おうと試みると、想定外の挙動をする。

詳細 / 機能詳細(オプション)

  • const router =use Router();でrouterを使おうとすると下記の警告が出力される。
    [Vue warn]: inject() can only be used inside setup() or functional components.
    useRouter()が内部的にinject()を呼び出しているらしいが、
    errorHandlerはvueコンポーネントではないので、inject()を呼び出すことができない。
  • その結果、router.currentRouteが取得できずにシステムエラーになってしまう。
    if (error instanceof UnauthorizedError) {
      if (handlingUnauthorizedError) {
        handlingUnauthorizedError();
      } else {
        const router = useRouter();
        const routingStore = useRoutingStore();
        routingStore.setRedirectFrom(router.currentRoute.value.path.slice(1));
        router.push({ name: 'authentication/login' });
        showToast('ログインしてください。');
      }

image

完了条件

ここにこの Issue の完了条件を箇条書きで記載します。

@KentaHizume KentaHizume added this to the v0.10 milestone Aug 21, 2024
@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 21, 2024

  • 直接routerの実装をimportすると、動作自体は期待通りになるが、循環参照になってしまいlintエラーになる。

CataogView⇒ErrorHandler⇒router⇒catalogRoutes⇒CatalogViewという関係。

import { router } from '@/router';
C:\maris-oss\maris\samples\Dressca\dressca-frontend\customer\src\views\catalog\CatalogView.vue
  17:1  error  Dependency cycle via @/router:1=>@/router/catalog/catalog:3  import/no-cycle

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Aug 30, 2024

対応案

検討・調査したがうまくいかなかったもの

storeでrouterをラップする

routeingStoreは使えるので、
routerのstoreにrouter.currentRoute.value.fullPathとrouter.pushをラップしたメソッドを持たせる
⇒挙動自体は想定通りになるが、同じく循環参照になってしまう

router

    currentPath: router.currentRoute.value.fullPath,

    navigateToNamedRoute(routeName: string) {
      router.push({ name: routeName });
    },

handler

        const routingStore = useRoutingStore();
        routingStore.setRedirectFrom(routingStore.currentPath);
        routingStore.navigateToNamedRoute('authentication/login');
        showToast('ログインしてください。');

循環参照

maris\samples\Dressca\dressca-frontend\admin\src\views\authentication\LoginView.vue  8:1  error  Dependency cycle via @/router:2=>@/router/authentication/authentication:3  import/no-cycle

errorHandlerをVueのコンポーネント化する

[Vue warn]: inject() can only be used inside setup() or functional components.
というメッセージなので、ハンドラーがvueのcomponentであれば使えるはず、というアイデア。

  • composable

https://ja.vuejs.org/guide/reusability/composables

  • 作りが変わってしまう、templateを返したいわけではないのでそもそもこれでいけるのかよくわからない

composableの例を下記で探してみたが、エラーハンドラーは見つからなかった。

https://vueuse.org/

plugin化

あまりよくわかっていない状態。
すでにerror-handler-pluginがあり、その中ではうまく処理できなかったのでhandlerを分けたはず、なのでNG?

https://ja.vuejs.org/guide/reusability/plugins#provide-inject-with-plugins

@KentaHizume
Copy link
Contributor Author

ComposableとFunctional Componentsを混同していたが、これらは別物で、
(ComposableはComponentではない)ので、Composableにしても解決しない。

@KentaHizume
Copy link
Contributor Author

plugin化

この方法で回避できることを確認した。

App.vue

// プラグイン呼び出す
app.use(customHandler);

custom-handler.ts

// routerをimportする
import { router } from '@/router';

// injectするクラスとメソッドを定義
class Handler {
  static errorHandler()
}

// Plugin用のメソッド定義
export const customHandler = {
  install: (app: App) => {
    app.provide('customHandler', Handler);
  },
};

View側

// ハンドラーをinjectする
// このときcustomHandlerをimportしているわけではないので、循環参照にはならない
const customHandler: any = inject('customHandler');
// 一旦any
// 型情報だけのimportであれば循環参照にはならない…はず

const addBasket = async (catalogItemId: number) => {
  try {
    await addItemToBasket(catalogItemId);
    router.push({ name: 'basket' });
  } catch (error) {
    customHandler.errorHandler(error, () => {
      showToast('カートに追加できませんでした。');
    });
  }
};

@KentaHizume KentaHizume self-assigned this Oct 2, 2024
@KentaHizume
Copy link
Contributor Author

  • テストを書くときに、Appをマウントするときにプラグインを渡してあげる必要があるので注意
    (routerやpiniaと同様、子コンポーネントでinjectできるようにprovideしてあげる)
describe('App.vue', () => {
  it('トップページに遷移する', async () => {
    const wrapper = mount(App, { global: { plugins: [router] } });
    await router.isReady();
    expect(wrapper.html()).toContain('Dressca 管理 トップ');
  });
});
    const wrapper = mount(ItemsAddView, {
      global: { plugins: [pinia, router] },
    });

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Oct 8, 2024

Pluginの作り方

vue-routerとpiniaを確認し、同じ構造だったため、これを一般的なものと考えて問題ないはず…。

createXX

型解決のためにinterfaceを定義して、
install()メソッドを持つオブジェクトを返却するcreateXX経由でオブジェクトを取得し、
アプリケーションレベルでのプラグインを使用を宣言する。

export interface CustomErrorHandler {
// 略
export function createCustomErrorHandler(): CustomErrorHandler {
  const customErrorHandler: CustomErrorHandler = {
    install: (app: App) => {
      app.provide(customErrorHandlerKey, customErrorHandler);
    },
    handle: (
// 略
  return customErrorHandler;
}

main.ts

const customErrorHandler = createCustomErrorHandler();
app.use(customErrorHandler);

useXX

プラグインを利用したい子コンポーネントでは、直接injectを書かず、
useXX経由でプラグインを呼び出す。

https://ja.vuejs.org/guide/reusability/composables#naming
コンポーザブル関数には "use" で始まるキャメルケースの名前をつけるのが慣例です。

定義

export function useCustomErrorHandler(): CustomErrorHandler {
  return inject(customErrorHandlerKey)!;
}

子コンポーネントでの利用

const customErrorHandler = useCustomErrorHandler();

Injectionkey

プラグインは、アプリケーションレベルでprovide、子コンポーネントinjectして使用するが、
その際に特定するためのキーが必要になる。
このキーを型付けするためのInjectionKeyインターフェースが提供されている。

https://ja.vuejs.org/guide/typescript/composition-api.html#typing-provide-inject

1箇所に定義し、利用するコンポーネントから呼び出せばよいので、べた書きのキー値が散らばることはない。

import type { InjectionKey } from 'vue';
import type { CustomErrorHandler } from './error-handler/custom-error-handler';

export const customErrorHandlerKey = Symbol(
  'customErrorHandler',
) as InjectionKey<CustomErrorHandler>;

@tsuna-can-se tsuna-can-se modified the milestones: v1.0, v0.10.1 Oct 8, 2024
@KentaHizume
Copy link
Contributor Author

KentaHizume commented Oct 8, 2024

テスト

確認対象は下記。
実際にバックエンド側のレスポンスを作るのは仕込みが大変なので、
mockのレスポンスを加工して確認する。
(レスポンスコードを見ているので、mockで構わない)

カスタムエラーハンドラー

コールバックは画面側で確認する。

共通処理

  • レスポンスなし
    • 「ネットワークエラーが発生しました」表示
  • 401 Unauthorized
    • 「ログインしてください」表示、ログイン画面に遷移
    • ログイン後、エラー直前の画面に戻る
  • 500 サーバーエラー
    • 「サーバーエラーが発生しました」表示

画面

買い物かご

  • 数量変更に失敗
  • 商品の削除に失敗
  • カートの取得に失敗

カタログ

  • カートへの追加に失敗
  • 商品の取得に失敗

チェックアウト

  • 注文に失敗

完了画面

  • 注文情報の取得に失敗

@KentaHizume
Copy link
Contributor Author

ネットワークエラーの起こし方

mockからレスポンスを返さないようにした状態で、
axiosのタイムアウトを設定する。

axiosはデフォルトではタイムアウトしない設定なので、ずっと待ち続ける。

https://axios-http.com/ja/docs/req_config

const axiosInstance = axios.create({
  baseURL: import.meta.env.VITE_AXIOS_BASE_ENDPOINT_ORIGIN,
  headers: {
    'Content-Type': 'application/json',
  },
  withCredentials: true,
  timeout: 2500,
});

@KentaHizume
Copy link
Contributor Author

KentaHizume commented Oct 8, 2024

コールバックと共通処理が被った場合の挙動

既存の挙動として、

下記で注文が500エラーで失敗したときのアプリの挙動として、

  1. コールバック実行で、注文が失敗しましたをToast表示
  2. 共通処理実行で、サーバーエラーが発生しましたをToast表示
  3. エラー画面に遷移
    になり、1の注文に失敗しましたのToastのメッセージが、2で一瞬で更新されて見た目としては見えない状態になる。

少し違和感はあるが、いまのところ問題になるケースはなさそう…?

CheckOutView.vue

const checkout = async () => {
  try {
// 略
  } catch (error) {
    customErrorHandler.handle(error, () => {
      showToast('注文に失敗しました。');
      router.push({ name: 'error' });
    });
  }
};
      if (error instanceof CustomErrorBase) {
        callback();

        // エラーの種類によって共通処理を行う
        // switch だと instanceof での判定ができないため if 文で判定
        if (error instanceof UnauthorizedError) {
          if (handlingUnauthorizedError) {
// 略
        } else if (error instanceof ServerError) {
          if (handlingServerError) {
            handlingServerError();
          } else {
            showToast('サーバーエラーが発生しました。');
          }
        }

@KentaHizume
Copy link
Contributor Author

ハンドラーがrouterに依存しているので、
routerより先にハンドラーをuseしたらエラーになるような気がするが、エラーにならない。
偶然うまくいっているのか、うまく依存関係を解決してくれているのか不明。

app.use(customErrorHandler);
app.use(pinia);
app.use(router);

@KentaHizume KentaHizume added the 中止状態 現在着手予定がない label Oct 9, 2024
@tsuna-can-se tsuna-can-se added target: Dressca サンプルアプリケーションDresscaに関係がある target: ConsoleAppWithDI コンソールアプリケーションの要件別サンプルに関係がある and removed 中止状態 現在着手予定がない サンプルAP labels Oct 15, 2024
@tsuna-can-se tsuna-can-se modified the milestones: v0.10.2, v1.0 Oct 23, 2024
@KentaHizume KentaHizume removed this from the v1.0 milestone Nov 6, 2024
@KentaHizume KentaHizume added this to the v0.10.2 milestone Nov 6, 2024
@KentaHizume
Copy link
Contributor Author

KentaHizume commented Nov 6, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
target: ConsoleAppWithDI コンソールアプリケーションの要件別サンプルに関係がある target: Dressca サンプルアプリケーションDresscaに関係がある
Projects
None yet
2 participants