vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > vue异步组件加载

vue异步组件使用及加载失败异常处理

作者:小白路过

在构建大型单页应用时,组件的按需加载和延迟加载对于性能优化至关重要,本文主要介绍了vue异步组件使用及加载失败异常处理,具有一定的参考价值,感兴趣的可以了解一下

引言

在构建大型单页应用时,组件的按需加载和延迟加载对于性能优化至关重要。Vue.js 提供了一种实现这个需求的方式,那就是异步组件。异步组件允许我们将组件的加载延迟到实际需要的时候,而不是一开始就全部加载。这不仅可以减少首屏加载时间,还可以提高应用的响应速度。本文将介绍Vue2中异步组件的概念、使用方法以及一些技巧,重点介绍一下高阶异步组件及高阶异步组件加载失败后,如何实现重新加载。

什么是异步组件

在Vue中,通常我们使用import语句来导入组件,然后在components选项中注册它们。这种方式会导致所有组件在应用程序初始化时都被加载,可能会影响应用的初始加载性能。异步组件的概念就是延迟加载组件,只有在需要时才进行加载,从而提高了应用的加载速度。

如何区别一个组件是同步组件还是异步组件呢?我们日常开发过程中,常见的那些组件引用哪些是异步组件,哪些不是呢?

下面我们来举例说明:

// Welcome.vue
import HelloWorld from './HelloWorld.vue'
export default {
	components: {HelloWorld},
	template: '<HelloWorld/>'
}

在上面的例子中我们定义了一个组件Welcome.vue,在该组件中引入了局部组件HelloWorld,对于Welcome而言,此处的HelloWorld组件是作为同步组件引入的。

如果想使用异步组件的形式引入,该如何修改上面的代码呢?

// Welcome.vue
//import HelloWorld from './HelloWorld.vue'
const HellowWold = ()=>import('./HelloWorld.vue')
export default {
	components: {HelloWorld},
	template: '<HelloWorld/>'
}

将组件改成上面的形式注册局部组件,此时就是注册了一个异步组件。组件自身的实现并没有所谓的同步异步一说,关键在于如何引入组件。

上面异步组件的引入形式,常见于vue-router中注册路由表时。这样能能让不同的路由独立打包,按需加载。

异步组件的几种书写形式

普通异步组件,通过vue内部曝露出的resolve方法返回对应的组件

Vue.component('async-component', function(resolve, reject) {
  import('./AsyncComponent.vue').then((module) => {
    resolve(module.default);
  }).catch((error) => {
    reject(error);
  });
});

promise异步组件

Vue.component('async-component',()=>import('./AsyncComponent.vue'))

高阶异步组件

// 高级异步组件
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

异步组件的应用场景

路由懒加载

在 Vue Router 中,我们可以使用异步组件来实现路由懒加载。懒加载是一种优化策略,它只加载当前需要显示的组件,而不是一次性加载所有的组件。这样就可以减少首屏加载时间,提高应用的响应速度。

我们可以将每个路由配置中的 component 选项替换为一个返回 Promise 的函数,这样 Vue Router 就会自动将这个组件注册为异步组件,并在需要的时候进行加载。例如:

const router = new VueRouter({
  routes: [
    { path: '/foo', component: () => import('./Foo.vue') }
  ]
})

加载联邦模块

模块联邦:(Module Federation)是一种软件架构模式,主要用于解决在复杂分布式系统中,不同模块间的依赖管理和共享问题。这种模式通过将大型应用拆分并独立开发、构建和部署各个模块,使得它们可以在运行时动态地加载和共享。

日常开发中,开发人员可能将A应用中的某一组件独立打包js,以中间件或者SDK的形式供其他应用使用。而B作为引用方,如何加载远程js,引入对应的组件呢?

下面以我在开发中的一个实际应用举例,介绍两种常规解决方法:

项目组中B应用有一个远程服务的js文件,其中打包了我们需要引入的组件。在我们自身项目中需引入该组件,之所以采用加载js,而非npm包的形式,主要是为了解耦,后续该组件的迭代更新都不影响我们自身应用,自身应用也无需发版即可使用最新版本的组件。

首先,为方便后续使用,我们先封装一个异步方法,用于加载和执行对应的js文件。加载执行js的方式大致有两种:

方法一:通过fetch请求获取远程js内容,然后通过eval函数执行对应的js文件。这种方式要考虑跨域问题和eval安全执行策略问题,有些浏览器禁止在非安全环境下执行eval函数。

fetch('path/to/remote/js/file.js')
        .then(response => response.text())
        .then(jsCode => {
          // 执行js
         new Function('return ' + jsCode)();
        })
        .catch(error => {
          console.error('Error loading remote component:', error);
        });

方法二:通过动态创建script标签来执行对应的js文件。这种方式比较稳妥,无需考虑跨域问题。

const script = document.createElement('script')
script.type = 'text/javascript'
script.onload = () => resolve(void 0)
script.onerror = (err) => {
  console.error(`资源加载失败:`, src)
  reject(err)
}
script.src = src
document.head.appendChild(script)

这里我们项目中采用第二种动态标签的形式,封装了一个类,主要用于加载远程js,便于后续使用。其代码简写如下:

import { Vue } from 'vue-property-decorator'
export class AsyncComponentLoader {
  static cache: Record<string, any> = {}
  static async loadAsyncComponent(src: string, cachable: boolean = false) {
    if (!src) throw new Error('无法加载远程组件:未传入远程组件加载地址')
    const name = 'remote_sdk_key' 
    // 引入缓存,避免重复加载
    if (cachable && AsyncComponentLoader.cache[name]) {
        ///远程组件js执行后,在window上挂载,后续的逻辑有赖于远程js如何打包的,具体问题具体分析
      Vue.component('RemoteComp',window[name]?.default)
      return
    } else {
      await AsyncComponentLoader.loadScript(src)
      Vue.component('RemoteComp',window[name]?.default)
      AsyncComponentLoader.cache[name] = window[name]
    }
  }

  static async loadScript(src) {
    // 加载远程js并执行
    return new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.type = 'text/javascript'
      script.onload = () => resolve(void 0)
      script.onerror = (err) => {
        console.error(`资源加载失败:`, src)
        reject(err)
      }
      script.src = src
      document.head.appendChild(script)
    })
  }
}

同步引入
创建一个新的Vue组件文件AsyncComponent,在AsyncComponent文件中,你可以编写一个方法来请求远程JS文件,并将其动态导入为组件。例如:

import { Vue, Component, Prop } from 'vue-property-decorator'
import { AsyncComponentLoader } from './AsyncComponentLoader'
@Component
export default class extends Vue {
  @Prop() src!: string
  // sdk加载成功标识
  sdkLoaded = false
  // sdk加载loading
  showLoading = false
  async created() {
  //加载组件js
    this.loadComponent()
  }

  async loadComponent() {
    this.showLoading = true
    AsyncComponentLoader.loadAsyncComponent(this.src, true)
      .then(() => {
        this.sdkLoaded = true
        this.$emit('onload')
        //强制父组件重新渲染
        this.$parent?.$forceUpdate()
      })
      .catch(() => {
        this.$emit('onerror')
      })
      .finally(() => {
        this.showLoading = false
      })
  }

  genError() {
    const h = this.$createElement
    const style = { height: '150px', cursor: 'pointer', backgroundColor: '#fff' }
    return (
        ///加载失败时显示
      <div style={style} onClick={this.loadComponent}>
        加载失败,点击重新加载
      </div>
    )
  }
  render() {
    const h = this.$createElement
    if (this.showLoading) {
        //加载loading
      const directives = [{ name: 'loading', value: this.showLoading }]
      return <div {...{ directives }} style="width: 100%;height: 150px"></div>
    }
    if (!this.sdkLoaded) {
      return this.genError()
    }
    return this.$slots.default
  }
}

在上面代码中定义了一个组件,该组件在created钩子函数中去加载远程js组件。在加载过程中,显示loading,加载成功后显示默认插槽内容,在插槽里,可以直接使用注册的远程组件,如果js加载失败,则显示对应的error,并支持点击重新加载。

使用方式:

<async-load src="path/to/remote/js/file.js">
    <RemoteComp :env="env"  />
</async-load>

异步引入
通过直接注册一个高阶异步组件的形式,来引入远程js组件

Vue.component('async-component', function() {
  // 显示loading状态
  const loadingComponent = {
    template: '<div>Loading...</div>'
  };

  // 显示error状态
  const errorComponent = {
    template: '<div>Error! Failed to load component.</div>'
  };

  return {
    loading: loadingComponent,
    error:errorComponent,
    component:  new Promise((resolve,reject)=>{
        // 加载远程组件的js文件
        const script = document.createElement('script');
        script.src = 'path_to_remote_component.js';
        script.onload = () => {
          // 注册远程组件
          resolve({
            template: '<div>远程组件的模板</div>',
            // 远程组件的其他配置
          });
        };
        script.onerror = (error) => {
          reject(error)
        };
        document.head.appendChild(script);
    }),
    delay: 0,
    timeout: 3000
  }
});

注册的异步组件可以直接使用,该组件使用方式:

<async-component></async-component>

无论成功还是失败,异步组件的加载只执行一次,之后就一直保留该状态。这也就意味着,当异步组件作为局部组件引入时,一旦加载失败,后续无论路由如何跳转,该异步组件也一直渲染的是errorComp。除非刷新整个页面,重新加载响应异步组件。但是很多时候,作为局部组件加载失败,我们只想重新加载失败的那部分,局部刷新,该如何做呢

异步组件加载失败异常处理

通常,异步组件作为路由组件,一个路由就是一个页面,这个时候加载失败时,我们一般都是刷新整个浏览器页面就行。但是针对上面我们说的,当一个异步组件作为页面的一部分渲染时,如果加载失败,如何只加载这部分,而不需要刷新整个浏览器页面也是很有必要的。

在vue的issue中,也有人提出了类似的问题: https://github.com/vuejs/vue/issues/8524

在这个问题中,vue的作者尤大给出的解决方案是强制父组件重新渲染。

在这里插入图片描述

根据以上回答,我做了以下尝试,将上面异步组件的errorComponent重新改了下,增加了刷新方法,调用强制刷新方法$forceUpdate,强制父组件刷新。

@Component
class ErrorComp extends Vue {
  async refresh() {
    // 强制父组件刷新,用以重新加载异步组件
    this.$parent?.$forceUpdate()
  }
  render() {
    const h = this.$createElement
    return (
      <div style="min-height:150px;padding: 10px"  onClick={this.refresh}>
        加载失败!请刷新重试
      </div>
    )
  }
}

然而,经实际代码试验,这种直接调用父实例刷新方法,并不能重新加载异步组件,对应的异步组件依然渲染为errorComponent。此时,我们就需要执行下调试,看看强制刷新后,为什么没有重新加载异步组件。

首先,代码执行到vue源码中的createComponent中,这是vue在解析组件类型标签中很重要的一个函数,不了解的可以看下相关源码。源码中的Ctor就是上面我们注册异步组件时Vue.component('async-component', function() {/**bula bula/})的第二个函数入参。

function createComponent (
  Ctor,
  data,
  context,
  children,
  tag
) {
  if (isUndef(Ctor)) {
    return
  }
  var baseCtor = context.$options._base;
  // plain options object: turn it into a constructor
  //异步组件这里Ctor是个函数,所以不走extend方法
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(("Invalid Component definition: " + (String(Ctor))), context);
    }
    return
  }

  // async component
  var asyncFactory;
  //由于没有走extend方法,自然没有对应的cid,异步组件可以执行到这个里面
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor; //在异步组件加载失败以后,可以在此处打上断点,然后点击重新加载,观察后续执行过程
    //这里重点要进入这个方法,这个方法决定了异步组件渲染的是loading组件还是远程组件或者error组件
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context);
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
//后续代码省略不看
  ……
}

在上述代码中我们重点关注resolveAsyncComponent方法的执行,在下面代码中,我们可以看到,方法在最开始的时候就对factory.error进行了判断。在我们首次执行异步组件加载时,由于加载失败,此时会将factory.error置为true。所以后续我们强制父组件重新渲染时,在这个函数里,第一行判断成功就直接返回了factory.errorComp组件,所以我们如果要重向第一次那样重新加载该远程组件,我们需要把函数里面所有的提前返回都杜绝调,以便于执行到最终的加载逻辑中。

function resolveAsyncComponent (
    factory,
    baseCtor
  ) {
  	//此处加载失败,直接返回了errorComp,如重新加载,将对应的factory.error设为false
    if (isTrue(factory.error) && isDef(factory.errorComp)) {
      return factory.errorComp
    }
	//由于是失败重新加载,factory.resolved无值,无需处理
    if (isDef(factory.resolved)) {
      return factory.resolved
    }

    var owner = currentRenderingInstance;
    if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
      // already pending
      factory.owners.push(owner);
    }
	// 失败重新时,将loading重新置为false
    if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
      return factory.loadingComp
    }
	//为了进入这个判断里面,这里将factory.owners置为null
    if (owner && !isDef(factory.owners)) {
      var owners = factory.owners = [owner];
      var sync = true;
      var timerLoading = null;
      var timerTimeout = null

      ;(owner).$on('hook:destroyed', function () { return remove(owners, owner); });

      var forceRender = function (renderCompleted) {
        for (var i = 0, l = owners.length; i < l; i++) {
          (owners[i]).$forceUpdate();
        }

        if (renderCompleted) {
          owners.length = 0;
          if (timerLoading !== null) {
            clearTimeout(timerLoading);
            timerLoading = null;
          }
          if (timerTimeout !== null) {
            clearTimeout(timerTimeout);
            timerTimeout = null;
          }
        }
      };

      var resolve = once(function (res) {
        // cache resolved
        factory.resolved = ensureCtor(res, baseCtor);
        // invoke callbacks only if this is not a synchronous resolve
        // (async resolves are shimmed as synchronous during SSR)
        if (!sync) {
          forceRender(true);
        } else {
          owners.length = 0;
        }
      });

      var reject = once(function (reason) {
        warn(
          "Failed to resolve async component: " + (String(factory)) +
          (reason ? ("\nReason: " + reason) : '')
        );
        if (isDef(factory.errorComp)) {
        //执行失败时,将factory.error设为true
          factory.error = true;
          forceRender(true);
        }
      });
	  //这里传入上面定义的resolve,reject方法
      var res = factory(resolve, reject);

      if (isObject(res)) {
        if (isPromise(res)) {
          // () => Promise
          if (isUndef(factory.resolved)) {
            res.then(resolve, reject);
          }
        } else if (isPromise(res.component)) {
          res.component.then(resolve, reject);

          if (isDef(res.error)) {
          //此处初始化factory.errorComp,这里的res.error就是我们先前定义高阶异步组件时,函数返回对象中的error
            factory.errorComp = ensureCtor(res.error, baseCtor);
          }

          if (isDef(res.loading)) {
            factory.loadingComp = ensureCtor(res.loading, baseCtor);
            if (res.delay === 0) {
              factory.loading = true;
            } else {
              timerLoading = setTimeout(function () {
                timerLoading = null;
                if (isUndef(factory.resolved) && isUndef(factory.error)) {
                  factory.loading = true;
                  forceRender(false);
                }
              }, res.delay || 200);
            }
          }

          if (isDef(res.timeout)) {
            timerTimeout = setTimeout(function () {
              timerTimeout = null;
              if (isUndef(factory.resolved)) {
                reject(
                  "timeout (" + (res.timeout) + "ms)"
                );
              }
            }, res.timeout);
          }
        }
      }

      sync = false;
      // return in case resolved synchronously
      return factory.loading
        ? factory.loadingComp
        : factory.resolved
    }
  }

根据上面源码中代码执行结果,重新调整刷新方法如下:

@Component
class ErrorComp extends Vue {
  async refresh() {
    // 异步组件加载失败后,在不刷新页面的情况下重新加载远程组件
    // 该方法hack了vue内部实现,非必要不使用,且依赖于vue源码中resolveAsyncComponent方法,需注意vue版本
    // @ts-ignore
    const asyncFactory: any = this.constructor.asyncFactory //注意这里的asyncFactory不是本身就有的,需要手动挂载。这里是为了统一封装errorComp组件,便于其他异步组件也可以使用该errorComp
    if (!asyncFactory) return window.location.reload()
    // 异步组件加载失败后,该标识为true,返回之前设置的error component,如需重新加载,需重设error
    asyncFactory.error = false
    // 重设loading,否则重新加载后返回的是loading component
    asyncFactory.loading = false
    // 重设实例,否则无法进入异步组件加载逻辑,不同版本的变量命名不同,应用里用的是2.6.10版本,变量为owners,2.5中为contexts
    asyncFactory.contexts = null
    asyncFactory.owners = null
    // 强制父组件刷新,用以重新加载异步组件
    this.$parent?.$forceUpdate()
  }
  render() {
    const h = this.$createElement
    return (
      <div style="min-height:150px;padding: 10px">
        加载失败!请
        <span style="cursor: pointer;color: #3693ff" onClick={this.refresh}>
          刷新
        </span>
        重试
      </div>
    )
  }
}

在上面代码中,在$forceUpdate方法之前执行了一些重置操作,用于清空异步组件的加载状态。这里需要强调一下,代码中的constructor.asyncFactory不是本身就有的,需要手动挂载。这里是为了统一封装errorComp组件,便于其他异步组件也可以使用该errorComp。至于后面的owners和contexts是由于不同版本的vue,在此处的实现略有不同,为了兼容性,这里两个变量值都重置了一下。至于如何挂载asyncFactory,以下面伪代码为例:

import ErrorComp from 'ErrorComp'
Vue.component('async-component', function asyncComponent() {
	const error  = ErrorComp.extend()
	//这里挂载一下,便于后面失败时重新加载引用
	error.asyncFactory = asyncComponent
	return {
		loading: loadingComp,
		error:error,
		component: new Promise((resolve,reject)=>{
		// ……
		})
	}
})

经试验,改造后的代码可以实现异步组件加载失败重新加载。如果有遇到相似问题的,可以参考下该方案。不过由于该方案有较强的侵入性,依赖于vue源码的内部实现,不同vue版本,在这方面的实现策略不同,可能会导致不同的执行效果,所以请注意使用的vue版本及其在这一块的实现细节。本文中对应的代码适应于vue 2.5-2.6,我并没有查询其他版本的vue源码,有兴趣的可以自行参考此方案做响应调整。

结语

通过使用Vue2中的异步组件,我们可以优化应用程序的性能和加载速度,提升用户体验。同时,合理地处理加载状态和错误情况,以及灵活地使用高阶异步组件和按需加载,可以让我们更好地利用异步组件的优势,为用户提供更好的应用体验。

到此这篇关于vue异步组件使用及加载失败异常处理的文章就介绍到这了,更多相关vue异步组件加载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文