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


