【踩坑实录】vue异步组件加载失败会怎样?

写在最前

看官们好,我是JetTsang,之前都是在掘金潜水来着,现在偶尔做一些内容输出吧。

问题引出

项目技术栈: vue2.6

由于业务场景需要,会在同一个路由组件当中,有一部分页面的组件需要根据数据类型的不同,显示对应的页面组件。

页面结构的示意图:

而在逐步的迭代过程当中,数据类型越来越多,使用之前的同步组件引入时,会导致该路由组件过于臃肿,进而影响到该路由页面的加载速度。就像这样:

后续设计了动态异步组件Component + AsyncComponent的方案 vue2官方文档-异步组件

异步组件采用了Vue官方的文档示例的工厂函数的方式去编写

方案如下:

当然这里有好几种变体写法,vue注册的组件,可以是特定的对象,函数,promise等。

而后打包体积果然减少了,成功缩减了该路由页面的体积。

但在后来遇到了问题:这里异步组件只加载1次,如果失败了就会显示对应的ErrorComponent,之后再加载也只会加载到失败的组件,不会再次去发请求。

我预想的结果是,在失败组件那里点击重新加载,或者用户通过路由操作返回后,能重新请求组件。

看了网上的一些解决方案,说是需要reload这个页面,难道这样了吗?

遇到问题,要先冷静分析。

问题剖析

首先看下render函数上写的是什么,在vue开发者面板可以找到这个功能

js 复制代码
function render() {

var _vm = this,

_c = _vm._self._c;

return _c("div", [
            _c("button", {
                on: {
                    click: _vm.handleClick
                }
            }, [_vm._v(" 点击加载组件 ")]), 
            _c("hr"),
            _c(_vm.asyncCom, {
                tag: "Component"
            })
        ], 1);
}

对应的template为:

js 复制代码
<template>
    <div>
        <button @click="handleClick" >
        点击加载组件
        </button>
        <hr/>
         // 这里是重点
        <Component :is="asyncCom"/>
    </div>
</template>

可以忽略掉我写的最外层的div,以及测试用的button和hr元素,最后最关键的Component is就被编译成了_c(_vm.asyncCom, { tag: "Component" })

这个_c就是createElement查看一下源码

根据条件,会进入到createComponent

那最终的路径就是这样:

createElement --> _createElement --> createComponent --> resolveAsyncComponent

那么只需要仔细分析resolveAsyncComponent

typescript 复制代码
export function resolveAsyncComponent(
    factory: { (...args: any[]): any; [keye: string]: any },
    baseCtor: typeof Component
): typeof Component | void {
    // 如果有errorComp属性和error为true,则返回错误的组件
    if (isTrue(factory.error) && isDef(factory.errorComp)) {
        return factory.errorComp
    }
    // 如果已经resolve状态了,则返回resolved的结果
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
    const owner = currentRenderingInstance
    // 如果最近渲染的实例里没有在owners里并且owners存在,则放入到owners数组里记录下来
    if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
        // already pending
        factory.owners.push(owner)
    }
    // 如果在loading,则加载loading组件
    if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
        return factory.loadingComp
    }
    //如果有当前渲染实例,并且factory.owners不存在,则开启加载流程
    if (owner && !isDef(factory.owners)) {
        // 这里省略了。。。。
        // return in case resolved synchronously
        return factory.loading ? factory.loadingComp : factory.resolved
    }

}

这里的factory就是我们传入的_vm.asyncCom,会发现它给_vm.asyncCom放了一些属性,根据这些属性来返回对应的组件。那么我们打印一下_vm.asyncCom看看

果然如此!

解决方案

既然到这里,问题解决方案就很简单了

方案一(不推荐): 可以给asyncCom重新设置好error 和owners,让源码里的判断失效,能继续走到加载组件的逻辑里

js 复制代码
// 在失效的组件errorComp里,设置对应的内容,同时强制刷新组件
this.$parent.asyncCom.error = false
this.$parent.asyncCom.owners = null
this.$forceUpdate()

可以看到确实生效了 但这有明显的问题,这太hack了,如果源码里的变量名后续改了,又会失效,换言之,耦合程度太高。

方案二(推荐): 这里的问题在于源码给factory放了属性,那我们只要每次都返回新的factory不就可以。

这里的方法很多,比如将comMap改造成工厂函数返回。或者干脆把ComMapFactory写在vue的data里面,或者函数里面等等。

js 复制代码
 // 改造成工厂函数
const ComMapFactory = () =>( {
    // 这里注意⚠️:import 一定要静态写出来,因为webpack是静态编译的
    A: ()=> AsyncComponent(import('xxx')),

    B: ()=> AsyncComponent(import('xxx')),

    C: ()=> AsyncComponent(import('xxx')),

    D: ()=> AsyncComponent(import('xxx')),

})

最后小疑问: 为什么成功获取到组件之后,再次点击就不会再请求一次组件呢?

2个原因:

  • 这其实是webpack的Runtime里面有对应模块的缓存,当已经获取到对应的组件之后,再次import('xxx')会直接从缓存里面取值
  • 刚刚的resolveAsyncComponent也提到了,一旦resolved,就会直接返回resolved
相关推荐
Ticnix23 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人26 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl30 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅33 分钟前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人42 分钟前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust
Mr Xu_1 小时前
Vue 3 中计算属性的最佳实践:提升可读性、可维护性与性能
前端·javascript
jerrywus1 小时前
我写了个 Claude Code Skill,再也不用手动切图传 COS 了
前端·agent·claude
玖月晴空1 小时前
探索关于Spec 和Skills 的一些实战运用-Kiro篇
前端·aigc·代码规范