在 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>
我已经从 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>