拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

动态组件与异步加载:从原理到实践详解

动态组件与异步加载:从原理到实践详解

动态组件这个题目,我拆解的时候其实是有一点感慨的。因为很多前端开发做了两三年,组件化的静态渲染玩得很溜,但一碰到"运行时才知道渲染什么组件"这种需求,还是容易懵。动态组件不是炫技,它是真实业务场景里绕不开的一块硬骨头。这篇文章我不想讲那些浮在表面的API用法,而是从原理到实践,把动态组件和动态组件加载这件事掰开揉碎了讲清楚,你会明白它背后的设计逻辑,也能直接拿里面的方案去解决自己项目里的问题。

1. 动态组件到底是什么:一个日常开发中的高频痛点

1.1 从静态渲染到动态切换的需求演变

先从一个最朴素的需求说起。假设你做一个工单审核页面,这个工单有文字描述、有图片附件、有视频链接。老板说,工单类型不同,展示的模块顺序和形式也要不一样。你当然可以写一大堆v-if去判断当前是什么类型,然后手动罗列所有可能的组件。但问题来了:如果工单类型有十几种,甚至产品经理说下周还要加新的类型,你的v-if链会膨胀成一座屎山。

我见过很多项目的代码就是这么烂掉的。一个渲染工单的模板里有十几个v-if,每个v-if绑定一个组件,新增类型就得改模板、改逻辑。这种代码改起来我们内部叫"扫雷式开发"——你不知道哪次改动会踩到哪个分支。

动态组件的思路完全不一样:与其在模板里把所有可能性都枚举出来,不如把"渲染什么组件"变成一个运行时才确定的值。本质上就是让模板的某个位置成为一个"插槽",这个插槽在运行时根据配置动态地决定具体渲染哪个组件。这种模式在前端领域有一个专门的名字,叫依赖反转——调用方不再依赖具体的组件类,而是依赖一个"抽象的组件接口"。

这种设计解决了三个核心痛点:

  • 代码的扩展性:新增一个工单类型,只需要新增一个组件文件,并在类型映射表里注册一下,模板和公共逻辑完全不用改动
  • 配置与实现的解耦:页面结构可以由后端下发的配置来决定,前端只负责"根据配置渲染组件"这件执行的事
  • 心智负担降低:模板里不再堆积成山的v-if,一眼就能看出这个位置的渲染是动态的、可配置的

1.2 动态组件和条件渲染的本质区别

有些同学可能会有疑问:v-if不也是动态的吗?我根据条件渲染不同的子组件,这跟动态组件有什么区别?

有区别,而且区别很大。v-if是"在多个已知候选中做选择",它是静态的决策树;动态组件是"在运行时接收一个组件名或组件对象,然后渲染它",它是动态的寻址表。打个比方:v-if就像你在菜单上点菜,菜单上的东西是固定的,你只是在里面选;动态组件就像你拿着一个"菜品编号",后厨根据这个编号去查中央厨房的物料数据库,如果这个编号已经注册过,就能出菜,即使这个编号对应的菜品是上周刚研发的新品,也不用改后厨的流程。

这里最关键的思维转换是:动态组件的选择范围是开放的,而v-if的选择范围是封闭的。这意味着你的系统可以通过"注册"机制持续扩展,而不是通过"修改"机制来适配变化。在我参与过的低代码平台项目中,动态组件是整个系统的地基,因为它直接决定了平台上能创建出多少种业务组件。

2. 核心实现方案:Vue和React中的动态组件写法

2.1 Vue中的component标签与is属性

在Vue 3中,动态组件的标准写法是使用<component>标签配合:is属性。这个is属性可以是一个字符串(组件的注册名称),也可以直接是一个组件对象。

<template> <component :is="currentComponent" v-bind="componentProps" /> </template> <script setup> import { ref, computed } from 'vue' import TextBlock from './components/TextBlock.vue' import ImageBlock from './components/ImageBlock.vue' import VideoBlock from './components/VideoBlock.vue' const blockType = ref('text') const componentMap = { text: TextBlock, image: ImageBlock, video: VideoBlock } const currentComponent = computed(() => componentMap[blockType.value]) const componentProps = computed(() => { // 根据当前组件类型返回对应的props if (blockType.value === 'text') { return { content: '这里是文本内容' } } if (blockType.value === 'image') { return { src: 'https://example.com/img.jpg', alt: '示例图片' } } return { videoUrl: 'https://example.com/video.mp4', autoplay: false } }) </script>

这里有一个重要的细节:componentMap里的值应该是组件对象,而不是注册名。直接传对象有几个好处:

  • 组件的类型是确定的,IDE可以做类型检查
  • Tree-shaking可以生效,没有用到的组件不会被打包进去
  • 不依赖全局注册,组件作用域清晰,代码好维护

Vue 2时代很多人习惯用全局注册的组件名字符串来动态渲染,这在项目规模小的时候没毛病,但一旦项目复杂起来,全局命名空间容易冲突,而且你很难追踪某个动态组件到底在哪些地方被用到。我自己的习惯是,能用对象传值就不传字符串。

需要注意的一点是,当:is的值在多个组件之间切换时,Vue默认会复用同一个元素节点。这在某些场景下会引发状态残留问题。比如你在文本组件里输入了一行字,切换到图片组件再切回来,文本还在。这通常是预期行为,但如果你希望每次切换都重建组件,可以通过:key属性强制区分:

<component :is="currentComponent" :key="blockType" v-bind="componentProps" />

加了:key之后,每次blockType变化,Vue都会销毁旧组件实例并创建新实例,状态不会被复用。

2.2 React中的动态组件方案

React中实现动态组件的思路和Vue不太一样。React的组件本质上是函数或类,所以动态渲染一个组件,其实就是动态渲染一个变量。你可以把这个变量当作JSX的标签名使用,但有一个约定俗成的规则:变量名必须首字母大写,否则React会把它当作原生HTML标签处理。

import TextBlock from './components/TextBlock' import ImageBlock from './components/ImageBlock' import VideoBlock from './components/VideoBlock' const componentMap = { text: TextBlock, image: ImageBlock, video: VideoBlock } function BlockRenderer({ blockType, props }) { const Component = componentMap[blockType] if (!Component) { return <div>未支持的区块类型: {blockType}</div> } return <Component {...props} /> }

这段代码的精华在const Component = componentMap[blockType]这一行。如果直接在JSX里写<componentMap[blockType] />,React会直接报错或者把它当成一个名为componentMap的HTML标签,这是初学者最常踩的坑。

React里还有另一种高阶玩法:使用React.createElement动态创建元素。这个方法接收三个参数——类型、props、子节点:

function DynamicRenderer({ componentType, props }) { return React.createElement(componentMap[componentType], props) }

React.createElement比较适合在非JSX环境下使用,比如你需要根据配置文件来生成组件时。但在正常的JSX代码里,直接用大写变量名更清晰、更好读。

React 16.3版本之后还支持了<Fragment>快捷键,这在动态组件中也有它的用处。当你需要动态渲染多个组件时,可以用一个数组加map来生成组件列表:

function DynamicList({ configs }) { return configs.map((config, index) => { const Component = componentMap[config.type] return <Component key={index} {...config.props} /> }) }

React和Vue的动态组件实现思路殊途同归,核心都是"用一个映射表把业务标识符映射到具体的组件类",然后"在运行时根据标识符取用组件"。理解了这一层,框架差异就只是语法层面的问题了。

2.3 为什么这样设计:虚拟节点与渲染器的配合

聊完具体写法,我们要往底层挖一挖:为什么框架能支持动态组件?这就要提到虚拟节点(VNode)的设计了。

在Vue和React里,组件的渲染最终都会产出一个虚拟节点描述对象。这个对象只有两个核心字段我们最关心:type表示节点的类型(可以是原生标签名,也可以是组件对象),props表示传给节点的属性。渲染器在拿到这个VNode之后,会检查type的类型,如果是字符串就走原生DOM渲染逻辑,如果是对象或函数就走组件实例化逻辑。

动态组件的Vue代码<component :is="currentComponent" />,编译后其实就是在创建VNode时,把currentComponent的值赋值给了type字段。Vue的is属性是一个语法糖,它让开发者可以不用直接操作VNode,但底层逻辑是一样的。

这个设计的精妙之处在于:渲染器根本不关心"组件是静态的还是动态的",它只按VNode的type字段工作。所以动态组件在框架层面没有任何特殊处理,它只是"VNode的type在运行时才确定"这个事实的自然推论。理解了这一点,你再去读渲染器的源码,就不会觉得一头雾水。

Vue 3的编译器有一个值得注意的优化点:静态提升。如果你的<component :is="currentComponent" />是静态的(不依赖任何响应式数据),编译器会把VNode的创建提升到render函数之外,避免每次渲染都重新创建。但如果:is绑定了响应式变量,编译器无法静态分析,就会保留为动态创建。这就是为什么很多性能分析报告会建议尽量把动态组件的范围缩小,让编译器有更多静态优化的空间。

3. 动态组件加载的进阶玩法:异步组件与性能优化

3.1 异步组件的原理与使用场景

动态组件解决了"运行时渲染哪个组件"的问题,但如果你的组件库体积非常大,全部打包进一个JS文件里,首屏加载时间会变得很漫长。这个时候就需要动态组件加载的进阶技能:异步组件。

异步组件的核心思想是:在组件真正要被渲染之前,不下载它的代码。只有当运行时确认需要渲染某个组件时,才通过网络去加载对应组件的代码块。这种按需加载的模式,业内叫代码分割。

在Vue 3中,定义一个异步组件很简单:

import { defineAsyncComponent } from 'vue' const AsyncTextBlock = defineAsyncComponent(() => import('./components/TextBlock.vue')) const AsyncImageBlock = defineAsyncComponent(() => import('./components/ImageBlock.vue'))

关键点在于import()函数,它是Webpack和Vite支持的原生动态导入语法。Webpack遇到import()会把它单独打包成一个chunk文件,Vite则天然支持ES Module的按需加载。你只需要在组件映射表里使用异步组件,就可以做到"用到才下载":

const componentMap = { text: AsyncTextBlock, image: AsyncImageBlock, video: AsyncVideoBlock }

React中对应的方案是React.lazy()加Suspense:

const AsyncTextBlock = React.lazy(() => import('./components/TextBlock'))

然后在渲染的时候用Suspense包裹,在组件加载完成之前显示一个fallback:

<Suspense fallback={<div>加载中...</div>}> <BlockRenderer blockType={blockType} props={props} /> </Suspense>

这里我特别想多说一句实战经验:并不是所有组件都适合做成异步的。异步组件引入了额外的加载时间,如果这个组件在首屏就会用到,把它异步化反而会拖慢首屏渲染。我一般遵循三条判断标准:

  • 首屏不需要渲染的组件,可以做异步
  • 体积超过5KB gzip的组件,在做异步前先审视一下是否有优化空间
  • 业务低频且独立的组件,比如"导出Excel"、"打印预览",做异步收益最大

3.2 代码分割与按需加载

要理解异步组件,就不能不懂代码分割。这里我用一个类比:你去超市购物,如果超市把所有的货品都搬到你家里放着,你家就成了仓库,出门都困难。合理的做法是你列一个购物清单,只把你需要的东西买回来。代码分割就是这个"购物清单"机制。

在没有代码分割时,一个普通的Vue项目打包后可能是一个1MB甚至更大的主JS文件。用户打开网页,必须等这1MB全部下载完才能看到页面。如果其中包含一个超大图表库,而大部分用户根本用不到图表功能,那他们就白白浪费了网络流量和时间。

代码分割把大的JS包拆成若干个小chunk,初始加载只下载首屏必需的chunk,其他chunk在路由切换或组件需要时才下载。异步组件是代码分割在组件维度上的应用。

Vite项目里,Webpack和Vite对动态导入的处理方式略有差异,但最终效果都是生成独立chunk。如果你用Vite,还需要注意chunk的命名策略:

// vite.config.js export default { build: { rollupOptions: { output: { chunkFileNames: 'assets/[name]-[hash].js' } } } }

[name]是chunk的含义化名称,[hash]是内容哈希。内容哈希的好处是,当代码内容不变时,浏览器可以继续使用缓存;只有内容变化了,才会重新下载。这可以显著减少重复加载。

在实际项目中,我见过有些团队把组件拆得太碎,每个组件都单独一个chunk。这样做的负面影响是:HTTP请求数量爆炸,浏览器在同一时间对同一域名的并发连接数有限制,请求多了反而会排队,拖慢加载速度。我的建议是:把同一业务域下的组件归到一个chunk里,用Webpack的魔法注释控制分割粒度:

const AsyncFormBlocks = defineAsyncComponent(() => import(/* webpackChunkName: "form-blocks" */ './blocks/index.js') )

3.3 加载状态与错误处理

异步组件带来性能优化的同时,也带来了新的问题:组件加载失败怎么办?加载过程中网络卡顿怎么办?真实项目中网络不可能永远稳定,所以加载状态和错误处理必须考虑。

Vue的defineAsyncComponent支持传入一个配置对象,你可以自定义加载延迟、超时时间和错误组件:

const AsyncTextBlock = defineAsyncComponent({ loader: () => import('./components/TextBlock.vue'), // 延迟显示loading的时间,防止闪烁 delay: 200, // 超时时间,超过则认为加载失败 timeout: 10000, loadingComponent: () => import('./components/LoadingPlaceholder.vue'), errorComponent: () => import('./components/ErrorFallback.vue') })

delay选项容易被忽略,但其实它很重要。如果组件加载很快(比如从缓存加载,几十毫秒就完成了),你显示loading组件就会产生一个闪烁。设置一个200ms的延迟,意思是如果200ms内加载完成,就不显示loading组件了,用户在视觉上根本没有感知。

React中的错误处理稍微复杂一些。Suspense只负责加载状态的展示,无法捕获组件加载失败的错误。你需要使用React 16.8+的Error Boundary(错误边界)来捕获子组件的渲染错误:

class ErrorBoundary extends React.Component { state = { hasError: false } static getDerivedStateFromError() { return { hasError: true } } componentDidCatch(error, errorInfo) { console.error('组件加载失败:', error, errorInfo) } render() { if (this.state.hasError) { return this.props.fallback || <div>组件加载失败</div> } return this.props.children } } // 使用 <ErrorBoundary fallback={<div>出错了</div>}> <Suspense fallback={<div>加载中...</div>}> <BlockRenderer blockType={blockType} props={props} /> </Suspense> </ErrorBoundary>

错误边界还有一种用法值得注意:它可以捕获所有子组件的渲染期错误,不只是异步加载错误。所以即使你不用React.lazy,在动态组件的公共渲染层加一个错误边界也是很好的兜底策略。有一次我在生产环境遇到一个诡异的线上问题:某个配置下发的组件因数据异常渲染崩溃,因为当时没有错误边界,整个页面白屏,用户只能刷新。后来我在所有动态渲染的出口都加了错误边界,即使某个组件崩了,也只是那个区块变成错误提示,页面其他部分依然可用。

4. 高频场景实操:从表单渲染到标签页切换

4.1 场景一:动态表单项渲染

动态组件在日常业务中最高频的场景之一就是动态表单项。一个表单可能是多种类型的字段组合:文本框、下拉框、日期选择器、单选框、文件上传、甚至自定义的富文本编辑器。传统写法是把所有类型的输入控件全部塞进模板,然后通过v-if控制显示。但这种方式在字段类型增长到一定程度后,模板会变得非常臃肿。

用动态组件改造后,整个渲染逻辑就清晰了:

<template> <div class="dynamic-form"> <component v-for="field in formFields" :is="getFieldComponent(field.type)" :key="field.name" :field="field" :value="formData[field.name]" @update:value="handleFieldUpdate(field.name, $event)" /> </div> </template> <script setup> import InputField from './fields/InputField.vue' import SelectField from './fields/SelectField.vue' import DateField from './fields/DateField.vue' import UploadField from './fields/UploadField.vue' const fieldComponentMap = { input: InputField, select: SelectField, date: DateField, upload: UploadField } function getFieldComponent(type) { return fieldComponentMap[type] || InputField } </script>

这里我给每个字段组件定了统一的数据协议:props接受field描述对象和value当前值,通过事件向外传更新。有了这个统一协议,新增字段类型时,只需要实现这套协议的新组件,然后在fieldComponentMap里注册。设计逻辑和后台管理系统的字段配置完全解耦了。

我特别想强调一个细节:不要把getFieldComponent的返回值直接内联在模板里,而是让它返回一个确定的组件对象。如果写成:is="fieldComponentMap[field.type]",当映射表里没有对应类型时,:is的值为undefined,Vue会渲染成一个注释节点,整个字段就消失了。用户不知道那里应该有一个字段,就会觉得表单"莫名少了一块"。加一个兜底组件是实用性很强的习惯。

动态表单项在做工单系统的时候帮了大忙。当时产品经理提了一个需求:不同渠道提交的工单有不同的附加字段。比如线上渠道提交的工单,需要额外填一个"订单号"字段;电话渠道提交的工单,需要填"客服编号"。如果用静态表单,每个渠道都要写一个表单组件,而且要维护各自的数据逻辑。用动态表单配置驱动之后,渠道字段只需要在配置文件里增删字段描述对象,加一个类型也就是新增一个组件的事。

4.2 场景二:多标签页应用

多标签页应用是动态组件的另一个经典场景。常见的实现方式是:标签栏固定,点击不同标签切换内容区域。虽然Vue有强大的内置组件keep-alive,但很多人不了解它跟动态组件结合时的细节。

这里给出一个带缓存功能的多标签页实现:

<template> <div class="tab-container"> <div class="tab-nav"> <div v-for="tab in tabs" :key="tab.name" class="tab-item" :class="{ active: activeTab === tab.name }" @click="switchTab(tab.name)" > {{ tab.label }} </div> </div> <div class="tab-content"> <keep-alive :include="cachedTabs"> <component :is="activeTabComponent" :key="activeTab" /> </keep-alive> </div> </div> </template> <script setup> import { ref, computed } from 'vue' const tabs = [ { name: 'overview', label: '总览', component: Overview }, { name: 'details', label: '明细', component: Details }, { name: 'logs', label: '操作日志', component: OperationLogs } ] const activeTab = ref('overview') const activeTabComponent = computed(() => { const tab = tabs.find(item => item.name === activeTab.value) return tab ? tab.component : null }) // 需要缓存的标签页 const cachedTabs = computed(() => tabs.filter(tab => tab.cache).map(tab => tab.name) ) </script>

keep-alive包裹动态组件时,include属性的值应该是对应组件的name,而不是标签的name。当你发现标签页没有生效缓存时,先检查组件是否定义了name,这是这个场景下最容易踩的坑。还有一点要注意,include匹配是按组件名精确匹配的,如果你的组件用了匿名函数式定义(export default {}),没有显式设置name,Vue会尝试从变量名推断,这在对组件做转换时常常导致缓存失效。

多标签页应用切出去再切回来,往往希望保留用户之前填写的表单数据或者滚动位置,这正是keep-alive存在的意义。但它的代价是内存占用增加,如果你的应用有大量标签页且每个都很大,切忌无脑缓存所有标签页。可以做上限控制,比如只缓存最近5个访问过的标签,用LRU算法淘汰最少使用的。

4.3 场景三:低代码平台的动态页面渲染

低代码平台是动态组件的终极舞台。我在设计页面渲染器时,核心思路就是:页面是一个组件树的结构化描述,树的每个节点就是一个组件实例,节点包含组件类型和属性配置。渲染器递归地遍历这棵树,为每个节点找到对应的组件并渲染。

这个组件的核心渲染逻辑可以用一个递归组件来实现。在这个渲染器中,节点是一个包含type和props的对象,children是子节点数组:

<template> <component :is="resolveComponent(node.type)" :...="node.props" > <template v-if="node.children?.length"> <DynamicNode v-for="child in node.children" :key="child.id" :node="child" /> </template> </component> </template> <script> export default { name: 'DynamicNode', props: { node: { type: Object, required: true } }, methods: { resolveComponent(type) { // 从已注册的组件库中查找 return this.$registry.get(type) || FallbackComponent } } } </script>

解析配置渲染页面有三大要素,这是我在实际项目中踩出来的经验:

  • 每个node必须有唯一id,否则组件状态和树更新会错乱
  • props的传递要支持两种形态:静态对象和动态计算函数。低代码配置里,有些属性依赖其他字段的值,比如"当表单类型是A时显示这个区块",这时候props需要通过一个求值引擎动态计算
  • 组件销毁时要有完整的生命周期通知,方便清理事件监听、定时器等资源

低代码平台的坑是和收益成正比的。平台一旦做起来,你可能会遇到几百种组件类型。这个时候组件映射表本身就需要动态化——你不能在代码里写死一个巨大的映射表,而是让后端的组件配置中心返回组件URL,前端根据URL动态加载组件脚本注册到渲染器中。这一层做深了,你的平台才能真正称得上"低代码"。

5. 常见问题与排查技巧实录

5.1 状态丢失问题:keep-alive与动态组件的恩怨情仇

动态组件配合keep-alive状态丢失是高频问题。很多人遇到的情况是:在动态组件中填入了一些数据,切换到别的组件再切回来,数据没了,组件回到了初始状态。

出现这个问题的根因往往在于:组件没有使用keep-alive缓存,或者缓存的key设置不对。这里我给出一个排查思路:

  • 确认是否使用了keep-alive,让组件实例得以保留
  • 确认keep-alive的include/exclude匹配是否正确,如果不匹配缓存不会生效
  • 确认组件切换时的key是否稳定。如果key每次都不同,Vue会认为是不同的组件节点,旧实例会被销毁

另外还有一个容易被忽视的因素:如果动态组件的父组件销毁了,keep-alive缓存也会一并清掉。比如你用v-if控制整个Tab内容区的显示,切到一个"完全不同的页面"再切回来,父组件已经重建,缓存自然不存在了。

解决方法是把Tab内容区用keep-alive缓存,并且给整个Tab组件加一个稳定的key,确保切换Tab时组件树不会整体销毁。

5.2 切换闪屏问题:加载态与过渡动画的平衡

动态组件加载慢,切换时出现白屏或闪一下,这个问题在异步组件场景下特别常见。打开浏览器控制台的Network面板,你会发现切换组件时才触发对应chunk的下载,下载期间页面是空白的。

优化思路有三个:

  • 预加载:在用户即将切换到某个组件之前,提前触发异步组件的加载。比如鼠标hover到Tab上时就执行动态import,而不是等到点击才加载
  • 加载占位组件占位:给异步组件提供loading组件,不要让它渲染空白
  • 过渡动画遮挡:给动态组件包裹一层Transition,切换时用淡入淡出动画掩盖加载过程

预加载的实现代码很简单,但很多人忽略了。比如Vue里你可以这样写:

const tabs = [ { name: 'logs', label: '操作日志', component: () => import('./components/OperationLogs.vue'), preload: () => import('./components/OperationLogs.vue') } ] // 在鼠标移入标签时触发预加载 function handleTabHover(tab) { if (tab.preload) { tab.preload().then(() => { // 主动完成预加载,之后切换就不会再等待 }) } }

这种方式的核心是以时间换空间:提前在空闲时把组件代码下载下来,让用户点击切换时感觉是"秒开"。

5.3 组件解析与注册失败排查

动态组件最常见的一种运行时报错是:Failed to resolve component: xxx。这个错误的产生原因通常是使用了字符串类型的组件名,但这个名称没有正确注册。排查时按以下顺序依次检查:

  • 确认组件是否已注册。Vue中局部注册需要引入组件后在components选项里声明,或者script setup中引入即可使用。如果你使用的是全局注册,检查app.component()的调用是否在组件实例化之前执行了
  • 确认组件名是否拼写一致。kebab-case和PascalCase是等价映射,但部分自动化工具生成的名字可能有坑
  • 确认动态导入路径是否正确。异步组件的loader里用了错误的相对路径,运行时才报错,这个最隐蔽。建议先打印一下import()的路径,确认resolve到的是正确的文件

React中对应的报错信息是Element type is invalid: expected a string ... but got: undefined。它同样是组件类型映射缺失。这类报错的原因大多数都是某个组件没有默认导出,或者导出的名字和映射表里不一致。

我在排查这些问题时的策略是:不要在报错发生的那一刻去死磕控制台,而是先梳理"类型字符串到组件对象"这条链路。你可以在动态渲染器里打一段日志,把传入的type值和解析到的组件对象打印出来,一眼就能看出哪一步断了。

5.4 动态组件实战速查表

这里把前面提过的核心要点整理成一个速查表,在实际开发中遇到问题可以直接对照排查:

场景核心思路常见坑
根据类型渲染不同组件用映射表将类型字符串映射到组件对象映射表中缺少对应项,页面静默空白
渲染时替换组件并保存状态使用keep-alive包裹动态组件include匹配的是组件name,不是标签key
按需加载组件代码使用defineAsyncComponent或React.lazy组件首屏也用,反而拖慢加载
组件加载失败兜底设置error组件或错误边界没有兜底,页面整体崩溃
给切换加过渡动画Transition + 动态组件加载慢时,动画结束后才显示内容,没有loading过渡
低代码渲染组件树递归动态组件check循环引用,导致递归溢出

这张表里的每一条都是从真实项目里趟出来的。动态组件还有一个容易被忽略的性能问题:组件切换时会触发整个组件树的重新渲染。如果你的动态组件内部有大量计算,建议在子组件内部使用computed或memo来缓存中间结果,避免每次切换都做无用的重复计算。

动态组件用好了,不仅能让代码伸缩性变好,还能提升整个前端的架构格局。它本质上是一种"运行时多态"的组织方式。如果希望动态组件发挥最大价值,最根本的还是要梳理好你的"组件协议",让每个组件对外暴露的接口都一致,这样后续不管加多少组件,主流程代码都不会变。这个习惯在我长期做前端架构之后,愈发觉得比任何框架技巧都重要。

返回列表