【踩坑实录】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
相关推荐
超哥--22 分钟前
B站视频内容智能分析系统(九):React 前端与管理面板
前端·react.js·前端框架
Cutecat_3 小时前
视频字幕处理工具横向:提取模式 vs 编辑模式,该如何选择
android·前端·ios·语音识别
dsyyyyy11013 小时前
JavaScript变量
开发语言·javascript·ecmascript
qq_422152574 小时前
PDF 加水印工具怎么选?2026 年文档版权保护方案对比
前端·pdf·github
kyriewen4 小时前
手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势
前端·javascript·面试
brucelee1865 小时前
OpenClaw 浏览器控制(Chrome MCP)完整教程
前端·chrome
ct9785 小时前
React 状态管理方案深度对比
开发语言·前端·react
胡志辉的博客5 小时前
深入浅出理解浏览器事件循环:从一道输出题讲到 Chrome 源码
前端·javascript·chrome·chromium·event loop
代码不加糖5 小时前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
懂懂tty5 小时前
Vue2与Vue3之间API差异
前端·javascript·vue.js