在 Vue 3 中从父子调用函数 - TypeScript

Calling a function from parent to child in Vue 3 - TypeScript

我已经从 Quasar v1 迁移到 Quasar v2,并在这样做的过程中迁移到 Vue 3。我有下面的代码在 Quasar v1 (Vue v2) 中工作,但是现在在 Quasar 中 运行 v2 (Vue v3) 选择过滤器时,我在控制台中收到以下错误消息:

Uncaught (in promise) TypeError: view.value.fetchFilteredResources is not a function
    searchResourcesString MainLayout.vue:128
    fulfilled index.js:16
    promise callback*step index.js:18
    __awaiter index.js:19
    __awaiter index.js:15
    searchResourcesString MainLayout.vue:124
    callWithErrorHandling runtime-core.esm-bundler.js:154
    callWithAsyncErrorHandling runtime-core.esm-bundler.js:163
    invoker runtime-dom.esm-bundler.js:333
    addEventListener runtime-dom.esm-bundler.js:283
    patchEvent runtime-dom.esm-bundler.js:301
    patchProp runtime-dom.esm-bundler.js:369
    mountElement runtime-core.esm-bundler.js:3905
    processElement runtime-core.esm-bundler.js:3868
    patch runtime-core.esm-bundler.js:3788
    mountChildren runtime-core.esm-bundler.js:3975
    mountElement runtime-core.esm-bundler.js:3896
    processElement runtime-core.esm-bundler.js:3868
    patch runtime-core.esm-bundler.js:3788
    mountChildren runtime-core.esm-bundler.js:3975
    mountElement runtime-core.esm-bundler.js:3896
    processElement runtime-core.esm-bundler.js:3868
    patch runtime-core.esm-bundler.js:3788
    mountChildren runtime-core.esm-bundler.js:3975
    mountElement runtime-core.esm-bundler.js:3896
    processElement runtime-core.esm-bundler.js:3868
    patch runtime-core.esm-bundler.js:3788
    mountChildren runtime-core.esm-bundler.js:3975
    mountElement runtime-core.esm-bundler.js:3896
    processElement runtime-core.esm-bundler.js:3868
    patch runtime-core.esm-bundler.js:3788
    componentEffect runtime-core.esm-bundler.js:4298
    reactiveEffect reactivity.esm-bundler.js:42
    effect reactivity.esm-bundler.js:17
    setupRenderEffect runtime-core.esm-bundler.js:4263
    mountComponent runtime-core.esm-bundler.js:4222
    processComponent runtime-core.esm-bundler.js:4182
    patch runtime-core.esm-bundler.js:3791
    mountChildren runtime-core.esm-bundler.js:3975 MainLayout.vue:128

应该发生的是调用函数以过滤 GraphQL 查询的结果。

./src/pages/Index.vue

<template ref="indexPage">
  <q-page class="row items-center justify-evenly">
    <div>
        ...
    </div>
  </q-page>
</template>

<script lang="ts">
/* eslint-disable vue/require-default-prop */
import { useQuery } from '@vue/apollo-composable'
import { defineComponent, onMounted, ref } from 'vue'
import { GET_RESOURCES, GET_RESOURCES_LENGTH } from '../gql/resource/queries'
import { Loading } from 'quasar'

export default defineComponent({
  name: 'PageIndex',
  props: {
    formats: {
      type: Array
    },
    tags: {
      type: Array
    },
    levels: {
      type: Array
    },
    keyword: {
      type: String
    },
    languages: {
      type: Array
    }
  },
  setup (props) {
    const apiIsUp = ref<boolean>(true)
    const { onError } = useQuery(GET_RESOURCES, { limit: 1 })
    onError(() => {
      apiIsUp.value = false
    })
    // Read envs for page state
    const onDevice = ref<any>(process.env.ONDEVICE)
    // Loading boolean in case the api is very fast, the UI still loads for a lil bit - better User Experience
    const limit = ref<number>(250)
    const disableButton = ref<boolean>(true)
    // Fetch resources query
    const {
      result: fetchedResources,
      loading: fetchResourcesLoading,
      refetch: fetchResources
    } = useQuery(GET_RESOURCES, { limit: 250 })
    const {
      result: fetchedResourcesLength,
      loading: fetchResourcesLengthLoading,
      refetch: fetchResourcesLength
    } = useQuery(GET_RESOURCES_LENGTH, {})

    // On mount, enable loading and fetch resources
    onMounted(async () => {
      await fetchResources()
    })

    // Enable loading and filter resources according to all inputs
    const fetchFilteredResources = async (
      keyword:string = props.keyword!,
      formats: string[] = props.formats! as string[],
      languages: string[] = props.languages as string[],
      tags: string[] = props.tags as string[],
      levels: string[] = props.levels as string[]) => {
      Loading.show()
      await fetchResourcesLength(
        {
          keyword,
          languages,
          formats,
          tags,
          levels
        }
      )
      await fetchResources({
        keyword,
        languages,
        formats,
        tags,
        levels,
        limit: limit.value
      } as any)
      Loading.hide()
    }

    // ...

    return {
      apiIsUp,
      disableButton,
      fetchedResources,
      fetchFilteredResources,
      fetchResourcesLoading,
      fetchedResourcesLength,
      fetchResourcesLengthLoading,
      limit,
      loadMore,
      onDevice,
      redirect
    }
  }
})
</script>

./src/layouts/MainLayout.vue

<template>
  <q-layout view="lHh Lpr lFf">

      // ...

    <q-drawer
      v-model="leftDrawerOpen"
      show-if-above
      v-if="isInIndex"
      bordered
      class="bg-white"
    >
      <q-list>
            <q-item-label
            header
            class="text-grey-8 text-h5 text-center q-mt-md"
            >
            {{ $t('search_options') }}
            </q-item-label>
            <q-separator class="q-mt-md" />

        <q-input
          outlined
          class="q-mt-lg q-mx-auto w-90"
          v-model="keyword"
          clearable
          @keyup="searchResourcesString"
          :label="$t('keywords')"
        />
        <q-select
          class="w-90 q-mx-auto q-mt-md"
          outlined
          v-if="fetchedLanguages"
          v-model="selectedLanguages"
          :label="$t('languages')"
          :option-label="(lang) => lang.language"
          :option-value="(lang) => lang.id"
          :options="fetchedLanguages.languages"
          @update:model-value="searchResources"
          multiple
          map-options
          emit-value
        >
          <template #option="{ itemProps, opt, selected, toggleOption }">
            <q-item
              v-bind="itemProps"
              v-on="itemProps"
            >
              <q-item-section>
                <q-item-label v-html="opt.language" />
              </q-item-section>
              <q-item-section side>
                <q-toggle
                  :model-value="selected"
                  @update:model-value="toggleOption(opt)"
                />
              </q-item-section>
            </q-item>
          </template>
        </q-select>

    // ...

    <q-page-container>
      <router-view
        ref="view"
        :keyword="keyword"
        :formats="selectedFormats"
        :tags="selectedTags"
        :levels="selectedLevels"
        :languages="selectedLanguages"
      />
    </q-page-container>
  </q-layout>
</template>

<script lang="ts">

import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { GET_LANGUAGES } from '../gql/language/queries'
import { GET_FORMATS } from '../gql/format/queries'
import { GET_TAGS } from '../gql/tag/queries'
import { GET_LEVELS } from '../gql/level/queries'
import { GET_RESOURCES_LENGTH } from '../gql/resource/queries'
import { Quasar, useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'MainLayout',
  setup () {
    const $q = useQuasar()
    const $router = useRouter()
    const keyword = ref<string>('')
    // Router view reference in order to call method from parent to child
    const view = ref<any>(null)
    // Selected languages for select dropdown - IDs
    const selectedLanguages = ref<string[]>([])
    // Selected formats
    const selectedFormats = ref<[]>([])
    // Selected tags
    const selectedTags = ref<string[]>([])
    // Selected level
    const selectedLevels = ref<string[]>([])

    // ...

    // Fetch languages query
    const { result: fetchedLanguages, loading: fetchLanguagesLoading, refetch: fetchLanguages } = useQuery(GET_LANGUAGES)
    // Fetch formats query
    const { result: fetchedFormats, loading: fetchFormatsLoading, refetch: fetchFormats } = useQuery(GET_FORMATS)
    // Fetch tags query
    const { result: fetchedTags, loading: fetchTagsLoading, refetch: fetchTags } = useQuery(GET_TAGS)
    // Fetch level query
    const { result: fetchedLevels, loading: fetchLevelsLoading, refetch: fetchLevels } = useQuery(GET_LEVELS)
    // Fetch resources query
    const {
      result: fetchedResourcesLength
    } = useQuery(GET_RESOURCES_LENGTH, {})

    onMounted(async () => {
      await fetchLanguages()
      await fetchFormats()
      await fetchTags()
      await fetchLevels()
    })
    // If keyword input is cleared, then execute the query
    watch(() => keyword.value, (newValue) => {
      if (newValue === null) {
        searchResources()
      }
    })

    // Method to call fetchFilteredResources from parent to child
    const searchResources = async () => {
      view.value.fetchFilteredResources(keyword.value, selectedFormats.value, selectedLanguages.value, selectedTags.value, selectedLevels.value)
    }

    // Method to call fetchFilteredResources from parent to child
    const searchResourcesString = async () => {
      if (!searching.value) {
        searching.value = true
        await delay(1000)
        view.value.fetchFilteredResources(keyword.value, selectedFormats.value, selectedLanguages.value, selectedTags.value, selectedLevels.value)
        searching.value = false
      }
    }

    return {
      changeLanguage,
      fetchedLanguages,
      fetchFormatsLoading,
      fetchLanguagesLoading,
      fetchedResourcesLength,
      fetchedTags,
      fetchTagsLoading,
      fetchedLevels,
      fetchLevelsLoading,
      fetchedFormats,
      isInIndex,
      keyword,
      languages,
      leftDrawerOpen,
      onDevice,
      resetInputs,
      selectedLanguages,
      selectedFormats,
      selectedTags,
      selectedLevels,
      selectedLanguage,
      searchResources,
      searchResourcesString,
      view
    }
  }
})
</script>

有什么想法可以通过最小的更改恢复功能(我希望只是让它工作而不是必须进行任何重构)?

vue-router 4 的迁移指南没有明确说明,但您不能再通过 <router-view> 标记本身引用 child 组件。提到了一种用于处理 child 的插槽的新范例,它也适用于您的用例;这是调整后的代码:

      <router-view
        v-slot="{ Component, route }"
        :keyword="keyword"
        :formats="selectedFormats"
        :tags="selectedTags"
        :levels="selectedLevels"
        :languages="selectedLanguages"
      >
        <component
          :is="Component"
          :key="route.meta.usePathKey ? route.path : undefined"
          ref="view"
        />
      </router-view>