好的,这是一个非常实际且有用的需求,尤其是在不使用构建工具(如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下运行
网友回复