Vue動態異步組件實現思路及其問題

前言:在vue 官方資料中,咱們能夠能夠很學會如何經過vue構建「動態組件」以及「異步組件」,然而,在官方資料中,並無涉及到真正的「動態異步」組件,通過大量的時間研究和技術分析,咱們給出目前比較合理的技術實現方式,並分析一下vue動態異步組件的實現思路。

動態/異步組件的問題

官網中介紹,動態組件是經過tagcomponent來構建,在當中綁定組件的引用便可,大體代碼以下:html

<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所帶來的隱患。按照實際的要求,咱們須要一個能夠在客戶端隨意配置的組件,即經過組件的地址或配置在進行動態加載的方式。這就是爲何要進行動態異步組件的構建。vue

這種構建方式是剛需的,一方面他能夠構築相似portal中的porlet組件,另外一方面,他的實現方式將給組件加載帶來巨大的提高。緩存

構建AsyncComponent

首先,咱們看一下預期的結果,咱們但願構建以下的代碼模式:異步

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

所以,咱們建立一個AsyncComponent.vue文件,並書寫代碼:async

<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函數構建;

爲了可以讓組件可被從新激活,而且重用性更高,咱們對組件進行更多的參數化,最終結果以下:ide

<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能夠加載遠程組件。遠程組件就能夠實現配置化,就能夠脫離代碼自己而進行前端的組件構建。

相關文章
相關標籤/搜索