vue 2.x 如何“主动”的动态注入组件?

vue 2.6

目前主要用的组件“异步注入”方式,大概如下:

需要注入的组件文件:

global-custom-components.js

const install = Vue => {
    Vue.component('comp-name', () => import(./components/compName.vue))
}

export default install

然后
main.js

import customComponents from './global-custom-components.js'
Vue.use(customComponents)

然后当某个包含组件 comp-name 的页面被打开时,该组件才会被下载、导入,从而达到“减负”的做用。

<comp-name></comp-name>

但是感觉这种方式还是显得比较“隐式”或者说“被动”,当某个页面使用到该组件时,该组件非常隐蔽的被“下载”下来,这是不受我们控制的。

现在的有一个需求是,我需要在组件开始下载的过程,显示一个动画,当组件被完全下载,动画结束,然后展示这些组件。

假设,有一个预览页面,页面上有 10 个自定义组件,我需要做如下步骤:

let compList = [comp_1, comp2, ......] // 共 10 个组件
1、展示 loading 动画效果;
2、循环判断 compList 当前vue实例是否包含了每一个组件(因为组件类型相同,虽然有 10 个组件,但实际上只有 5 类不统的组件);
3、如果不存在,同步导入组件;
4、当组件全部导入完毕;
5、暂停动画并展示组件;

请问第三步该如何实现?

——————————— 分割线 ——————————

@lining 感谢回复,我回答的内容太长,保存不了,以补充形式回复在这,感谢!

看了回复,但还是不太明白,感觉没回到到点上。
我尝试着写了一个 mixins ,里面有一个函数用来动态注册组件,发现有如下问题,伪代码如下:

loadComp() {
    //组件json对象,包含组件需要的一些参数
    let componentList = [] 
    componentList.forEach(async (e) => {
        let targetData = e
        // 实际监听,能获取到正确的组件路径
        let path = `../../../packages/components/${targetData.package}/${targetData.category}/${targetData.key}/index.vue`
        let key = `V${targetData.key}`
        let ifExists = Vue.component(key)
        if(!ifExists) {
            Vue.component(
                key,
                () => import(path)
            )
        }
    })
},

上面代码有如下问题:
1、Vue.component(key),key为任何参数,该方法在 bool 环境下都为 true,所以不能用来判断 xxx 组件是否被注册过;
2、如下代码,在 path 获取到正确的路径后,

Vue.component(
    key,
    () => import(path)
)

执行报错,错误信息

Failed to resolve async component: () => __webpack_require__("./src/views/preview/mixins lazy recursive")(path)
Reason: Error: Cannot find module '../../../packages/components/Charts/Bars/BarCrossrange/index.vue'

—————————————— 分割线 ————————————

—————————————— 答案 ————————————

定义获取组件js文件:


// 通过 require.context 找到路径“./components/”下的所有 index.vue 文件
// 获取到政府列表,如:
// ./components/comp_a/index.vue
// ./components/comp_b/index.vue
// ./components/comp_c/index.vue
// 所有组件均已 export default {} 方式定义——vue 2.6(以下) 的通用方式

const modules = require.context(
  // 其组件目录的相对路径
  './components/',
  // 是否查询其子目录
  true,
  // 匹配基础组件文件名的正则表达式
  // /Base[A-Z]\w+\.(vue|js)$/
  /\w+\/index.vue/
)


// 通过组件名称 compName(comp_a),获取组件
export const fetchComponent = (compName) => {
  const module = modules

  let keys = module.keys()

  for(let i = 0; i < keys.length; i++) {
    let key = keys[i]
    const urlSplit = key.split('/')
    if(urlSplit[urlSplit.length -2 ] === compName) {
      const componentConfig = module(key).default
      // 返回组件对象
      return componentConfig.default || componentConfig
    }
  }
}

安装组件文件:

import Vue from 'vue'
/**
 * * 动态注册组件
 */
 export const componentInstall = (key, node) => {
    //如果key组件没有被注册过,且组件对象node存在,则注册该组件
    if(!Vue.component(key) && node) {
      console.log('注册组件:' + key)
      Vue.component(key, node)
    }else{
      console.log('组件:' + key + ' 已存在')
    }
}

消费文件:

componentInstall('comp_a', fetchComponent('comp_a'))

—————————————— 分割线 ————————————

—————————————— 答案 (最终)————————————

在之前得到的答案中,其实还是有问题的,我发现所有组件都被打包到app.js里面了,这样就失去了异步加载的意义了。

经过我的测试,发现问题出在require.context()方法,该方法把匹配到的文件全部打包进app.js了,所以不能用此方法。

const modules = require.context(
  // 其组件目录的相对路径
  './components/',
  // 是否查询其子目录
  true,
  // 匹配基础组件文件名的正则表达式
  // /Base[A-Z]\w+\.(vue|js)$/
  /\w+\/index.vue/
)

经过我的测试,发现问题其实很简单,如果你只需要单个组件异步加载(不需要多个组件“全部”完成加载的场景),那么直接可以:

//item.chartKey 组件名称
//() => fetchChartComponent(item) 异步加载组件对象

//注册异步组件 item.chartKey,页面可以使用该组件<item.chartKey></item.chartKey>,组件下载完成后,自动展示在页面上。
//重要(注意):
//也就是说,页面使用该组件时,即使组件没有下载完成,也是没有问题的。
componentInstall(item.chartKey, () => fetchChartComponent(item))
//注册异步组件 item.conKey
componentInstall(item.conKey, () => fetchConfigComponent(item))

此种方式其实和原问题描述中的使用方式一致。

 export const fetchChartComponent = (chartConfig) => {
  const chart = import(`./components/${chartConfig.package}/${chartConfig.category}/${chartConfig.key}/index.vue`)
  return chart
}
import Vue from 'vue'

/**
 * * 动态注册组件
 */
 export const componentInstall = (key, node) => {
    if(!Vue.component(key) && node) {
      console.log('注册组件:' + key)
      Vue.component(key, node)
    }else{
      console.log('组件:' + key + ' 已存在')
    }
}

如果你需要等很多组件全部加载完成后,再执行某个逻辑,那么可以写

let f1 = fetchChartComponent(item1)
let f2 = fetchChartComponent(item2)
let f3 = fetchChartComponent(item3)

//重要(注意):
//此时,在所有组件未"全部"加载完成前,页面不能使用到了某个为下载完成的组件。
//通常,此种场景下,会在页面所有组件最外层套一个div,然后用 if 来控制,如果全部加载完成,该div才能展示(v-if='true')。
this.loading = true
Promise.all([f1, f2, f3]).then(res => {
    this.loading = false
}).catch(err => {
    this.loading = false
})
阅读 3.3k
2 个回答

可以看一下这个项目 unplugin-vue-components,应该是符合你预期的。

Anthony Fu 这位佬还提供、参与了了其它类似项目的开发,都可以了解一下。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题