+
19
-

回答

好的,这是一个非常实际且有用的需求,尤其是在不使用构建工具(如Vite、Webpack)的纯CDN环境中。

我们将通过Vue 3的 defineAsyncComponent 函数和一个自定义的加载器来实现这个功能。这种方法可以将您的应用模块化,保持主HTML文件的整洁,并按需加载组件。

核心思路

创建组件文件: 每个Vue组件将被写在一个独立的 .js 文件中。这个文件只包含组件的选项对象({ template: '...', data() {...} })。

全局注册: 为了让主应用能找到加载后的组件,每个组件文件在定义完自己后,会把自己注册到一个全局对象上(例如 window.VUE_COMPONENTS)。

自定义加载器: 我们会编写一个通用的JavaScript函数,该函数负责通过动态创建<script>标签来加载组件的URL。它会返回一个Promise,在脚本加载成功并找到注册的组件后,Promise会resolve。

defineAsyncComponent: 在主Vue应用中,我们使用 defineAsyncComponent 来包裹我们的自定义加载器。当Vue需要渲染这个组件时,它会自动调用我们的加载器,等待组件加载完成,然后渲染它。

项目文件结构

为了演示,你需要创建一个简单的项目结构,并使用一个本地服务器来运行它(因为浏览器安全策略限制直接从file://协议加载文件)。

/my-vue-app/
|-- index.html              (主HTML文件)
|-- /components/
|   |-- Header.js           (页头组件)
|   |-- UserProfile.js      (用户资料组件)
|   |-- LoadingSpinner.js   (加载中组件)

第1步:创建各个组件文件

/components/Header.js

这是一个简单的页头组件。

// /components/Header.js

// 确保全局组件对象存在
window.VUE_COMPONENTS = window.VUE_COMPONENTS || {};

// 定义组件并注册到全局对象
window.VUE_COMPONENTS['AppHeader'] = {
  template: `
    <header class="bg-white shadow-md">
      <nav class="max-w-4xl mx-auto px-4 py-3 flex justify-between items-center">
        <h1 class="text-xl font-bold text-indigo-600">动态组件加载系统</h1>
        <p class="text-sm text-gray-500">Vue 3 CDN 版</p>
      </nav>
    </header>
  `
};

/components/UserProfile.js

这是一个稍微复杂一点的组件,它接收props。

// /components/UserProfile.js

window.VUE_COMPONENTS = window.VUE_COMPONENTS || {};

window.VUE_COMPONENTS['UserProfile'] = {
  props: ['userId'],
  template: `
    <div class="bg-white p-6 rounded-lg shadow-lg border-l-4 border-emerald-500">
      <div v-if="loading" class="text-gray-500">正在加载用户 {{ userId }} 的信息...</div>
      <div v-else>
        <div class="flex items-center space-x-4">
          <img :src="user.avatar" class="h-16 w-16 rounded-full">
          <div>
            <h2 class="text-2xl font-bold text-gray-800">{{ user.name }}</h2>
            <p class="text-gray-600">{{ user.email }}</p>
          </div>
        </div>
      </div>
    </div>
  `,
  data() {
    return {
      loading: true,
      user: null
    }
  },
  created() {
    // 模拟API请求
    setTimeout(() => {
      this.user = {
        name: '爱丽丝',
        email: 'alice@example.com',
        avatar: `https://i.pravatar.cc/150?u=${this.userId}`
      };
      this.loading = false;
    }, 1000);
  }
};

/components/LoadingSpinner.js

这是一个用作加载占位符的组件。

// /components/LoadingSpinner.js

window.VUE_COMPONENTS = window.VUE_COMPONENTS || {};

window.VUE_COMPONENTS['LoadingSpinner'] = {
  template: `
    <div class="flex items-center justify-center p-8">
      <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
      <p class="ml-4 text-gray-500">组件加载中...</p>
    </div>
  `
};
第2步:创建主 index.html 文件

这是所有逻辑的核心。它包含了Vue的CDN链接、主应用实例、自定义加载器和页面布局。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue 3 动态组件加载</title>
    <!-- 引入 Vue 3 -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <!-- 引入 Tailwind CSS 用于美化 (可选) -->
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">

    <div id="app" class="max-w-4xl mx-auto p-4 space-y-6">
        <!-- 头部组件将在这里渲染 -->
        <app-header></app-header>

        <main class="space-y-4">
            <h2 class="text-2xl font-semibold text-gray-700">主控制区</h2>

            <div class="flex space-x-4">
                <button @click="showProfile = !showProfile" 
                        class="px-4 py-2 font-semibold rounded-md text-white transition"
                        :class="showProfile ? 'bg-red-500 hover:bg-red-600' : 'bg-emerald-500 hover:bg-emerald-600'">
                    {{ showProfile ? '隐藏' : '显示' }}用户资料
                </button>
            </div>

            <!-- 动态组件将在这里渲染 -->
            <transition name="fade" mode="out-in">
                <user-profile v-if="showProfile" user-id="123"></user-profile>
            </transition>
        </main>
    </div>

    <script>
        const { createApp, defineAsyncComponent, ref } = Vue;

        /**
         * 动态组件加载器
         * @param {string} url - 组件JS文件的URL
         * @param {string} componentName - 在 window.VUE_COMPONENTS 中注册的组件名
         * @returns {Promise<Object>} - 解析为Vue组件对象的Promise
         */
        function loadComponentFromURL(url, componentName) {
            // 使用一个全局Map来防止重复加载同一个脚本
            window.LOADED_SCRIPTS = window.LOADED_SCRIPTS || new Map();

            if (window.LOADED_SCRIPTS.has(url)) {
                // 如果脚本已加载,直接返回解析组件的Promise
                return window.LOADED_SCRIPTS.get(url);
            }

            const promise = new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = url;
                script.async = true;

                script.onload = () => {
                    // 脚本加载成功后,从全局对象中获取组件
                    const component = window.VUE_COMPONENTS?.[componentName];
                    if (component) {
                        resolve(component);
                    } else {
                        reject(new Error(`组件 ${componentName} 在 ${url} 加载后未找到。`));
                    }
                };

                script.onerror = () => {
                    reject(new Error(`加载脚本失败: ${url}`));
                };

                document.head.appendChild(script);
            });

            // 缓存Promise,这样即使多次同时请求也只会加载一次
            window.LOADED_SCRIPTS.set(url, promise);
            return promise;
        }

        // 创建Vue应用
        const app = createApp({
            setup() {
                const showProfile = ref(false);
                return { showProfile };
            }
        });

        // 使用 defineAsyncComponent 注册所有动态组件
        app.component('AppHeader', defineAsyncComponent({
            loader: () => loadComponentFromURL('./components/Header.js', 'AppHeader'),
            // 在异步组件加载时显示的组件
            loadingComponent: { template: '<div class="h-14 bg-gray-200 animate-pulse"></div>' },
            delay: 200, // 200ms后才显示loading
        }));

        app.component('UserProfile', defineAsyncComponent({
            loader: () => loadComponentFromURL('./components/UserProfile.js', 'UserProfile'),
            loadingComponent: defineAsyncComponent( // 加载组件本身也可以是异步的
                 () => loadComponentFromURL('./components/LoadingSpinner.js', 'LoadingSpinner')
            ),
            errorComponent: { template: '<div class="p-4 bg-red-100 text-red-700 rounded">组件加载失败!</div>' },
            delay: 200,
            timeout: 5000 // 5秒后超时
        }));

        // 挂载应用
        app.mount('#app');
    </script>

    <style>
        .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
        .fade-enter-from, .fade-leave-to { opacity: 0; }
    </style>

</body>
</html>

需要在webserver下运行

网友回复

我知道答案,我要回答