Vue 3 машинопись: Объекты, возвращенные из setup() с помощью оператора распространения JS, выдают ошибку в vscode

#typescript #vue.js #visual-studio-code #auth0 #vue-composition-api

Вопрос:

У меня есть простое репозиторий Vue 3 для машинописи, в котором я пытаюсь интегрировать плагин Auth0.

Он отображает строковый user объект на интерфейсе, и он работает так, как ожидалось.

Но код Visual Studio показывает ошибку машинописного Cannot find name 'user'. ts(2304) текста, потому что он не может видеть объект user при возврате внутри ...auth оператора распространения.

Я не уверен, почему он это делает и как это решить.

Это код для плагина Auth0. В двух словах, он используется app.provide("Auth", authPlugin); для предоставления доступа ко множеству вещей, включая user объект:

 import createAuth0Client, {
  Auth0Client,
  GetIdTokenClaimsOptions,
  GetTokenSilentlyOptions,
  GetTokenWithPopupOptions,
  LogoutOptions,
  RedirectLoginOptions,
  User,
} from "@auth0/auth0-spa-js";
import { App, Plugin, computed, reactive, watchEffect } from "vue";
import { NavigationGuardWithThis } from "vue-router";

let client: Auth0Client;

interface Auth0PluginState {
  loading: boolean;
  isAuthenticated: boolean;
  user: User | undefined;
  popupOpen: boolean;
  error: any;
}

const state = reactive<Auth0PluginState>({
  loading: true,
  isAuthenticated: false,
  user: {},
  popupOpen: false,
  error: null,
});

async function handleRedirectCallback() {
  state.loading = true;

  try {
    await client.handleRedirectCallback();
    state.user = await client.getUser();
    state.isAuthenticated = true;
  } catch (e) {
    state.error = e;
  } finally {
    state.loading = false;
  }
}

function loginWithRedirect(o: RedirectLoginOptions) {
  return client.loginWithRedirect(o);
}

function getIdTokenClaims(o: GetIdTokenClaimsOptions) {
  return client.getIdTokenClaims(o);
}

function getTokenSilently(o: GetTokenSilentlyOptions) {
  return client.getTokenSilently(o);
}

function getTokenWithPopup(o: GetTokenWithPopupOptions) {
  return client.getTokenWithPopup(o);
}

function logout(o: LogoutOptions) {
  return client.logout(o);
}

const authPlugin = {
  isAuthenticated: computed(() => state.isAuthenticated),
  loading: computed(() => state.loading),
  user: computed(() => state.user),
  getIdTokenClaims,
  getTokenSilently,
  getTokenWithPopup,
  handleRedirectCallback,
  loginWithRedirect,
  logout,
};

const routeGuard: NavigationGuardWithThis<undefined> = (
  to: any,
  from: any,
  next: any
) => {
  const { isAuthenticated, loading, loginWithRedirect } = authPlugin;

  const verify = async () => {
    // If the user is authenticated, continue with the route
    if (isAuthenticated.value) {
      return next();
    }

    // Otherwise, log in
    await loginWithRedirect({ appState: { targetUrl: to.fullPath } });
  };

  // If loading has already finished, check our auth state using `fn()`
  if (!loading.value) {
    return verify();
  }

  // Watch for the loading property to change before we check isAuthenticated
  watchEffect(() => {
    if (!loading.value) {
      return verify();
    }
  });
};

interface Auth0PluginOptions {
  domain: string;
  clientId: string;
  audience: string;
  redirectUri: string;

  onRedirectCallback(appState: any): void;
}

async function init(options: Auth0PluginOptions): Promise<Plugin> {
  client = await createAuth0Client({
    // domain: process.env.VUE_APP_AUTH0_DOMAIN,
    // client_id: process.env.VUE_APP_AUTH0_CLIENT_KEY,
    domain: options.domain,
    client_id: options.clientId,
    audience: options.audience,
    redirect_uri: options.redirectUri,
  });

  try {
    // If the user is returning to the app after authentication
    if (
      window.location.search.includes("code=") amp;amp;
      window.location.search.includes("state=")
    ) {
      // handle the redirect and retrieve tokens
      const { appState } = await client.handleRedirectCallback();

      // Notify subscribers that the redirect callback has happened, passing the appState
      // (useful for retrieving any pre-authentication state)
      options.onRedirectCallback(appState);
    }
  } catch (e) {
    state.error = e;
  } finally {
    // Initialize our internal authentication state
    state.isAuthenticated = await client.isAuthenticated();
    state.user = await client.getUser();
    state.loading = false;
  }

  return {
    install: (app: App) => {
      app.provide("Auth", authPlugin);
    },
  };
}

interface Auth0Plugin {
  init(options: Auth0PluginOptions): Promise<Plugin>;
  routeGuard: NavigationGuardWithThis<undefined>;
}

export const Auth0: Auth0Plugin = {
  init,
  routeGuard,
};
 

Здесь, на моей Profile.vue странице, я внедряю плагин Auth0, используя const auth = inject<Auth0Client>("Auth")!; и возвращая все его содержимое с setup() помощью оператора ...auth распространения. Это включает в user себя объект, который теперь доступен для использования в шаблоне.

Все это работает на переднем конце. Он отображает строковый user объект, как и ожидалось.

Но vscode выдает Cannot find name 'user'. ts(2304) ошибку, потому user что объект явно не возвращается из setup() .

Похоже, он не знает, что оператор ...auth распространения имеет user объект внутри auth :

 <template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(user, null, 2) }} <!-- ERROR: Cannot find name 'user'.ts(2304) -->
  </div>
</template>

<script lang="ts">
import { Auth0Client } from "@auth0/auth0-spa-js";
import { inject } from "vue";

export default {
  name: "Profile",
  setup() {
    const auth = inject<Auth0Client>("Auth")!;
    return {
      ...auth,
    };
  },
};
</script>
 

Я попытался решить эту проблему, явно вернув user объект, как показано ниже, но это нарушает функциональность. Строковый user объект больше не отображается на переднем конце:

 <template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(auth_user, null, 2) }}
  </div>
</template>

<script lang="ts">
import { Auth0Client } from "@auth0/auth0-spa-js";
import { inject } from "vue";

export default {
  name: "Profile",
  setup() {
    const auth = inject<Auth0Client>("Auth")!;
    const auth_user = auth.getUser(); // This does not work
    //const auth_user = auth.user; // This variation also doesn't work
    return {
      auth_user,
    };
  },
};
</script>
 

Может ли кто-нибудь понять, что здесь происходит и как устранить ошибку?

Ответ №1:

Есть несколько проблем:

  1. У Auth0Client класса нет user поля, поэтому возврат { ...auth } из setup() не создаст user свойство. Но это не тот тип, который вам нужен, как мы увидим в следующем пункте.
 export default class Auth0Client {
  private options;
  private transactionManager;
  private cacheManager;
  private customOptions;
  private domainUrl;
  private tokenIssuer;
  private defaultScope;
  private scope;
  private cookieStorage;
  private sessionCheckExpiryDays;
  private orgHintCookieName;
  private isAuthenticatedCookieName;
  private nowProvider;
  cacheLocation: CacheLocation;
  private worker;
  constructor(options: Auth0ClientOptions);
  private _url;
  private _getParams;
  private _authorizeUrl;
  private _verifyIdToken;
  private _parseNumber;
  private _processOrgIdHint;
  buildAuthorizeUrl(options?: RedirectLoginOptions): Promise<string>;
  loginWithPopup(options?: PopupLoginOptions, config?: PopupConfigOptions): Promise<void>;
  getUser<TUser extends User>(options?: GetUserOptions): Promise<TUser | undefined>;
  getIdTokenClaims(options?: GetIdTokenClaimsOptions): Promise<IdToken>;
  loginWithRedirect(options?: RedirectLoginOptions): Promise<void>;
  handleRedirectCallback(url?: string): Promise<RedirectLoginResult>;
  checkSession(options?: GetTokenSilentlyOptions): Promise<void>;
  getTokenSilently(options: GetTokenSilentlyOptions amp; {
      detailedResponse: true;
  }): Promise<GetTokenSilentlyVerboseResponse>;
  getTokenSilently(options?: GetTokenSilentlyOptions): Promise<string>;
  private _getTokenSilently;
  getTokenWithPopup(options?: GetTokenWithPopupOptions, config?: PopupConfigOptions): Promise<string>;
  isAuthenticated(): Promise<boolean>;
  buildLogoutUrl(options?: LogoutUrlOptions): string;
  logout(options?: LogoutOptions): Promise<void> | void;
  private _getTokenFromIFrame;
  private _getTokenUsingRefreshToken;
  private _getEntryFromCache;
}
 
  1. В то время Auth как объект редактируется inject как an Auth0Client , фактический объект provide d в @/auth/index.ts имеет тип, который не перекрывается Auth0Client . Фактический тип должен быть экспортирован, чтобы компоненты, на которые inject Auth объект может ввести ссылку:
 const authPlugin = {
  isAuthenticated: computed(() => state.isAuthenticated),
  loading: computed(() => state.loading),
  user: computed(() => state.user),
  getIdTokenClaims,
  getTokenSilently,
  getTokenWithPopup,
  handleRedirectCallback,
  loginWithRedirect,
  logout,
};

export type ProvidedAuthPlugin = typeof authPlugin; 👈
⋮
app.provide("Auth", authPlugin);
 
  1. Чтобы включить поддержку машинописи в компоненте (в том числе внутри <template> ), определение компонента должно быть объявлено с defineComponent :
 import { defineComponent } from "vue";

export default defineComponent({
  ⋮
});
 
  1. И Auth тип объекта должен использоваться в компоненте при inject его создании:
 import type { ProvidedAuthPlugin } from "@/auth"; 👈
import { inject, defineComponent } from "vue";

export default defineComponent({
  name: "Profile",
  setup() {                              👇
    const auth = inject("Auth") as ProvidedAuthPlugin;
    return {
      ...auth,
    };
  },
});
 

GitHub PR

Ответ №2:

Хорошо для того, что я понимаю (я не эксперт по API композиции).

Здесь , например setup() , в return заявлении должно быть указано, что у вас будет доступно внутри <template> .

допустим, вы хотите использовать пользователя здесь

  <div class="row">
    {{ JSON.stringify(user, null, 2) }} <!-- ERROR: Cannot find name 'user'.ts(2304) -->
  </div>
 

В основном он не находит никаких user данных. давайте попробуем добавить это в return заявление о setup()

Попробуй это:

 <template>
  <div class="about">
    <h1>This is a profile page, only logged in users can see it.</h1>
  </div>
  <div class="row">
    {{ JSON.stringify(user, null, 2) }}
  </div>
</template>

<script lang="ts">
import { inject, ref } from 'vue'
import { Auth0Client, User } from '@auth0/auth0-spa-js'

export default {
  name: 'Profile',
  setup() {
    /* Added for you this 2 lines, one for getting types of auth
       I think the other one is reactive */
    const auth = inject('Auth') as Auth0Client
    const user = ref<User | undefined>(undefined)

    auth.getUser().then((authuser) => (user.value = authuser))
    return {
      ...auth, // Check this one, I don't see it being used in <template>
      user // This one should be available in <template> now
    }
  }
}
</script>
 

Надеюсь, это сработает… Кроме того, я не большой поклонник composition API, если по какой-либо причине вы только изучаете Vue, используйте API по умолчанию, его намного проще изучать и использовать :).

Комментарии:

1. Спасибо за помощь, но это нарушило отображение user на странице профиля. Я думаю, что это каким-то образом сделало его нереактивным.

2. Я думаю, что в оригинале оператор распространения ...auth помогал возвращать user то, что было внутри auth . Я все еще изучаю операторы распространения, так что я могу ошибаться, но я думаю, что именно поэтому это работало раньше.

3. Побочные эффекты могут быть устранены, поэтому не рекомендуется применять их непосредственно при настройке. Или их можно было бы ожидать в настройках, но это заставляет использовать его с неизвестностью.

4. @EstusFlask не могли бы вы подробнее ответить на этот вопрос? Я не совсем понимаю

5. Вам нужно положить auth.getUser() внутрь на монтировку или дождаться ее v3.vuejs.org/guide/migration/suspense.html#introduction если вы хотите, чтобы компонент не отображался без данных.