Files
test-repo/docs/.vitepress/theme/components/appendix/frontend-engineering/BundlerComparisonDemo.vue
T

761 lines
18 KiB
Vue
Raw Normal View History

<!--
BundlerComparisonDemo.vue
打包工具对比演示 (Vite/Webpack/Rollup)
用途
直观对比三大主流打包工具的差异和适用场景
交互功能
- 工具切换对比 ViteWebpackRollup
- 维度对比构建速度配置复杂度生态丰富度等
- 场景推荐根据项目类型推荐最适合的工具
-->
<template>
<div class="bundler-comparison-demo">
<div class="control-panel">
<div class="title-section">
<span class="icon"></span>
<span class="title">打包工具对比</span>
<span class="subtitle">Vite vs Webpack vs Rollup</span>
</div>
<div class="view-controls">
<button
v-for="view in viewModes"
:key="view.id"
class="view-btn"
:class="{ active: currentView === view.id }"
@click="currentView = view.id"
>
{{ view.icon }} {{ view.name }}
</button>
</div>
</div>
<!-- 雷达图对比视图 -->
<div v-if="currentView === 'radar'" class="radar-view">
<div class="radar-container">
<svg viewBox="0 0 400 400" class="radar-chart">
<!-- 背景网格 -->
<g class="grid">
<polygon
v-for="i in 5"
:key="i"
:points="getGridPoints(i * 20)"
fill="none"
stroke="var(--vp-c-divider)"
stroke-width="1"
/>
<!-- 轴线 -->
<line
v-for="(dim, i) in dimensions"
:key="i"
:x1="200"
:y1="200"
:x2="getAxisEnd(i).x"
:y2="getAxisEnd(i).y"
stroke="var(--vp-c-divider)"
stroke-width="1"
/>
</g>
<!-- 数据区域 -->
<g class="data-areas">
<polygon
v-for="(tool, toolIndex) in bundlers"
:key="tool.id"
:points="getDataPoints(tool.scores)"
:fill="tool.color"
:stroke="tool.borderColor"
fill-opacity="0.2"
stroke-width="2"
class="data-polygon"
:class="{ dimmed: highlightedTool && highlightedTool !== tool.id }"
@mouseenter="highlightedTool = tool.id"
@mouseleave="highlightedTool = null"
/>
</g>
<!-- 维度标签 -->
<g class="labels">
<text
v-for="(dim, i) in dimensions"
:key="i"
:x="getLabelPos(i).x"
:y="getLabelPos(i).y"
text-anchor="middle"
dominant-baseline="middle"
fill="var(--vp-c-text-1)"
font-size="12"
font-weight="bold"
>
{{ dim.name }}
</text>
</g>
</svg>
</div>
<!-- 图例 -->
<div class="legend">
<div
v-for="tool in bundlers"
:key="tool.id"
class="legend-item"
:class="{ dimmed: highlightedTool && highlightedTool !== tool.id }"
@mouseenter="highlightedTool = tool.id"
@mouseleave="highlightedTool = null"
>
<span class="legend-color" :style="{ background: tool.borderColor }"></span>
<span class="legend-name">{{ tool.name }}</span>
<span class="legend-desc">{{ tool.shortDesc }}</span>
</div>
</div>
</div>
<!-- 表格对比视图 -->
<div v-else-if="currentView === 'table'" class="table-view">
<table class="comparison-table">
<thead>
<tr>
<th>对比维度</th>
<th v-for="tool in bundlers" :key="tool.id">
<span class="tool-header">
<span class="tool-icon" :style="{ background: tool.borderColor }">{{ tool.icon }}</span>
{{ tool.name }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(dim, dimIndex) in dimensions" :key="dim.key">
<td class="dim-name">
<span class="dim-icon">{{ dim.icon }}</span>
{{ dim.name }}
</td>
<td
v-for="tool in bundlers"
:key="tool.id"
class="score-cell"
>
<div class="score-bar-wrapper">
<div
class="score-bar"
:style="{
width: `${tool.scores[dimIndex] * 10}%`,
background: tool.borderColor
}"
></div>
<span class="score-value">{{ tool.scores[dimIndex] }}/10</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 场景推荐视图 -->
<div v-else-if="currentView === 'recommend'" class="recommend-view">
<div class="scenario-list">
<div
v-for="scenario in scenarios"
:key="scenario.id"
class="scenario-card"
:class="{ expanded: expandedScenario === scenario.id }"
>
<div
class="scenario-header"
@click="toggleScenario(scenario.id)"
>
<span class="scenario-icon">{{ scenario.icon }}</span>
<div class="scenario-title-wrap">
<span class="scenario-name">{{ scenario.name }}</span>
<span class="scenario-desc">{{ scenario.shortDesc }}</span>
</div>
<span class="expand-icon">{{ expandedScenario === scenario.id ? '▼' : '▶' }}</span>
</div>
<div v-if="expandedScenario === scenario.id" class="scenario-content">
<div class="recommendation">
<div class="best-choice">
<span class="choice-label">🏆 首选推荐</span>
<div class="choice-content">
<span
class="tool-badge"
:style="{ background: getTool(scenario.bestChoice).borderColor }"
>
{{ getTool(scenario.bestChoice).icon }} {{ getTool(scenario.bestChoice).name }}
</span>
<p class="choice-reason">{{ scenario.bestReason }}</p>
</div>
</div>
<div v-if="scenario.alternative" class="alternative">
<span class="choice-label">🥈 备选方案</span>
<div class="choice-content">
<span
class="tool-badge alt"
:style="{ background: getTool(scenario.alternative).borderColor }"
>
{{ getTool(scenario.alternative).icon }} {{ getTool(scenario.alternative).name }}
</span>
<p class="choice-reason">{{ scenario.altReason }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">💡</span>
<strong>选择建议</strong>
{{ currentView === 'radar' ? '雷达图展示了各工具在多个维度的能力分布,面积越大代表综合能力越强。' :
currentView === 'table' ? '表格详细对比了各工具在每个维度的具体得分,方便精确对比。' :
'根据你的项目类型和团队情况,选择最适合的工具往往比选择"最好"的工具更重要。' }}
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentView = ref('radar')
const highlightedTool = ref(null)
const expandedScenario = ref(null)
const viewModes = [
{ id: 'radar', name: '雷达图', icon: '📊' },
{ id: 'table', name: '对比表', icon: '📋' },
{ id: 'recommend', name: '场景推荐', icon: '🎯' }
]
const dimensions = [
{ key: 'speed', name: '构建速度', icon: '⚡' },
{ key: 'config', name: '配置难度', icon: '🔧' },
{ key: 'ecosystem', name: '生态丰富', icon: '📦' },
{ key: 'hmr', name: '热更新速度', icon: '🔥' },
{ key: 'output', name: '产物优化', icon: '✨' },
{ key: 'memory', name: '内存占用', icon: '💾' }
]
const bundlers = [
{
id: 'vite',
name: 'Vite',
icon: '⚡',
shortDesc: '下一代前端构建工具',
color: 'rgba(100, 108, 255, 0.3)',
borderColor: '#646cff',
scores: [10, 8, 7, 10, 8, 9],
features: ['原生 ESM', '极速 HMR', '基于 esbuild']
},
{
id: 'webpack',
name: 'Webpack',
icon: '📦',
shortDesc: '老牌强大的打包工具',
color: 'rgba(142, 214, 251, 0.3)',
borderColor: '#8ed6fb',
scores: [5, 5, 10, 6, 9, 5],
features: ['生态最丰富', 'loader/plugin 多', '配置灵活']
},
{
id: 'rollup',
name: 'Rollup',
icon: '📜',
shortDesc: 'JavaScript 模块打包器',
color: 'rgba(255, 107, 107, 0.3)',
borderColor: '#ff6b6b',
scores: [7, 7, 6, 7, 10, 8],
features: ['Tree Shaking', '输出最优', '适合库开发']
}
]
const scenarios = [
{
id: 'spa',
icon: '🚀',
name: '中小型 SPA 项目',
shortDesc: '单页应用,快速开发',
bestChoice: 'vite',
bestReason: 'Vite 的极速冷启动和热更新让开发体验极佳,配置简单,是中小型项目的首选。',
alternative: 'webpack',
altReason: '如果需要大量自定义配置或依赖特定的 webpack loaderwebpack 仍然是可靠的选择。'
},
{
id: 'library',
icon: '📚',
name: 'JavaScript 库/组件库',
shortDesc: '打包发布 npm 包',
bestChoice: 'rollup',
bestReason: 'Rollup 生成的代码最干净,Tree Shaking 效果最好,非常适合打包 JavaScript 库。',
alternative: 'vite',
altReason: 'Vite 使用 Rollup 进行生产构建,同时提供更好的开发体验,也是现代库开发的好选择。'
},
{
id: 'enterprise',
icon: '🏢',
name: '大型企业级应用',
shortDesc: '复杂业务,多人协作',
bestChoice: 'webpack',
bestReason: 'Webpack 生态最成熟,loader 和 plugin 最丰富,能应对各种复杂场景和定制化需求。',
alternative: 'vite',
altReason: '如果团队追求更好的开发体验,且项目不需要太多自定义构建逻辑,Vite 也是值得考虑的选项。'
},
{
id: 'ssg',
icon: '📝',
name: '静态站点生成 (SSG)',
shortDesc: '文档站、博客、营销页',
bestChoice: 'vite',
bestReason: 'VitePress、Astro 等现代 SSG 工具都基于 Vite,开发体验好,构建速度快。',
alternative: 'rollup',
altReason: '一些轻量级 SSG 工具直接使用 Rollup,如果对产物体积要求极高可以考虑。'
}
]
// 雷达图计算
const getGridPoints = (radius) => {
const points = []
for (let i = 0; i < 6; i++) {
const angle = (i * 60 - 90) * Math.PI / 180
const x = 200 + radius * Math.cos(angle)
const y = 200 + radius * Math.sin(angle)
points.push(`${x},${y}`)
}
return points.join(' ')
}
const getAxisEnd = (index) => {
const angle = (index * 60 - 90) * Math.PI / 180
return {
x: 200 + 100 * Math.cos(angle),
y: 200 + 100 * Math.sin(angle)
}
}
const getLabelPos = (index) => {
const angle = (index * 60 - 90) * Math.PI / 180
return {
x: 200 + 125 * Math.cos(angle),
y: 200 + 125 * Math.sin(angle)
}
}
const getDataPoints = (scores) => {
const points = []
for (let i = 0; i < scores.length; i++) {
const angle = (i * 60 - 90) * Math.PI / 180
const radius = scores[i] * 10
const x = 200 + radius * Math.cos(angle)
const y = 200 + radius * Math.sin(angle)
points.push(`${x},${y}`)
}
return points.join(' ')
}
const getTool = (id) => bundlers.find(b => b.id === id)
const toggleScenario = (id) => {
expandedScenario.value = expandedScenario.value === id ? null : id
}
const togglePlay = () => {
// Placeholder for play functionality in this component
}
const reset = () => {
// Placeholder for reset functionality
}
</script>
<style scoped>
.bundler-comparison-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background-color: var(--vp-c-bg-soft);
padding: 0.75rem;
margin: 0.5rem 0;
font-family: var(--vp-font-family-mono);
}
.control-panel {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
background: var(--vp-c-bg);
padding: 0.75rem;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
gap: 1rem;
}
.title-section {
display: flex;
align-items: center;
gap: 0.5rem;
}
.title-section .icon {
font-size: 1.5rem;
}
.title-section .title {
font-weight: bold;
font-size: 1.1rem;
}
.title-section .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.view-controls {
display: flex;
gap: 0.25rem;
}
.view-btn {
padding: 0.35rem 0.75rem;
border-radius: 4px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s;
}
.view-btn:hover {
background: var(--vp-c-bg-alt);
}
.view-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
/* 雷达图视图 */
.radar-view {
display: grid;
grid-template-columns: 1fr 280px;
gap: 1rem;
margin-bottom: 1rem;
}
.radar-container {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
}
.radar-chart {
width: 100%;
max-width: 350px;
height: auto;
}
.data-polygon {
transition: all 0.3s ease;
cursor: pointer;
}
.data-polygon:hover {
fill-opacity: 0.4;
}
.data-polygon.dimmed {
fill-opacity: 0.1;
opacity: 0.3;
}
.legend {
background: var(--vp-c-bg);
border-radius: 6px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 6px;
transition: all 0.2s;
cursor: pointer;
}
.legend-item:hover {
background: var(--vp-c-bg-soft);
}
.legend-item.dimmed {
opacity: 0.3;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
}
.legend-name {
font-weight: bold;
font-size: 0.9rem;
}
.legend-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-left: auto;
}
/* 表格视图 */
.table-view {
margin-bottom: 1rem;
overflow-x: auto;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
background: var(--vp-c-bg);
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
font-size: 0.85rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--vp-c-divider);
}
.comparison-table th {
background: var(--vp-c-bg-soft);
font-weight: bold;
}
.comparison-table tr:last-child td {
border-bottom: none;
}
.tool-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.tool-icon {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.dim-name {
display: flex;
align-items: center;
gap: 0.3rem;
font-weight: 500;
}
.score-cell {
min-width: 120px;
}
.score-bar-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.score-bar {
height: 8px;
border-radius: 4px;
min-width: 20px;
transition: width 0.3s ease;
}
.score-value {
font-size: 0.75rem;
color: var(--vp-c-text-2);
white-space: nowrap;
}
/* 推荐视图 */
.recommend-view {
margin-bottom: 1rem;
}
.scenario-list {
display: grid;
gap: 0.75rem;
}
.scenario-card {
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
transition: all 0.2s;
}
.scenario-card:hover {
border-color: var(--vp-c-brand);
}
.scenario-card.expanded {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.scenario-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background 0.2s;
}
.scenario-header:hover {
background: var(--vp-c-bg-soft);
}
.scenario-icon {
font-size: 1.5rem;
}
.scenario-title-wrap {
flex: 1;
display: flex;
flex-direction: column;
}
.scenario-name {
font-weight: bold;
font-size: 0.95rem;
}
.scenario-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
}
.expand-icon {
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.scenario-content {
padding: 0 1rem 1rem;
border-top: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
}
.recommendation {
display: grid;
gap: 0.75rem;
margin-top: 0.75rem;
}
.best-choice,
.alternative {
display: grid;
grid-template-columns: 100px 1fr;
gap: 0.75rem;
align-items: start;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.choice-label {
font-size: 0.75rem;
font-weight: bold;
color: var(--vp-c-text-2);
padding-top: 0.3rem;
}
.choice-content {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.tool-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0.6rem;
border-radius: 4px;
color: white;
font-weight: bold;
font-size: 0.85rem;
width: fit-content;
}
.tool-badge.alt {
opacity: 0.85;
}
.choice-reason {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin: 0;
line-height: 1.4;
}
.info-box {
background-color: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
line-height: 1.4;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 0.5rem;
}
@media (max-width: 768px) {
.radar-view {
grid-template-columns: 1fr;
}
.control-panel {
flex-direction: column;
align-items: flex-start;
}
.comparison-table {
font-size: 0.75rem;
}
.comparison-table th,
.comparison-table td {
padding: 0.5rem;
}
}
</style>