38
前言:在vue官方资料中,我们可以可以很学会如何通过vue构建“动态组件”以及“异步组件”,然而,在官方资料中,并没有涉及到真正的“动态异步”组件,经过大量的时间研究和技术分析,我们给出目前比较合理的技术实现方式,并分析一下vue动态异步组件的实现思路。

动态/异步组件的问题

官网中介绍,动态组件是通过tagcomponent来构建,在当中绑定组件的引用即可,大致代码如下:

<template>
    <component :is="currentComp"></component>
</template>
<script>
  import compA from './CompA';
  import compB from './CompB';
  import compC from './CompC';
  export default {
    data () {
      return {
        currentComp: compA
      }
    },
    methods: {
      changeComp (name) {
        switch (name) {
          case 'compA' : {
            this.currentComp = compA; 
            break;
          case 'compB' : 
            this.currentComp = compB; 
            break;
          case 'compC' : 
            this.currentComp = compC; 
            break;
          default :
            this.currentComp = compA;
            break;
        }
      }
    }
  }
</script>

简单说明一下,通过对字符串的判断,来切换组件的实例,实例发生变化后,component组件会自动切换。这就是vue中最基本的动态组件。但是,这样的代码有个很显著的问题:

  1. 所有的组件都是写死的,比如在代码层面上提前定义,即代码5-7行;
  2. 组件的引入是同步的,即便compB和compC在一开始没渲染,但组件的内存已经被加载;

对于第二点,我们可以很容易的使用require或者import语法来进行异步加载。然而这并没有解决问题1所带来的隐患。按照实际的要求,我们需要一个可以在客户端随意配置的组件,即通过组件的地址或配置在进行动态加载的方式。这就是为什么要进行动态异步组件的构建。

这种构建方式是刚需的,一方面他可以构筑类似portal中的porlet组件,另一方面,他的实现方式将给组件加载带来巨大的提升。

构建AsyncComponent

首先,我们看一下预期的结果,我们希望构建如下的代码模式:

<template>
    <async-component path="/views/moduleA/compA"></async-component>
</template>

因此,我们创建一个AsyncComponent.vue文件,并书写代码:

<template>
    <component v-bind:is="componentFile"></component>
</template>

<script>
  export default {
    props: {
      path: {
        type: String,
        required: true,
        default: () => null
      }
    },
    data () {
      const componentFile = this.render;
      return {
        componentFile: componentFile
      }
    },
    methods: {
      render () {
        this.componentFile =  (resolve) => ({
          component: import(`@/${this.path}`),
          loading: { template: '<div style="height: 100%; width: 100%; display: table;"><div style="display: table-cell; vertical-align: middle; text-align: center;"><div>加载中</div></div></div>' },
          error:  { template: '<div style="height: 100%; width: 100%; display: table;"><div style="display: table-cell; vertical-align: middle; text-align: center;"><div>加载错误</div></div></div>' },
          delay: 200,
          timeout: 10000
        });
      }
    },
    watch: {
      file () {
        this.render();
      }
    }
  }
</script>

这个代码很好解释:

  1. 组件中传入path属性,构建时候通过render函数创建异步组件;
  2. render函数为官网的异步组件构建方式;
  3. 监控path属性,当发生变化时,重新通过render函数构建;

为了能够让组件可被重新激活,并且重用性更高,我们对组件进行更多的参数化,最终结果如下:

<template>
  <keep-alive v-if="keepAlive">
    <component
      :is="AsyncComponent"
      v-bind="$attrs"
      v-on="$listeners"/>
  </keep-alive>
  <component
    v-else
    :is="AsyncComponent"
    v-bind="$attrs"
    v-on="$listeners"/>
</template>

<script>
import factory from './factory.js';

/**
 * 动态文件加载器
 */
export default {
  inheritAttrs: false,
  // 外部传入属性
  props: {
    // 文件的路径
    path: {
      type: String,
      default: null
    },
    // 是否保持缓存
    keepAlive: {
      type: Boolean,
      required: false,
      default: true
    },
    // 延迟加载时间
    delay: {
      type: Number,
      default: 20
    },
    // 超时警告时间
    timeout: {
      type: Number,
      default: 2000
    }
  },
  data () {
    return {
      // 构建异步组件 - 懒加载实现
      AsyncComponent: factory(this.path, this.delay, this.timeout)
    }
  },
  watch: {
    path () {
      this.AsyncComponent = factory(this.path, this.delay, this.timeout);
    }
  },
  methods: {
    load (path = this.path) {
      this.AsyncComponent = factory(path, this.delay, this.timeout);
    }
  }
}
</script>

具体改动如下:

  1. 代码第2行增加keep-alive配置,可让组件持久化;
  2. 代码第5行增加属性绑定,可让异步渲染的组件通过外部(async-componenttag)传参;
  3. 代码第6行增加事件绑定,可让异步渲染的组件通过外部(async-componenttag)进行事件监听;
  4. 代码16行为封装异步组件构造器factory(工厂模式),可暴露出去方便其他开发者使用;
  5. factory函数增加加载组件错误组件未定义组件的封装;
  6. 增加了delaytimeout配置;
  7. 暴露了load函数,方便外部JavaScript调用;

至此,我们可以通过<async-component path="/views/moduleA/compA.vue"></async-component>来构建组件,如果path是一个传入属性,通过改变该属性触发watch来重新加载新组件。或者给该组件添加ref属性来获取异步组件容器,通过调用load方法来重新装载。

后续的问题

看上去,现在的封装已经非常好,但是距离完美还差很大一截。虽然已经解决了开发人员重复造轮子的问题,并优化了最佳的代码模式,然而我们仍然能发现一个问题:ref之后,拿到的是AsyncComponent组件,并非能像<component>tag一样可以直接对内部组件进行获取。如果按照这样的理想去思考,有利也是有弊的。是我们构建出了<component>tag的动态异步版,AsyncComponent作为容器的属性很容易被内部的装载物所替换,比如load方法。

且不考虑这样实现之后的问题,我们可以在开发过程中通过约束来避免,代码中也可以增加属性检测机制。但这样的实现方式非常困难,我尝试过如下方式,均不能实现:

函数式组件: 通过添加functional: true可以把一个组件函数化,这样使得该组件无法获取到this即组件实例,因此他所挂载的就是内部的装载组件。然而在构建过程中,出现了无限递归render函数的问题,无法解决,而且createElement函数无法创建空节点,导致组件总会出现一个不必要的标签。

抽象化组件: 通过添加abstract: true可以把一个组件抽象化,这样组件是无法渲染其自身,而只会挂在内部的装载组件。但这样做之后,会导致外部容器无法通过ref属性查找到它,更无法获取到装载组件,因此也以失败告终。

继承式组件: 通过添加extends可以在构建AsyncComponent的时候实现继承,这样装载组件的属性都会传入到AsyncComponent中,然而,这样的方式不支持动态继承,所以还是宣告失败。

虽然都是失败的结果,但这并不能停止我们的想象,相信未来会有方法来解决这个问题。目前,我们还只能老老实实的把AsyncComponent当做容器来使用。

动态异步的未来

你要问我动态异步组件的未来是什么,我会告诉你我们的梦想有多大。我们希望实现分布式前端UIServer,让AsyncComponent可以加载远程组件。远程组件就可以实现配置化,就可以脱离代码本身而进行前端的组件构建。


loong
234 声望35 粉丝

看到问题不代表解决问题,系统化才能挖掘问题的本真!