动态组件&异步组件

101 阅读3分钟

前言

问题起源于我们的一个页面,初始化加载资源过多,导致页面的Load时间过长

问题分析

  • 页面由大量模块组成
  • 所有模块是同时进行加载
  • 每个模块的依赖资源较多(包括js文件,接口文件,css文件等)

image.png

应用场景

在项目中,在一个页面中有大量业务需要处理,我们可能需要拆分业务细化为更小的组件,并且仅在需要的时候再加载相关组件。

组件理解

component

一个用于渲染动态组件或元素的“元组件”,要渲染的实际组件由is决定

  • is是字符串,它既可以是 HTML 标签名也可以是组件的注册名。
  • 或者,is 也可以直接绑定到组件的定义。

示例

<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

<template>
   // 渲染组件
  <component :is="Math.random() > 0.5 ? Foo : Bar" />
  // 渲染HTML
  <component :is="href ? 'a' : 'span'"></component>
</template>

defineAsyncComponent

defineAsyncComponent 方法接收一个返回 Promise 的加载函数。

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

示例

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

解决思路

  • 业务组件化
  1. 将各模块拆分未组件模块,每个模块之间降低耦合
  2. 组件依赖的资源全部封装在组件内部进行调用
  • 组件懒加载
  1. 优先加载可见模块
  2. 其余不可见模块懒加载,待需要时加载

实现原理

// 父组件
<template>
    <!-- 动态组件 -->
    <component v-if="currentComponentsName" :is="components[currentComponentsName]" ref="componentRef" @close="componentClose" />
</template>

<script setup lang="ts">
import { defineAsyncComponent } from "vue";

// 当前组件 Ref
const componentRef = ref();
// 当前组件名称
const currentComponentsName = ref("");
// 初始化组件传入的参数
let currentComponentsParams = {};
// 动态组件引入,使用时候才加载vue页面
const components = {
  PatientRecallVue: defineAsyncComponent(() => import("./PIQComps/PatientRecall.vue")),
  UrgentValueRecordVue: defineAsyncComponent(() => import("./PIQComps/UrgentValueRecord.vue")),
  TransferTimeVue: defineAsyncComponent(() => import("./PIQComps/TransferTime.vue")),
  QCBatchNoVue: defineAsyncComponent(() => import("./PIQComps/QCBatchNo.vue")),
  EmergencyRelateVue: defineAsyncComponent(() => import("./PIQComps/EmergencyRelate.vue")),
  DataBatchCopyVue: defineAsyncComponent(() => import("./PIQComps/DataBatchCopy.vue")),
  ModifyReportRemarkVue: defineAsyncComponent(() => import("./PIQComps/ModifyReportRemark.vue")),
  DelayedReportVue: defineAsyncComponent(() => import("./PIQComps/DelayedReport.vue")),
  BatchAddCombinationsVue: defineAsyncComponent(() => import("./PIQComps/BatchAddCombinations.vue")),
  SeparateCombineVue: defineAsyncComponent(() => import("./PIQComps/SeparateCombine.vue"))
};
// 解决弹框组件 重复触发当前组件失效问题
const componentClose = () => {
  currentComponentsName.value = "";
  componentRef.value = null;
};
// 确保动态组件渲染完成,再初始化数据,防止 Initialization 方法未找到
watch(
  () => componentRef.value,
  (newValue, oldValue) => {
    /* ... */
    if (!newValue) {
      return;
    }
    componentRef.value.Initialization(currentComponentsParams);
  }
);
</script>
// 子组件 PatientRecall.vue
<template>
  <el-dialog
    title="召回病人"
    width="600px"
    center
    v-model="state.visible"
    draggable
    :close-on-click-modal="false"
    destroy-on-close
    @close="dialogClose"
  >
    <el-form class="mt10" label-width="80px">
      <el-form-item label="姓名" prop="pidName">
        <el-input v-model="state.ParentData.formData.pidName" readonly></el-input>
      </el-form-item>
      <el-form-item label="样本号" prop="repSid">
        <el-input v-model="state.ParentData.formData.repSid" readonly></el-input>
      </el-form-item>
      <el-form-item label="召回原因" prop="obrValueD">
        <el-input
          type="textarea"
          :autosize="{ minRows: 6, maxRows: 12 }"
          v-model="state.ParentData.formData.obrValueD"
        ></el-input>
      </el-form-item>
    </el-form>
    <el-tag class="mb10" effect="dark">召回记录</el-tag>
    <el-table :data="state.ParentData.tableData" height="200px">
      <el-table-column label="召回人" prop="obrSendUserName" align="center"></el-table-column>
      <el-table-column label="召回时间" prop="obrCreateTime" align="center"></el-table-column>
      <el-table-column label="召回内容" prop="obrValueD" align="center"></el-table-column>
    </el-table>
    <template #footer>
      <el-button @click="cancelBtn">取消</el-button>
      <el-button type="primary" @click="confirmBtn"> 确认 </el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ElMessage } from "element-plus";
import { SaveCallBackPatient } from "@/api/InspectionManagement/PatMenu";

const emits = defineEmits(["cancel", "confirm", "close"]);

const state = reactive({
  ParentData: {
    currentRow: {},
    currentUserInfo: {},
    formData: {},
    tableData: {}
  } as any,
  visible: false
});

const dialogClose = () => {
  emits("close");
};

const cancelBtn = () => {
  emits("cancel");
  state.visible = false;
};
const confirmBtn = () => {
  // 召回原因不能为空
  if (!state.ParentData.formData.obrValueD) {
    ElMessage.info("请输入召回原因");
    return;
  }
  const { currentRow, currentUserInfo, formData } = state.ParentData;
  const params: any = {
    callReason: formData.obrValueD,
    repId: currentRow.repId,
    sendUserId: currentUserInfo.userLoginid,
    sendUserName: currentUserInfo.userName,
    typeName: currentRow.patCtypeName ? currentRow.patCtypeName : "检验科"
  };
  SaveCallBackPatient(params).then((res: { success: any; msg: any }) => {
    const { success, msg } = res;
    if (success) {
      ElMessage.success("召回成功");
      cancelBtn();
    } else {
      ElMessage({
        type: "error",
        dangerouslyUseHTMLString: true,
        message: msg
      });
    }
  });
};

const Initialization = (initParam: { currentRow: any; currentUserInfo: any; formData: any; tableData: any }) => {
  state.ParentData = initParam;
  state.visible = true;
};

defineExpose({
  Initialization
});
</script>

<style scoped></style>