异步组件、动态插槽

11 阅读3分钟

异步加载的组件

代码

import { ref, defineAsyncComponent, watch, type Component } from 'vue'

interface TabsConfigItem {
    key: ENUM_DETAIL_TAB
    tab: string
    component: Component
}

// Tab 栏
export const enum ENUM_DETAIL_TAB {
    accountDetail = 'accountDetail',
    relatedTransaction = 'relatedTransaction',
    historicalCase = 'historicalCase',
    processLog = 'processLog'
}

/**
 * @constant tabsConfig
 * @description 动态组件的核心配置
 * 定义了每个 Tab 的唯一 key(引用枚举值)、显示的标题以及对应的异步加载组件。
 */
const tabsConfig: TabsConfigItem[] = [
    // 账户详情
    {
        key: ENUM_DETAIL_TAB.accountDetail,
        tab: riskManagementT('accountDetail'),
        component: defineAsyncComponent(() => import('./accountDetail.vue'))
    },
    // 关联交易
    {
        key: ENUM_DETAIL_TAB.relatedTransaction,
        tab: riskManagementT('relatedTransaction'),
        component: defineAsyncComponent(() => import('./relatedTransaction.vue'))
    },
    // 历史案件
    {
        key: ENUM_DETAIL_TAB.historicalCase,
        tab: riskManagementT('historicalCase'),
        component: defineAsyncComponent(() => import('./historicalCase.vue'))
    },
    // 处理日志
    {
        key: ENUM_DETAIL_TAB.processLog,
        tab: riskManagementT('processLog'),
        component: defineAsyncComponent(() => import('./processLog.vue'))
    }
]


// 从核心配置中派生出 Tab 列表和组件映射
const { tabList, tabComponentsMap } = tabsConfig.reduce(
    (acc, tab) => {
        const { key, tab: tabTitle, component } = tab
        acc.tabList.push({ key, tab: tabTitle })
        acc.tabComponentsMap[key] = component
        return acc
    },
    {
        tabList: [] as { key: ENUM_DETAIL_TAB; tab: string }[],
        tabComponentsMap: {} as Record<ENUM_DETAIL_TAB, Component>
    }
)

// 当前激活的 Tab,默认为账户详情
const activeKey = ref<ENUM_DETAIL_TAB>(ENUM_DETAIL_TAB.accountDetail)

defineAsyncComponentVue 3 的 Composition API​ 中,用来定义异步组件的函数。

它的作用可以概括为:让组件在需要时才加载,而不是一开始就打包进主包,从而减小首屏体积、提升加载速度


1. 基本作用

  • 把组件变成一个异步加载的组件

    在路由或父组件渲染时,才会去请求并加载这个组件的代码。

  • 内部基于 动态 import() ​ 实现,配合构建工具(Vite/Webpack)自动做代码分割(code splitting)


2. 基本用法

import { defineAsyncComponent } from 'vue'

// 方式1:简单动态 import
const AsyncComp = defineAsyncComponent(() => import('./MyComponent.vue'))

// 方式2:带加载/错误状态的配置
const AsyncComp = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  loadingComponent: LoadingSpinner,   // 加载中显示的组件
  errorComponent: ErrorDisplay,       // 加载失败时显示的组件
  delay: 200,                         // 延迟多少 ms 才显示 loading
  timeout: 3000                       // 超时时间
})

3. 使用场景

  • 路由懒加载

    const routes = [
      {
        path: '/dashboard',
        component: () => import('./views/Dashboard.vue')
      }
    ]
    

    在路由配置里,其实也是用了类似的异步组件思想。

  • 大型页面/低频功能

    比如“报表弹窗”“富文本编辑器”等,只有用户点开时才加载,减少首屏 JS 体积。

  • 需要加载状态/错误处理时

    loadingComponenterrorComponent给用户更好的体验。


4. 和 import()的区别

  • import()返回的是 Promise<Component>,需要你自己处理加载/错误状态。
  • defineAsyncComponent是对 import()封装,帮你更方便地配置加载中、错误、超时等行为,并且在 Vue 组件树里使用时更符合 Vue 的生命周期管理。

一句话总结

defineAsyncComponent的作用是 把组件变成按需异步加载的组件,既能减小首屏体积,又能优雅处理加载中和加载失败的情况,是 Vue 3 性能优化的重要工具。


动态插槽

通过 v-for 循环动态生成所有需要批量输入的具名插槽。#[item.field] 是 Vue 的动态插槽名语法,它会根据 item.field 的值(如 'icAccountList')来决定要渲染哪个插槽,从而避免了在模板中重复编写每个插槽的 template。

原代码

 <!-- 发卡账户 -->
            <template #icAccount="{ model, field }">
                <a-input-group compact>
                    <a-input
                        v-model:value.trim="model[field]"
                        :placeholder="dataT('icAccount')"
                        allow-clear
                    />
                    <a-button type="primary" class="border-5" @click="openBatchInputModal(field)">
                        <template #icon>
                            <PlusCircleOutlined />
                        </template>
                    </a-button>
                </a-input-group>
            </template>
            <!-- 企业账号 -->
            <template #opIdList="{ model, field }">
                <a-input-group compact>
                    <a-input
                        v-model:value.trim="model[field]"
                        :placeholder="dataT('opIds')"
                        allow-clear
                    />
                    <a-button type="primary" class="border-5" @click="openBatchInputModal(field)">
                        <template #icon>
                            <PlusCircleOutlined />
                        </template>
                    </a-button>
                </a-input-group>
            </template>

优化后

 <template
                v-for="item in editingList"
                :key="item.field"
                #[item.field]="{ model, field }"
            >
                <a-input-group compact>
                    <a-input
                        v-model:value.trim="model[field]"
                        :placeholder="item?.placeholder"
                        allow-clear
                        @input="handleInput($event, field)"
                    />
                    <a-button type="primary" class="border-5" @click="openBatchInputModal(field)">
                        <template #icon>
                            <PlusCircleOutlined />
                        </template>
                    </a-button>
                </a-input-group>
            </template>